Skip to content

The.sync.way #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/file-system-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( "" )
Expand Down
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -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 }
}
}


Expand Down
65 changes: 65 additions & 0 deletions src/synchronous-parser.js
Original file line number Diff line number Diff line change
@@ -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()
}
}
105 changes: 73 additions & 32 deletions test/test-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) )
} );
}
} );
} );