add support for :export {} block

This commit is contained in:
Tobias Koppers 2021-12-14 16:02:26 +01:00
parent b9c6691ff5
commit 453e5cac05
14 changed files with 530 additions and 38 deletions

View File

@ -32,6 +32,13 @@
* @property {CodeGenerationResults} codeGenerationResults the code generation results
*/
/**
* @typedef {Object} CssDependencyTemplateContextExtras
* @property {Map<string, string>} cssExports the css exports
*/
/** @typedef {DependencyTemplateContext & CssDependencyTemplateContextExtras} CssDependencyTemplateContext */
class DependencyTemplate {
/* istanbul ignore next */
/**

View File

@ -599,9 +599,27 @@ const applyModuleDefaults = (
preferRelative: true
}
};
const cssModulesRule = {
type: "css/module",
resolve: {
fullySpecified: true
}
};
rules.push({
test: /\.css$/i,
...cssRule
oneOf: [
{
test: /\.module\.css$/i,
...cssModulesRule
},
{
...cssRule
}
]
});
rules.push({
mimetype: "text/css+module",
...cssModulesRule
});
rules.push({
mimetype: "text/css",

View File

@ -32,8 +32,23 @@ class CssGenerator extends Generator {
const originalSource = module.originalSource();
const source = new ReplaceSource(originalSource);
const initFragments = [];
const cssExports = new Map();
for (const dependency of module.dependencies) {
const templateContext = {
runtimeTemplate: generateContext.runtimeTemplate,
dependencyTemplates: generateContext.dependencyTemplates,
moduleGraph: generateContext.moduleGraph,
chunkGraph: generateContext.chunkGraph,
module,
runtime: generateContext.runtime,
runtimeRequirements: generateContext.runtimeRequirements,
concatenationScope: generateContext.concatenationScope,
codeGenerationResults: generateContext.codeGenerationResults,
initFragments,
cssExports
};
const handleDependency = dependency => {
const constructor = /** @type {new (...args: any[]) => Dependency} */ (
dependency.constructor
);
@ -44,21 +59,17 @@ class CssGenerator extends Generator {
);
}
const templateContext = {
runtimeTemplate: generateContext.runtimeTemplate,
dependencyTemplates: generateContext.dependencyTemplates,
moduleGraph: generateContext.moduleGraph,
chunkGraph: generateContext.chunkGraph,
module,
runtime: generateContext.runtime,
runtimeRequirements: generateContext.runtimeRequirements,
concatenationScope: generateContext.concatenationScope,
codeGenerationResults: generateContext.codeGenerationResults,
initFragments
};
template.apply(dependency, source, templateContext);
};
module.dependencies.forEach(handleDependency);
if (module.presentationalDependencies !== undefined)
module.presentationalDependencies.forEach(handleDependency);
if (cssExports.size > 0) {
const data = generateContext.getData();
data.set("css-exports", cssExports);
}
return InitFragment.addToSource(source, initFragments, generateContext);
}

View File

