diff --git a/README.md b/README.md
index 8a666de..f3e0b6d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,9 @@
[](https://travis-ci.org/css-modules/css-modules-loader-core)
-## API
+## Async API
+
+The async API is default, simply load `css-modules-loader-core`
```js
import Core from 'css-modules-loader-core'
@@ -12,13 +14,33 @@ let core = new Core()
### core.load( sourceString , sourcePath , pathFetcher ) =>
Promise({ injectableSource, exportTokens })
-Processes the input CSS `sourceString`, looking for dependencies such as `@import` or `:import`. Any localisation will happen by prefixing a sanitised version of `sourcePath` When dependencies are found, it will ask the `pathFetcher` for each dependency, resolve & inline any imports, and return the following object:
+Processes the input CSS `sourceString`, looking for dependencies such as `@import` or `:import`. Any localisation will happen by prefixing a sanitised version of `sourcePath` When dependencies are found, it will ask the `pathFetcher` for each dependency, asynchronously resolve & inline any imports, and return the following object:
+
+- `injectableSource`: the final, merged CSS file without `@import` or `:import` statements
+- `exportTokens`: the mapping from local name to scoped name, as described in the file's `:export` block
+
+These should map nicely to what your build-tool-specific loader needs to do its job.
+
+
+## Sync API
+
+The sync API is available at `css-modules-loader-core/sync`
+
+```js
+import Core from 'css-modules-loader-core/sync'
+let core = new Core()
+```
+
+### core.load( sourceString , sourcePath , pathFetcher ) =>
{ injectableSource, exportTokens }
+
+Processes the input CSS `sourceString`, looking for dependencies such as `@import` or `:import`. Any localisation will happen by prefixing a sanitised version of `sourcePath` When dependencies are found, it will ask the `pathFetcher` for each dependency, synchronously resolve & inline any imports, and return the following object:
- `injectableSource`: the final, merged CSS file without `@import` or `:import` statements
- `exportTokens`: the mapping from local name to scoped name, as described in the file's `:export` block
These should map nicely to what your build-tool-specific loader needs to do its job.
+
### new Core([plugins])
The default set of plugins is [[postcss-modules-local-by-default](https://github.com/css-modules/postcss-modules-local-by-default), [postcss-modules-extract-imports](https://github.com/css-modules/postcss-modules-extract-imports), [postcss-modules-scope](https://github.com/css-modules/postcss-modules-scope)] (i.e. the CSS Modules specification). This can override which PostCSS plugins you wish to execute, e.g.
diff --git a/package.json b/package.json
index ecec1f9..3dd3b52 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,6 @@
"name": "css-modules-loader-core",
"version": "0.0.12",
"description": "A loader-agnostic CSS Modules implementation, based on PostCSS",
- "main": "lib/index.js",
"directories": {
"test": "test"
},
@@ -37,7 +36,8 @@
"loader"
],
"files": [
- "lib"
+ "lib",
+ "sync.js"
],
"author": "Glen Maddern",
"license": "ISC",
diff --git a/src/file-system-loader.js b/src/async/file-system-loader.js
similarity index 100%
rename from src/file-system-loader.js
rename to src/async/file-system-loader.js
diff --git a/src/async/index.js b/src/async/index.js
new file mode 100644
index 0000000..f5994ef
--- /dev/null
+++ b/src/async/index.js
@@ -0,0 +1,29 @@
+import postcss from 'postcss'
+import localByDefault from 'postcss-modules-local-by-default'
+import extractImports from 'postcss-modules-extract-imports'
+import scope from 'postcss-modules-scope'
+
+import Parser from './parser'
+
+export default class Core {
+ constructor( plugins ) {
+ this.plugins = plugins || Core.defaultPlugins
+ }
+
+ load( sourceString, sourcePath, trace, pathFetcher ) {
+ let parser = new Parser( pathFetcher, trace )
+
+ return postcss( this.plugins.concat( [parser.plugin] ) )
+ .process( sourceString, { from: "/" + sourcePath } )
+ .then( result => {
+ return { injectableSource: result.css, exportTokens: parser.exportTokens }
+ } )
+ }
+}
+
+
+// These three plugins are aliased under this package for simplicity.
+Core.localByDefault = localByDefault
+Core.extractImports = extractImports
+Core.scope = scope
+Core.defaultPlugins = [localByDefault, extractImports, scope]
diff --git a/src/parser.js b/src/async/parser.js
similarity index 100%
rename from src/parser.js
rename to src/async/parser.js
diff --git a/src/index.js b/src/index.js
index f5994ef..4cf6bd5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,29 +1,7 @@
-import postcss from 'postcss'
-import localByDefault from 'postcss-modules-local-by-default'
-import extractImports from 'postcss-modules-extract-imports'
-import scope from 'postcss-modules-scope'
+import async from './async'
+import _sync from './sync'
-import Parser from './parser'
-export default class Core {
- constructor( plugins ) {
- this.plugins = plugins || Core.defaultPlugins
- }
- load( sourceString, sourcePath, trace, pathFetcher ) {
- let parser = new Parser( pathFetcher, trace )
-
- return postcss( this.plugins.concat( [parser.plugin] ) )
- .process( sourceString, { from: "/" + sourcePath } )
- .then( result => {
- return { injectableSource: result.css, exportTokens: parser.exportTokens }
- } )
- }
-}
-
-
-// These three plugins are aliased under this package for simplicity.
-Core.localByDefault = localByDefault
-Core.extractImports = extractImports
-Core.scope = scope
-Core.defaultPlugins = [localByDefault, extractImports, scope]
+export default async
+export const sync = _sync
\ No newline at end of file
diff --git a/src/sync/file-system-loader.js b/src/sync/file-system-loader.js
new file mode 100644
index 0000000..cf1cf8e
--- /dev/null
+++ b/src/sync/file-system-loader.js
@@ -0,0 +1,62 @@
+import Core from './index.js'
+import fs from 'fs'
+import path from 'path'
+
+// Sorts dependencies in the following way:
+// AAA comes before AA and A
+// AB comes after AA and before A
+// All Bs come after all As
+// This ensures that the files are always returned in the following order:
+// - In the order they were required, except
+// - After all their dependencies
+const traceKeySorter = ( a, b ) => {
+ if ( a.length < b.length ) {
+ return a < b.substring( 0, a.length ) ? -1 : 1
+ } else if ( a.length > b.length ) {
+ return a.substring( 0, b.length ) <= b ? -1 : 1
+ } else {
+ return a < b ? -1 : 1
+ }
+};
+
+export default class FileSystemLoader {
+ constructor( root, plugins ) {
+ this.root = root
+ this.sources = {}
+ this.importNr = 0
+ this.core = new Core(plugins)
+ this.tokensByFile = {};
+ }
+
+ fetch( _newPath, relativeTo, _trace ) {
+ let newPath = _newPath.replace( /^["']|["']$/g, "" ),
+ trace = _trace || String.fromCharCode( this.importNr++ )
+ let relativeDir = path.dirname( relativeTo ),
+ rootRelativePath = path.resolve( relativeDir, newPath ),
+ fileRelativePath = path.resolve( path.join( this.root, relativeDir ), newPath )
+
+ // if the path is not relative or absolute, try to resolve it in node_modules
+ if (newPath[0] !== '.' && newPath[0] !== '/') {
+ try {
+ fileRelativePath = require.resolve(newPath);
+ }
+ catch (e) {}
+ }
+
+ const tokens = this.tokensByFile[fileRelativePath]
+ if (tokens) { return tokens }
+
+ const source = fs.readFileSync( fileRelativePath, "utf-8")
+ const { injectableSource, exportTokens } = this.core.load( source, rootRelativePath, trace, this.fetch.bind( this ) )
+
+ this.sources[trace] = injectableSource
+ this.tokensByFile[fileRelativePath] = exportTokens
+ return exportTokens
+
+ }
+
+ get finalSource() {
+ return Object.keys( this.sources ).sort( traceKeySorter ).map( s => this.sources[s] )
+ .join( "" )
+ }
+}
diff --git a/src/sync/index.js b/src/sync/index.js
new file mode 100644
index 0000000..635ad45
--- /dev/null
+++ b/src/sync/index.js
@@ -0,0 +1,28 @@
+import postcss from 'postcss'
+import localByDefault from 'postcss-modules-local-by-default'
+import extractImports from 'postcss-modules-extract-imports'
+import scope from 'postcss-modules-scope'
+
+import Parser from './parser'
+
+export default class Core {
+ constructor( plugins ) {
+ this.plugins = plugins || Core.defaultPlugins
+ }
+
+ load( sourceString, sourcePath, trace, pathFetcher ) {
+ let parser = new Parser( pathFetcher, trace )
+
+ const result = postcss( this.plugins.concat( [parser.plugin] ) )
+ .process( sourceString, { from: "/" + sourcePath } )
+
+ return { injectableSource: result.css, exportTokens: parser.exportTokens }
+ }
+}
+
+
+// These three plugins are aliased under this package for simplicity.
+Core.localByDefault = localByDefault
+Core.extractImports = extractImports
+Core.scope = scope
+Core.defaultPlugins = [localByDefault, extractImports, scope]
diff --git a/src/sync/parser.js b/src/sync/parser.js
new file mode 100644
index 0000000..d1daaf4
--- /dev/null
+++ b/src/sync/parser.js
@@ -0,0 +1,69 @@
+const importRegexp = /^:import\((.+)\)$/
+
+export default class Parser {
+ constructor( pathFetcher, trace ) {
+ this.pathFetcher = pathFetcher
+ this.plugin = this.plugin.bind( this )
+ this.exportTokens = {}
+ this.translations = {}
+ this.trace = trace
+ }
+
+ plugin( css, result ) {
+
+ this.fetchAllImports( css )
+ this.linkImportedSymbols( css )
+ this.extractExports( css )
+
+ return result
+ }
+
+ fetchAllImports( css ) {
+ let imports = []
+ css.each( node => {
+ if ( node.type == "rule" && node.selector.match( importRegexp ) ) {
+ imports.push( this.fetchImport( node, css.source.input.from, imports.length ) )
+ }
+ } )
+ return imports
+ }
+
+ linkImportedSymbols( css ) {
+ css.eachDecl( decl => {
+ Object.keys(this.translations).forEach( translation => {
+ decl.value = decl.value.replace(translation, this.translations[translation])
+ } )
+ })
+ }
+
+ extractExports( css ) {
+ css.each( node => {
+ if ( node.type == "rule" && node.selector == ":export" ) this.handleExport( node )
+ } )
+ }
+
+ handleExport( exportNode ) {
+ exportNode.each( decl => {
+ if ( decl.type == 'decl' ) {
+ Object.keys(this.translations).forEach( translation => {
+ decl.value = decl.value.replace(translation, this.translations[translation])
+ } )
+ this.exportTokens[decl.prop] = decl.value
+ }
+ } )
+ exportNode.removeSelf()
+ }
+
+ fetchImport( importNode, relativeTo, depNr ) {
+ let file = importNode.selector.match( importRegexp )[1],
+ depTrace = this.trace + String.fromCharCode(depNr)
+ const exports = this.pathFetcher( file, relativeTo, depTrace )
+ importNode.each( decl => {
+ if ( decl.type == 'decl' ) {
+ this.translations[decl.prop] = exports[decl.value]
+ }
+ } )
+ importNode.removeSelf()
+
+ }
+}
diff --git a/sync.js b/sync.js
new file mode 100644
index 0000000..0bfac6c
--- /dev/null
+++ b/sync.js
@@ -0,0 +1 @@
+module.exports = require('./lib/sync')
\ No newline at end of file
diff --git a/test/test-cases.js b/test/test-cases.js
index c6adcff..b301bb2 100644
--- a/test/test-cases.js
+++ b/test/test-cases.js
@@ -3,7 +3,9 @@
import assert from "assert"
import fs from "fs"
import path from "path"
-import FileSystemLoader from "../src/file-system-loader"
+import AsyncFileSystemLoader from "../src/async/file-system-loader"
+import SyncFileSystemLoader from "../src/sync/file-system-loader"
+
let normalize = ( str ) => {
return str.replace( /\r\n?/g, "\n" );
@@ -19,9 +21,9 @@ Object.keys( pipelines ).forEach( dirname => {
let testDir = path.join( __dirname, dirname )
fs.readdirSync( testDir ).forEach( testCase => {
if ( fs.existsSync( path.join( testDir, testCase, "source.css" ) ) ) {
- it( "should " + testCase.replace( /-/g, " " ), done => {
+ it( "should " + testCase.replace( /-/g, " " ) + '(async)', done => {
let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) )
- let loader = new FileSystemLoader( testDir, pipelines[dirname] )
+ let loader = new AsyncFileSystemLoader( testDir, pipelines[dirname] )
let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) )
loader.fetch( `${testCase}/source.css`, "/" ).then( tokens => {
assert.equal( loader.finalSource, expected )
@@ -34,14 +36,14 @@ Object.keys( pipelines ).forEach( dirname => {
} )
// special case for testing multiple sources
-describe( 'multiple sources', () => {
+describe( 'multiple sources async', () => {
let testDir = path.join( __dirname, 'test-cases' )
let testCase = 'multiple-sources';
let dirname = 'test-cases';
if ( fs.existsSync( path.join( testDir, testCase, "source1.css" ) ) ) {
- it( "should " + testCase.replace( /-/g, " " ), done => {
+ it( "should " + testCase.replace( /-/g, " " ) + '(async)', done => {
let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) )
- let loader = new FileSystemLoader( testDir, pipelines[dirname] )
+ let loader = new AsyncFileSystemLoader( testDir, pipelines[dirname] )
let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) )
loader.fetch( `${testCase}/source1.css`, "/" ).then( tokens1 => {
loader.fetch( `${testCase}/source2.css`, "/" ).then( tokens2 => {
@@ -53,3 +55,43 @@ describe( 'multiple sources', () => {
} );
}
} );
+
+
+
+Object.keys( pipelines ).forEach( dirname => {
+ describe( dirname, () => {
+ let testDir = path.join( __dirname, dirname )
+ fs.readdirSync( testDir ).forEach( testCase => {
+ if ( fs.existsSync( path.join( testDir, testCase, "source.css" ) ) ) {
+ it( "should " + testCase.replace( /-/g, " " ) + '(sync)', () => {
+ let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) )
+ let loader = new SyncFileSystemLoader( testDir, pipelines[dirname] )
+ let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) )
+ let tokens = loader.fetch( `${testCase}/source.css`, "/" )
+ assert.equal( loader.finalSource, expected )
+ assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) )
+
+ } );
+ }
+ } );
+ } );
+} )
+
+// special case for testing multiple sources
+describe( 'multiple sources async', () => {
+ let testDir = path.join( __dirname, 'test-cases' )
+ let testCase = 'multiple-sources';
+ let dirname = 'test-cases';
+ if ( fs.existsSync( path.join( testDir, testCase, "source1.css" ) ) ) {
+ it( "should " + testCase.replace( /-/g, " " ) + '(sync)', () => {
+ let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) )
+ let loader = new SyncFileSystemLoader( testDir, pipelines[dirname] )
+ let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) )
+ let tokens1 = loader.fetch( `${testCase}/source1.css`, "/" )
+ let tokens2 = loader.fetch( `${testCase}/source2.css`, "/" )
+ assert.equal( loader.finalSource, expected )
+ const tokens = Object.assign({}, tokens1, tokens2);
+ assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) )
+ } );
+ }
+} );