Skip to content

sync api #26

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -12,13 +14,33 @@ let core = new Core()

### core.load( sourceString , sourcePath , pathFetcher ) =><br>&nbsp;&nbsp;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 ) =><br>&nbsp;&nbsp;{ 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.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down Expand Up @@ -37,7 +36,8 @@
"loader"
],
"files": [
"lib"
"lib",
"sync.js"
],
"author": "Glen Maddern",
"license": "ISC",
Expand Down
File renamed without changes.
29 changes: 29 additions & 0 deletions src/async/index.js
Original file line number Diff line number Diff line change
@@ -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]
File renamed without changes.
30 changes: 4 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions src/sync/file-system-loader.js
Original file line number Diff line number Diff line change
@@ -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( "" )
}
}
28 changes: 28 additions & 0 deletions src/sync/index.js
Original file line number Diff line number Diff line change
@@ -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]
69 changes: 69 additions & 0 deletions src/sync/parser.js
Original file line number Diff line number Diff line change
@@ -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()

}
}
1 change: 1 addition & 0 deletions sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./lib/sync')
54 changes: 48 additions & 6 deletions test/test-cases.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" );
Expand All @@ -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 )
Expand All @@ -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 => {
Expand All @@ -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 ) )
} );
}
} );