diff --git a/src/file-system-loader.js b/src/file-system-loader.js index 2f9ab02..9382db9 100644 --- a/src/file-system-loader.js +++ b/src/file-system-loader.js @@ -51,6 +51,25 @@ export default class FileSystemLoader { } ) } + fetchSync( _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 ) + + const tokens = this.tokensByFile[fileRelativePath] + if (tokens) { return tokens } + + let source = fs.readFileSync( fileRelativePath, "utf-8" ) + // May occur an error while loading async plugins + let { injectableSource, exportTokens } = this.core.loadSync( source, rootRelativePath, trace, this.fetchSync.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/index.js b/src/index.js index f5994ef..2ab8a05 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import extractImports from 'postcss-modules-extract-imports' import scope from 'postcss-modules-scope' import Parser from './parser' +import SyncParser from './synchronous-parser' export default class Core { constructor( plugins ) { @@ -19,6 +20,16 @@ export default class Core { return { injectableSource: result.css, exportTokens: parser.exportTokens } } ) } + + loadSync( sourceString, sourcePath, trace, pathFetcher ) { + let parser = new SyncParser( pathFetcher, trace ) + + let result = postcss( this.plugins.concat( [parser.plugin] ) ) + .process( sourceString, { from: "/" + sourcePath } ) + .stringify() + + return { injectableSource: result.css, exportTokens: parser.exportTokens } + } } diff --git a/src/synchronous-parser.js b/src/synchronous-parser.js new file mode 100644 index 0000000..a9da4c6 --- /dev/null +++ b/src/synchronous-parser.js @@ -0,0 +1,65 @@ +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 ) + } + + 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) + var 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/test/test-cases.js b/test/test-cases.js index c6adcff..287e7df 100644 --- a/test/test-cases.js +++ b/test/test-cases.js @@ -14,42 +14,83 @@ const pipelines = { "cssi": [] } -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, " " ), done => { - let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) - let loader = new FileSystemLoader( testDir, pipelines[dirname] ) - let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) ) - loader.fetch( `${testCase}/source.css`, "/" ).then( tokens => { +describe( 'async api', () => { + 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, " " ), done => { + let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) + let loader = new FileSystemLoader( 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 ) + assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) ) + } ).then( done, done ) + } ); + } + } ); + } ); + } ) + + // special case for testing multiple sources + describe( 'multiple sources', () => { + 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 => { + let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) + let loader = new FileSystemLoader( 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 => { assert.equal( loader.finalSource, expected ) + const tokens = Object.assign({}, tokens1, tokens2); assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) ) } ).then( done, done ) - } ); - } - } ); + }) + } ); + } } ); -} ) +} ); -// special case for testing multiple sources -describe( 'multiple sources', () => { - 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 => { - let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) - let loader = new FileSystemLoader( 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 => { - assert.equal( loader.finalSource, expected ) - const tokens = Object.assign({}, tokens1, tokens2); - assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) ) - } ).then( done, done ) - }) +describe( 'sync api', () => { + 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, " " ), () => { + let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) + let loader = new FileSystemLoader( testDir, pipelines[dirname] ) + let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) ) + let tokens = loader.fetchSync( `${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', () => { + 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, " " ), () => { + let expected = normalize( fs.readFileSync( path.join( testDir, testCase, "expected.css" ), "utf-8" ) ) + let loader = new FileSystemLoader( testDir, pipelines[dirname] ) + let expectedTokens = JSON.parse( fs.readFileSync( path.join( testDir, testCase, "expected.json" ), "utf-8" ) ) + let tokens1 = loader.fetchSync( `${testCase}/source1.css`, "/" ) + let tokens2 = loader.fetchSync( `${testCase}/source2.css`, "/" ) + assert.equal( loader.finalSource, expected ) + const tokens = Object.assign({}, tokens1, tokens2); + assert.equal( JSON.stringify( tokens ), JSON.stringify( expectedTokens ) ) + } ); + } + } ); } );