From 6ed995132f141cb03511dd62b8f33de03e330faf Mon Sep 17 00:00:00 2001 From: davidmarkclements Date: Tue, 8 Sep 2015 21:47:18 +0100 Subject: [PATCH 1/3] sync api --- package.json | 1 - src/{ => async}/file-system-loader.js | 0 src/async/index.js | 29 +++++++++++ src/{ => async}/parser.js | 0 src/index.js | 30 ++---------- src/sync/file-system-loader.js | 62 ++++++++++++++++++++++++ src/sync/index.js | 28 +++++++++++ src/sync/parser.js | 69 +++++++++++++++++++++++++++ sync.js | 1 + test/test-cases.js | 54 ++++++++++++++++++--- 10 files changed, 241 insertions(+), 33 deletions(-) rename src/{ => async}/file-system-loader.js (100%) create mode 100644 src/async/index.js rename src/{ => async}/parser.js (100%) create mode 100644 src/sync/file-system-loader.js create mode 100644 src/sync/index.js create mode 100644 src/sync/parser.js create mode 100644 sync.js diff --git a/package.json b/package.json index ecec1f9..a8595d9 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" }, 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 ) ) + } ); + } +} ); From ed51d5869c8f89e618b3426682f68527fbf7c189 Mon Sep 17 00:00:00 2001 From: davidmarkclements Date: Tue, 8 Sep 2015 22:10:03 +0100 Subject: [PATCH 2/3] doc sync api --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8a666de..f3e0b6d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,9 @@ [![Build Status](https://travis-ci.org/css-modules/css-modules-loader-core.svg?branch=master)](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. From eb9fd4854d0ca3901ec7e577c7c7769373aadf2d Mon Sep 17 00:00:00 2001 From: davidmarkclements Date: Wed, 9 Sep 2015 10:03:27 +0100 Subject: [PATCH 3/3] add sync.js to package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a8595d9..3dd3b52 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "loader" ], "files": [ - "lib" + "lib", + "sync.js" ], "author": "Glen Maddern", "license": "ISC",