@ -120,6 +120,8 @@ class CssLoadingRuntimeModule extends RuntimeModule {
: ""
]);
const cc = str => str.charCodeAt(0);
return Template.asString([
"// object to store loaded and loading chunks",
"// undefined = chunk not loaded, null = chunk preloaded/prefetched",
@ -132,27 +134,29 @@ class CssLoadingRuntimeModule extends RuntimeModule {
).join(",")}};`,
"",
`var loadCssChunkData = ${runtimeTemplate.basicFunction("chunkId, link", [
'var data, tokens = [], token = "", i = 0;',
'var data, token = "", token2, exports = {}, i = 0, cc = 1;',
"try { if(!link) link = loadStylesheet(chunkId); data = link.sheet.cssRules; data = data[data.length - 1].style; } catch(e) { data = getComputedStyle(document.head); }",
'data = data.getPropertyValue("--webpack-" + chunkId);',
"if(!data) return;",
"for(; i < data.length; i++) {",
"for(; cc; i++) {",
Template.indent([
"var cc = data.charCodeAt(i);",
'if(cc == 44) { tokens.push(token); token = ""; }',
"else if(cc == 92) { token += data[++i] }",
"else { token += data[i]; }"
"cc = data.charCodeAt(i);",
`if(cc == ${cc("(")}) { token2 = token; token = ""; }`,
`else if(cc == ${cc(
")"
)}) { exports[token2.replace(/^_/, "")] = token.replace(/^_/, ""); token = ""; }`,
`else if(!cc || cc == ${cc(",")}) { ${
RuntimeGlobals.makeNamespaceObject
}(exports); ${
RuntimeGlobals.moduleFactories
}[token.replace(/^_/, "")] = (${runtimeTemplate.basicFunction(
"exports, module",
`module.exports = exports;`
)}).bind(null, exports); token = ""; exports = {}; }`,
`else if(cc == ${cc("\\")}) { token += data[++i] }`,
`else { token += data[i]; }`
]),
"}",
"token && tokens.push(token);",
`tokens.forEach(${runtimeTemplate.basicFunction("token", [
`${
RuntimeGlobals.moduleFactories
}[token.replace(/^_/, "")] = ${runtimeTemplate.basicFunction(
"module, exports",
[`${RuntimeGlobals.makeNamespaceObject}(exports);`]
)};`
])});`,
"installedChunks[chunkId] = 0;"
])}`,
'var loadingAttribute = "data-webpack-loading";',

View File

@ -8,6 +8,7 @@
const { ConcatSource } = require("webpack-sources");
const HotUpdateChunk = require("../HotUpdateChunk");
const RuntimeGlobals = require("../RuntimeGlobals");
const CssExportDependency = require("../dependencies/CssExportDependency");
const CssImportDependency = require("../dependencies/CssImportDependency");
const CssUrlDependency = require("../dependencies/CssUrlDependency");
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
@ -85,6 +86,10 @@ class CssModulesPlugin {
CssUrlDependency,
new CssUrlDependency.Template()
);
compilation.dependencyTemplates.set(
CssExportDependency,
new CssExportDependency.Template()
);
compilation.dependencyFactories.set(
CssImportDependency,
normalModuleFactory
@ -103,12 +108,36 @@ class CssModulesPlugin {
validateParserOptions(parserOptions);
return new CssParser();
});
normalModuleFactory.hooks.createParser
.for("css/global")
.tap(plugin, parserOptions => {
validateParserOptions(parserOptions);
return new CssParser({ allowPseudoBlocks: false });
});
normalModuleFactory.hooks.createParser
.for("css/module")
.tap(plugin, parserOptions => {
validateParserOptions(parserOptions);
return new CssParser({ allowPseudoBlocks: true });
});
normalModuleFactory.hooks.createGenerator
.for("css")
.tap(plugin, generatorOptions => {
validateGeneratorOptions(generatorOptions);
return new CssGenerator();
});
normalModuleFactory.hooks.createGenerator
.for("css/global")
.tap(plugin, generatorOptions => {
validateGeneratorOptions(generatorOptions);
return new CssGenerator();
});
normalModuleFactory.hooks.createGenerator
.for("css/module")
.tap(plugin, generatorOptions => {
validateGeneratorOptions(generatorOptions);
return new CssGenerator();
});
compilation.hooks.contentHash.tap("JavascriptModulesPlugin", chunk => {
const {
chunkGraph,
@ -204,25 +233,43 @@ class CssModulesPlugin {
renderChunk({ chunk, chunkGraph, codeGenerationResults }) {
const modules = this.getOrderedChunkCssModules(chunk, chunkGraph);
const source = new ConcatSource();
const metaData = [];
for (const module of modules) {
try {
const codeGenResult = codeGenerationResults.get(module, chunk.runtime);
const s =
codeGenerationResults.getSource(module, chunk.runtime, "css") ||
codeGenerationResults.getSource(module, chunk.runtime, "css-import");
codeGenResult.sources.get("css") ||
codeGenResult.sources.get("css-import");
if (s) {
source.add(s);
source.add("\n");
}
const exports =
codeGenResult.data && codeGenResult.data.get("css-exports");
metaData.push(
`${
exports
? Array.from(
exports,
([n, v]) =>
`${escapeCssIdentifierPart(
n,
true
)}(${escapeCssIdentifierPart(v, true)})`
).join("")
: ""
}${escapeCssIdentifierPart(chunkGraph.getModuleId(module), true)}`
);
} catch (e) {
e.message += `\nduring rendering of css ${module.identifier()}`;
throw e;
}
}
source.add(
`head{--webpack-${escapeCssIdentifierPart(chunk.id)}:${Array.from(
modules,
m => `${escapeCssIdentifierPart(chunkGraph.getModuleId(m), true)}`
).join(",")};}`
`head{--webpack-${escapeCssIdentifierPart(chunk.id)}:${metaData.join(
","
)};}`
);
return source;
}

View File

@ -6,6 +6,8 @@
"use strict";
const Parser = require("../Parser");
const ConstDependency = require("../dependencies/ConstDependency");
const CssExportDependency = require("../dependencies/CssExportDependency");
const CssImportDependency = require("../dependencies/CssImportDependency");
const CssUrlDependency = require("../dependencies/CssUrlDependency");
const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
@ -14,6 +16,12 @@ const walkCssTokens = require("./walkCssTokens");
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../Parser").PreparsedAst} PreparsedAst */
const CC_LEFT_CURLY = "{".charCodeAt(0);
const CC_RIGHT_CURLY = "}".charCodeAt(0);
const CC_COLON = ":".charCodeAt(0);
const CC_SLASH = "/".charCodeAt(0);
const CC_SEMICOLON = ";".charCodeAt(0);
const cssUnescape = str => {
return str.replace(/\\([0-9a-fA-F]{1,6}[ \t\n\r\f]?|[\s\S])/g, match => {
if (match.length > 2) {
@ -87,8 +95,9 @@ const explainMode = mode => {
};
class CssParser extends Parser {
constructor() {
constructor({ allowPseudoBlocks = true } = {}) {
super();
this.allowPseudoBlocks = allowPseudoBlocks;
}
/**
@ -111,7 +120,9 @@ class CssParser extends Parser {
const locConverter = new LocConverter(source);
let mode = CSS_MODE_TOP_LEVEL;
let modePos = 0;
let modeNestingLevel = 0;
let modeData = undefined;
const modeStack = [];
const eatWhiteLine = (input, pos) => {
for (;;) {
const cc = input.charCodeAt(pos);
@ -124,6 +135,104 @@ class CssParser extends Parser {
}
return pos;
};
const eatUntil = chars => {
const charCodes = Array.from({ length: chars.length }, (_, i) =>
chars.charCodeAt(i)
);
const arr = Array.from(
{ length: charCodes.reduce((a, b) => Math.max(a, b), 0) + 1 },
() => false
);
charCodes.forEach(cc => (arr[cc] = true));
return (input, pos) => {
for (;;) {
const cc = input.charCodeAt(pos);
if (cc < arr.length && arr[cc]) {
return pos;
}
pos++;
if (pos === input.length) return pos;
}
};
};
const eatText = (input, pos, eater) => {
let text = "";
for (;;) {
if (input.charCodeAt(pos) === CC_SLASH) {
const newPos = walkCssTokens.eatComments(input, pos);
if (pos !== newPos) {
pos = newPos;
if (pos === input.length) break;
} else {
text += "/";
pos++;
if (pos === input.length) break;
}
}
const newPos = eater(input, pos);
if (pos !== newPos) {
text += input.slice(pos, newPos);
pos = newPos;
} else {
break;
}
if (pos === input.length) break;
}
return [pos, text.trimRight()];
};
const eatExportName = eatUntil(":};/");
const eatExportValue = eatUntil("};/");
const parseExports = (input, pos) => {
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
const cc = input.charCodeAt(pos);
if (cc !== CC_LEFT_CURLY)
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of ':export' (expected '{')`
);
pos++;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
for (;;) {
if (input.charCodeAt(pos) === CC_RIGHT_CURLY) break;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
if (pos === input.length) return pos;
let start = pos;
let name;
[pos, name] = eatText(input, pos, eatExportName);
if (pos === input.length) return pos;
if (input.charCodeAt(pos) !== CC_COLON) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of export name in ':export' (expected ':')`
);
}
pos++;
if (pos === input.length) return pos;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
if (pos === input.length) return pos;
let value;
[pos, value] = eatText(input, pos, eatExportValue);
if (pos === input.length) return pos;
const cc = input.charCodeAt(pos);
if (cc === CC_SEMICOLON) {
pos++;
if (pos === input.length) return pos;
pos = walkCssTokens.eatWhitespaceAndComments(input, pos);
if (pos === input.length) return pos;
} else if (cc !== CC_RIGHT_CURLY) {
throw new Error(
`Unexpected ${input[pos]} at ${pos} during parsing of export value in ':export' (expected ';' or '}')`
);
}
const dep = new CssExportDependency(name, value);
const { line: sl, column: sc } = locConverter.get(start);
const { line: el, column: ec } = locConverter.get(pos);
dep.setLoc(sl, sc, el, ec);
module.addDependency(dep);
}
pos++;
if (pos === input.length) return pos;
pos = eatWhiteLine(input, pos);
return pos;
};
walkCssTokens(source, {
url: (input, start, end, contentStart, contentEnd) => {
const value = cssUnescape(input.slice(contentStart, contentEnd));
@ -208,6 +317,64 @@ class CssParser extends Parser {
mode = CSS_MODE_TOP_LEVEL;
modeData = undefined;
return end;
},
leftCurlyBracket: (input, start, end) => {
switch (mode) {
case CSS_MODE_TOP_LEVEL:
mode = CSS_MODE_IN_RULE;
modeNestingLevel = 1;
break;
case CSS_MODE_IN_RULE:
modeNestingLevel++;
break;
}
return end;
},
rightCurlyBracket: (input, start, end) => {
switch (mode) {
case CSS_MODE_IN_RULE:
if (--modeNestingLevel === 0) {
mode = CSS_MODE_TOP_LEVEL;
}
break;
}
return end;
},
leftParenthesis: (input, start, end) => {
return end;
},
rightParenthesis: (input, start, end) => {
return end;
},
pseudoClass: (input, start, end) => {
switch (mode) {
case CSS_MODE_TOP_LEVEL: {
if (this.allowPseudoBlocks) {
const name = input.slice(start, end);
if (name === ":global") {
modeData = ":global";
} else if (name === ":local") {
modeData = ":local";
} else if (name === ":export") {
const pos = parseExports(input, end);
const dep = new ConstDependency("", [start, pos]);
module.addPresentationalDependency(dep);
return pos;
}
}
break;
}
}
return end;
},
comma: (input, start, end) => {
switch (mode) {
case CSS_MODE_TOP_LEVEL:
modeData = undefined;
modeStack.length = 0;
break;
}
return end;
}
});

View File

@ -108,8 +108,8 @@ const consumePotentialComment = (input, pos, callbacks) => {
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos);
if (cc !== CC_ASTERISK) return pos;
pos++;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
@ -587,3 +587,60 @@ module.exports = (input, callbacks) => {
}
}
};
module.exports.eatComments = (input, pos) => {
loop: for (;;) {
const cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos + 1);
if (cc !== CC_ASTERISK) return pos;
pos++;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
pos++;
continue loop;
}
}
}
}
return pos;
}
};
module.exports.eatWhitespaceAndComments = (input, pos) => {
loop: for (;;) {
const cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
if (pos === input.length) return pos;
let cc = input.charCodeAt(pos + 1);
if (cc !== CC_ASTERISK) return pos;
pos++;
for (;;) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
while (cc === CC_ASTERISK) {
pos++;
if (pos === input.length) return pos;
cc = input.charCodeAt(pos);
if (cc === CC_SLASH) {
pos++;
continue loop;
}
}
}
} else if (_isWhiteSpace(cc)) {
pos++;
continue;
}
return pos;
}
};

View File

@ -0,0 +1,85 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const makeSerializable = require("../util/makeSerializable");
const NullDependency = require("./NullDependency");
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
/** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
class CssExportDependency extends NullDependency {
/**
* @param {string} name name
* @param {string} value value
*/
constructor(name, value) {
super();
this.name = name;
this.value = value;
}
get type() {
return "css :export";
}
/**
* Returns the exported names
* @param {ModuleGraph} moduleGraph module graph
* @returns {ExportsSpec | undefined} export names
*/
getExports(moduleGraph) {
const name = this.name;
return {
exports: [
{
name,
canMangle: true
}
],
dependencies: undefined
};
}
serialize(context) {
const { write } = context;
write(this.name);
write(this.value);
super.serialize(context);
}
deserialize(context) {
const { read } = context;
this.name = read();
this.value = read();
super.deserialize(context);
}
}
CssExportDependency.Template = class CssExportDependencyTemplate extends (
NullDependency.Template
) {
/**
* @param {Dependency} dependency the dependency for which the template should be applied
* @param {ReplaceSource} source the current replace source which can be modified
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
apply(dependency, source, { cssExports }) {
const dep = /** @type {CssExportDependency} */ (dependency);
cssExports.set(dep.name, dep.value);
}
};
makeSerializable(
CssExportDependency,
"webpack/lib/dependencies/CssExportDependency"
);
module.exports = CssExportDependency;

View File

@ -69,6 +69,8 @@ module.exports = {
require("../dependencies/CriticalDependencyWarning"),
"dependencies/CssImportDependency": () =>
require("../dependencies/CssImportDependency"),
"dependencies/CssExportDependency": () =>
require("../dependencies/CssExportDependency"),
"dependencies/CssUrlDependency": () =>
require("../dependencies/CssUrlDependency"),
"dependencies/DelegatedSourceDependency": () =>

View File

@ -0,0 +1,3 @@
import * as style from "./style.module.css?imported";
export default Object(style);

View File

@ -0,0 +1,57 @@
it("should allow to dynamic import a css module", done => {
import("./style.module.css").then(x => {
try {
expect(x).toEqual(
nsObj({
a: "a",
abc: "a b c",
comments: "abc def",
"white space": "abc\n\tdef",
default: "default"
})
);
} catch (e) {
return done(e);
}
done();
}, done);
});
it("should allow to reexport a css module", done => {
__non_webpack_require__("./reexported_js.bundle0.js");
import("./reexported").then(x => {
try {
expect(x).toEqual(
nsObj({
a: "a",
abc: "a b c",
comments: "abc def",
"white space": "abc\n\tdef"
})
);
} catch (e) {
return done(e);
}
done();
}, done);
});
it("should allow to import a css module", done => {
__non_webpack_require__("./imported_js.bundle0.js");
import("./imported").then(({ default: x }) => {
try {
expect(x).toEqual(
nsObj({
a: "a",
abc: "a b c",
comments: "abc def",
"white space": "abc\n\tdef",
default: "default"
})
);
} catch (e) {
return done(e);
}
done();
}, done);
});

View File

@ -0,0 +1 @@
export * from "./style.module.css?reexported";

View File

@ -0,0 +1,25 @@
:export {
a: a;
}
:export {
abc: a b c;
comments: abc/****/ /* hello world *//****/ def
}
:export
{
white space
:
abc
def
}
:export{default:default}

View File

@ -0,0 +1,8 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
target: "web",
mode: "development",
experiments: {
css: true
}
};