diff --git a/package.json b/package.json index ecec1f9..f537c28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "css-modules-loader-core", - "version": "0.0.12", + "version": "1.0.0-beta1", "description": "A loader-agnostic CSS Modules implementation, based on PostCSS", "main": "lib/index.js", "directories": { @@ -8,9 +8,10 @@ }, "dependencies": { "postcss": "^4.1.11", - "postcss-modules-extract-imports": "^0.0.5", + "postcss-custom-media": "^4.1.0", + "postcss-modules-extract-imports": "^1.0.0-beta1", "postcss-modules-local-by-default": "^0.0.9", - "postcss-modules-scope": "^0.0.8" + "postcss-modules-scope": "^1.0.0-beta1" }, "devDependencies": { "babel": "^5.5.4", diff --git a/src/file-system-loader.js b/src/file-system-loader.js index 2a3f47d..4beb2f1 100644 --- a/src/file-system-loader.js +++ b/src/file-system-loader.js @@ -20,11 +20,11 @@ const traceKeySorter = ( a, b ) => { }; export default class FileSystemLoader { - constructor( root, plugins ) { + constructor( root, plugins, postLinkers ) { this.root = root this.sources = {} this.importNr = 0 - this.core = new Core(plugins) + this.core = new Core( plugins, postLinkers ) this.tokensByFile = {}; } diff --git a/src/index.js b/src/index.js index f5994ef..1e7fa56 100644 --- a/src/index.js +++ b/src/index.js @@ -2,18 +2,23 @@ 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 customMedia from 'postcss-custom-media' import Parser from './parser' export default class Core { - constructor( plugins ) { + constructor( plugins, postLinkers ) { this.plugins = plugins || Core.defaultPlugins + this.postLinkers = postLinkers || Core.defaultPostLinkers } load( sourceString, sourcePath, trace, pathFetcher ) { - let parser = new Parser( pathFetcher, trace ) + let parser = new Parser( pathFetcher, trace ), + pluginChain = this.plugins + .concat( [parser.plugin] ) + .concat( this.postLinkers ); - return postcss( this.plugins.concat( [parser.plugin] ) ) + return postcss( pluginChain ) .process( sourceString, { from: "/" + sourcePath } ) .then( result => { return { injectableSource: result.css, exportTokens: parser.exportTokens } @@ -21,9 +26,10 @@ export default class Core { } } - -// These three plugins are aliased under this package for simplicity. +// These four plugins are aliased under this package for simplicity. Core.localByDefault = localByDefault Core.extractImports = extractImports Core.scope = scope +Core.customMedia = customMedia Core.defaultPlugins = [localByDefault, extractImports, scope] +Core.defaultPostLinkers = [customMedia] diff --git a/src/parser.js b/src/parser.js index 536e9cd..56a3a25 100644 --- a/src/parser.js +++ b/src/parser.js @@ -26,10 +26,18 @@ export default class Parser { } linkImportedSymbols( css ) { - css.eachDecl( decl => { - Object.keys(this.translations).forEach( translation => { - decl.value = decl.value.replace(translation, this.translations[translation]) - } ) + css.eachInside( node => { + if ( node.type === "decl" ) { + this.replaceOccurrences( node, "value" ) + } else if ( node.type === "atrule" && node.name === "custom-media" ) { + this.replaceOccurrences( node, "params" ) + } + }) + } + + replaceOccurrences( node, prop ) { + Object.keys(this.translations).forEach(translation => { + node[prop] = node[prop].replace(translation, this.translations[translation]) }) } @@ -45,7 +53,7 @@ export default class Parser { Object.keys(this.translations).forEach( translation => { decl.value = decl.value.replace(translation, this.translations[translation]) } ) - this.exportTokens[decl.prop] = decl.value + this.exportTokens[decl.prop] = decl.value.replace(/^['"]|['"]$/g, '') } } ) exportNode.removeSelf() @@ -57,7 +65,12 @@ export default class Parser { return this.pathFetcher( file, relativeTo, depTrace ).then( exports => { importNode.each( decl => { if ( decl.type == 'decl' ) { - this.translations[decl.prop] = exports[decl.value] + let translation = exports[decl.value] + if ( translation ) { + this.translations[decl.prop] = translation.replace(/^['"]|['"]$/g, '') + } else { + console.warn( `Missing ${decl.value} for ${decl.prop}` ) + } } } ) importNode.removeSelf() diff --git a/test/cssi/media-queries/breakpoints.css b/test/cssi/media-queries/breakpoints.css new file mode 100644 index 0000000..d18fcf8 --- /dev/null +++ b/test/cssi/media-queries/breakpoints.css @@ -0,0 +1,9 @@ +:export { + --small: "(max-width: 30em)"; + --medium: "(max-width: 60em)"; + --large: "(max-width: 90em)"; +} + +@custom-media --small (max-width: 30em); +@custom-media --medium (max-width: 60em); +@custom-media --large (max-width: 90em); diff --git a/test/cssi/media-queries/expected.css b/test/cssi/media-queries/expected.css new file mode 100644 index 0000000..943c6a9 --- /dev/null +++ b/test/cssi/media-queries/expected.css @@ -0,0 +1,28 @@ +@custom-media --small (max-width: 30em); +@custom-media --medium (max-width: 60em); +@custom-media --large (max-width: 90em); +@custom-media --small (max-width: 30em); +@custom-media --medium (max-width: 60em); +@custom-media --large (max-width: 90em); + +.exported-red { + color: red; +} + +@media (--small) { + .exported-red { + color: maroon; + } +} + +@media (--medium) { + .exported-red { + color: darkmagenta; + } +} + +@media (--large) { + .exported-red { + color: fuchsia; + } +} diff --git a/test/cssi/media-queries/expected.json b/test/cssi/media-queries/expected.json new file mode 100644 index 0000000..77e6a8e --- /dev/null +++ b/test/cssi/media-queries/expected.json @@ -0,0 +1,6 @@ +{ + "--small": "(max-width: 30em)", + "--medium": "(max-width: 60em)", + "--large": "(max-width: 90em)", + "red": "exported-red" +} diff --git a/test/cssi/media-queries/source.css b/test/cssi/media-queries/source.css new file mode 100644 index 0000000..222ce70 --- /dev/null +++ b/test/cssi/media-queries/source.css @@ -0,0 +1,38 @@ +:import("./breakpoints.css") { + i__breakpoints__small: --small; + i__breakpoints__medium: --medium; + i__breakpoints__large: --large; +} + +:export { + --small: i__breakpoints__small; + --medium: i__breakpoints__medium; + --large: i__breakpoints__large; + red: exported-red; +} + +@custom-media --small i__breakpoints__small; +@custom-media --medium i__breakpoints__medium; +@custom-media --large i__breakpoints__large; + +.exported-red { + color: red; +} + +@media (--small) { + .exported-red { + color: maroon; + } +} + +@media (--medium) { + .exported-red { + color: darkmagenta; + } +} + +@media (--large) { + .exported-red { + color: fuchsia; + } +} diff --git a/test/test-cases.js b/test/test-cases.js index c6adcff..f38d9d8 100644 --- a/test/test-cases.js +++ b/test/test-cases.js @@ -21,7 +21,7 @@ Object.keys( pipelines ).forEach( dirname => { 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 loader = new FileSystemLoader( testDir, pipelines[dirname], 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 ) diff --git a/test/test-cases/media-queries/breakpoints.css b/test/test-cases/media-queries/breakpoints.css new file mode 100644 index 0000000..158d1f2 --- /dev/null +++ b/test/test-cases/media-queries/breakpoints.css @@ -0,0 +1,2 @@ +@custom-media --small (max-width: 30em); +@custom-media --medium (max-width: 60em); diff --git a/test/test-cases/media-queries/expected.css b/test/test-cases/media-queries/expected.css new file mode 100644 index 0000000..795fd15 --- /dev/null +++ b/test/test-cases/media-queries/expected.css @@ -0,0 +1,22 @@ + +._media_queries_source__red { + color: red; +} + +@media (max-width: 30em) { + ._media_queries_source__red { + color: maroon; + } +} + +@media (max-width: 60em) { + ._media_queries_source__red { + color: darkmagenta; + } +} + +@media (max-width: 90em) { + ._media_queries_source__red { + color: fuchsia; + } +} diff --git a/test/test-cases/media-queries/expected.json b/test/test-cases/media-queries/expected.json new file mode 100644 index 0000000..8f5c598 --- /dev/null +++ b/test/test-cases/media-queries/expected.json @@ -0,0 +1,6 @@ +{ + "red": "_media_queries_source__red", + "--small": "(max-width: 30em)", + "--medium": "(max-width: 60em)", + "--large": "(max-width: 90em)" +} diff --git a/test/test-cases/media-queries/source.css b/test/test-cases/media-queries/source.css new file mode 100644 index 0000000..5c1058e --- /dev/null +++ b/test/test-cases/media-queries/source.css @@ -0,0 +1,25 @@ +@custom-media --small from "./breakpoints.css"; +@custom-media --medium from "./breakpoints.css"; +@custom-media --large (max-width: 90em); + +.red { + color: red; +} + +@media (--small) { + .red { + color: maroon; + } +} + +@media (--medium) { + .red { + color: darkmagenta; + } +} + +@media (--large) { + .red { + color: fuchsia; + } +}