feat: url assets

This commit is contained in:
Ivan Kopeykin 2020-08-05 00:42:29 +03:00
parent 3e4c2ef87a
commit ddc83b0d70
37 changed files with 508 additions and 0 deletions

View File

@ -1011,6 +1011,10 @@ export interface RuleSetRule {
* Match the child compiler name. * Match the child compiler name.
*/ */
compiler?: RuleSetConditionOrConditions; compiler?: RuleSetConditionOrConditions;
/**
* Match dependency type.
*/
dependency?: string;
/** /**
* Match values of properties in the description file (usually package.json). * Match values of properties in the description file (usually package.json).
*/ */

53
lib/BaseURIPlugin.js Normal file
View File

@ -0,0 +1,53 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const BaseURIRuntimeModule = require("./BaseURIRuntimeModule");
const RuntimeGlobals = require("./RuntimeGlobals");
/** @typedef {import("../declarations/WebpackOptions").Target} Target */
/** @typedef {import("./Compiler")} Compiler */
class BaseURIPlugin {
/**
* @param {Target} target target
*/
constructor(target) {
switch (target) {
case "webworker":
this.environment = /** @type {"webworker"} */ ("webworker");
break;
case "node":
case "async-node":
case "node-webkit":
case "electron-main":
case "electron-preload":
this.environment = /** @type {"node"} */ ("node");
break;
default:
this.environment = /** @type {"web"} */ ("web");
break;
}
}
/**
* @param {Compiler} compiler compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap("BaseURIPlugin", compilation => {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.baseURI)
.tap("BaseURIPlugin", (chunk, set) => {
compilation.addRuntimeModule(
chunk,
new BaseURIRuntimeModule(this.environment)
);
});
});
}
}
module.exports = BaseURIPlugin;

View File

@ -0,0 +1,44 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const RuntimeGlobals = require("./RuntimeGlobals");
const RuntimeModule = require("./RuntimeModule");
const Template = require("./Template");
class BaseURIRuntimeModule extends RuntimeModule {
/**
* @param {"node"|"web"|"webworker"} environment environment
*/
constructor(environment) {
super("baseURI");
this.env = environment;
}
/**
* @returns {string} runtime code
*/
generate() {
switch (this.env) {
case "web":
return Template.asString([
`${RuntimeGlobals.baseURI} = document.baseURI;`
]);
case "webworker":
return Template.asString([
`${RuntimeGlobals.baseURI} = self.location;`
]);
case "node":
return Template.asString([
`${RuntimeGlobals.baseURI} = require('url').pathToFileURL(__filename);`
]);
default:
return "";
}
}
}
module.exports = BaseURIRuntimeModule;

View File

@ -132,6 +132,7 @@ const dependencyCache = new WeakMap();
const ruleSetCompiler = new RuleSetCompiler([ const ruleSetCompiler = new RuleSetCompiler([
new BasicMatcherRulePlugin("test", "resource"), new BasicMatcherRulePlugin("test", "resource"),
new BasicMatcherRulePlugin("mimetype"), new BasicMatcherRulePlugin("mimetype"),
new BasicMatcherRulePlugin("dependency"),
new BasicMatcherRulePlugin("include", "resource"), new BasicMatcherRulePlugin("include", "resource"),
new BasicMatcherRulePlugin("exclude", "resource", true), new BasicMatcherRulePlugin("exclude", "resource", true),
new BasicMatcherRulePlugin("resource"), new BasicMatcherRulePlugin("resource"),

View File

@ -278,3 +278,8 @@ exports.hasOwnProperty = "__webpack_require__.o";
* the System.register context object * the System.register context object
*/ */
exports.systemContext = "__webpack_require__.y"; exports.systemContext = "__webpack_require__.y";
/**
* the baseURI of current document
*/
exports.baseURI = "__webpack_require__.b";

View File

@ -26,6 +26,8 @@ const TemplatedPathPlugin = require("./TemplatedPathPlugin");
const UseStrictPlugin = require("./UseStrictPlugin"); const UseStrictPlugin = require("./UseStrictPlugin");
const WarnCaseSensitiveModulesPlugin = require("./WarnCaseSensitiveModulesPlugin"); const WarnCaseSensitiveModulesPlugin = require("./WarnCaseSensitiveModulesPlugin");
const BaseURIPlugin = require("./BaseURIPlugin");
const URLPlugin = require("./dependencies/URLPlugin");
const DataUriPlugin = require("./schemes/DataUriPlugin"); const DataUriPlugin = require("./schemes/DataUriPlugin");
const FileUriPlugin = require("./schemes/FileUriPlugin"); const FileUriPlugin = require("./schemes/FileUriPlugin");
@ -313,6 +315,8 @@ class WebpackOptionsApply extends OptionsApply {
: true : true
}).apply(compiler); }).apply(compiler);
new BaseURIPlugin(options.target).apply(compiler);
new URLPlugin().apply(compiler);
new DataUriPlugin().apply(compiler); new DataUriPlugin().apply(compiler);
new FileUriPlugin().apply(compiler); new FileUriPlugin().apply(compiler);

View File

@ -356,6 +356,13 @@ const applyModuleDefaults = (
mimetype: "application/node", mimetype: "application/node",
type: "javascript/auto" type: "javascript/auto"
}, },
{
test: /\.js$/,
dependency: "url",
resolve: {
conditionNames: ["esm", "..."]
}
},
{ {
test: /\.json$/i, test: /\.json$/i,
type: "json" type: "json"

View File

@ -0,0 +1,75 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const ModuleDependency = require("./ModuleDependency");
/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../util/Hash")} Hash */
class URLDependency extends ModuleDependency {
/**
* @param {string} request request
* @param {[number, number]} range range
*/
constructor(request, range) {
super(request);
this.range = range;
}
get type() {
return "new URL()";
}
get category() {
return "url";
}
}
URLDependency.Template = class URLDependencyTemplate extends ModuleDependency.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, templateContext) {
const {
chunkGraph,
moduleGraph,
runtimeRequirements,
runtimeTemplate,
runtime
} = templateContext;
const dep = /** @type {URLDependency} */ (dependency);
const connection = moduleGraph.getConnection(dep);
if (connection && !connection.isActive(runtime)) return;
runtimeRequirements.add(RuntimeGlobals.baseURI);
runtimeRequirements.add(RuntimeGlobals.require);
source.replace(
dep.range[0],
dep.range[1] - 1,
`new URL(/* asset import */ ${runtimeTemplate.moduleRaw({
chunkGraph,
module: moduleGraph.getModule(dep),
request: dep.request,
runtimeRequirements,
weak: false
})}, ${RuntimeGlobals.baseURI})`
);
}
};
module.exports = URLDependency;

View File

@ -0,0 +1,78 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/
"use strict";
const URLDependency = require("./URLDependency");
/** @typedef {import("estree").NewExpression} NewExpressionNode */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
class URLPlugin {
/**
* @param {Compiler} compiler compiler
*/
apply(compiler) {
compiler.hooks.compilation.tap(
"URLPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(URLDependency, normalModuleFactory);
compilation.dependencyTemplates.set(
URLDependency,
new URLDependency.Template()
);
/**
* @param {JavascriptParser} parser parser
*/
const parserCallback = parser => {
parser.hooks.new.for("URL").tap("URLPlugin", _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);
if (expr.arguments.length !== 2) return;
const [arg1, arg2] = expr.arguments;
if (
arg2.type !== "MemberExpression" ||
arg1.type === "SpreadElement"
)
return;
const chain = parser.extractMemberExpressionChain(arg2);
if (
chain.members.length !== 1 ||
chain.object.type !== "MetaProperty" ||
chain.object.property.name !== "meta" ||
chain.members[0] !== "url"
)
return;
const request = parser.evaluateExpression(arg1).asString();
if (!request) return;
const dep = new URLDependency(request, expr.range);
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
return true;
});
};
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("URLPlugin", parserCallback);
normalModuleFactory.hooks.parser
.for("javascript/esm")
.tap("URLPlugin", parserCallback);
}
);
}
}
module.exports = URLPlugin;

View File

@ -2551,6 +2551,10 @@
} }
] ]
}, },
"dependency": {
"description": "Match dependency type.",
"type": "string"
},
"descriptionData": { "descriptionData": {
"description": "Match values of properties in the description file (usually package.json).", "description": "Match values of properties in the description file (usually package.json).",
"type": "object", "type": "object",

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,7 @@
const currentDir = require("url").pathToFileURL(__dirname);
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe(currentDir + "/public/index.css");
});

View File

@ -0,0 +1,21 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "node",
devtool: false,
output: {
assetModuleFilename: "[name][ext]",
publicPath: "public/"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,7 @@
const currentDir = require("url").pathToFileURL(__dirname);
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe(currentDir + "/index.css");
});

View File

@ -0,0 +1,20 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "node",
devtool: false,
output: {
assetModuleFilename: "[name][ext]"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,5 @@
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe("file:///index.css");
});

View File

@ -0,0 +1,21 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "node",
devtool: false,
output: {
assetModuleFilename: "[name][ext]",
publicPath: "/"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,5 @@
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe("https://test.cases/path/index.css");
});

View File

@ -0,0 +1,9 @@
let _URL = require("url").URL;
module.exports = {
moduleScope(scope) {
scope.URL = function URL(a, b) {
return new _URL(a, b);
};
}
};

View File

@ -0,0 +1,20 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "web",
devtool: false,
output: {
assetModuleFilename: "[name][ext]"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,5 @@
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe("https://test.cases/path2/index.css");
});

View File

@ -0,0 +1,9 @@
let _URL = require("url").URL;
module.exports = {
moduleScope(scope) {
scope.URL = function URL(a, b) {
return new _URL(a, b);
};
}
};

View File

@ -0,0 +1,21 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "web",
devtool: false,
output: {
assetModuleFilename: "[name][ext]",
publicPath: "/path2/"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,5 @@
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe("https://test.cases/path/index.css");
});

View File

@ -0,0 +1,9 @@
let _URL = require("url").URL;
module.exports = {
moduleScope(scope) {
scope.URL = function URL(a, b) {
return new _URL(a, b);
};
}
};

View File

@ -0,0 +1,20 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "webworker",
devtool: false,
output: {
assetModuleFilename: "[name][ext]"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -0,0 +1 @@
a {}

View File

@ -0,0 +1,5 @@
it("should handle import.meta.url in URL()", () => {
const {href} = new URL("./index.css", import.meta.url);
expect(href).toBe("https://test.cases/index.css");
});

View File

@ -0,0 +1,9 @@
let _URL = require("url").URL;
module.exports = {
moduleScope(scope) {
scope.URL = function URL(a, b) {
return new _URL(a, b);
};
}
};

View File

@ -0,0 +1,21 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
target: "webworker",
devtool: false,
output: {
assetModuleFilename: "[name][ext]",
publicPath: "/"
},
module: {
rules: [
{
test: /\.css$/,
type: "asset/resource"
}
]
},
experiments: {
asset: true
}
};

View File

@ -1,6 +1,7 @@
module.exports = class FakeDocument { module.exports = class FakeDocument {
constructor() { constructor() {
this.head = this.createElement("head"); this.head = this.createElement("head");
this.baseURI = "https://test.cases/path/index.html";
this._elementsByTagName = new Map([["head", [this.head]]]); this._elementsByTagName = new Map([["head", [this.head]]]);
} }

6
types.d.ts vendored
View File

@ -6948,6 +6948,11 @@ declare interface RuleSetRule {
*/ */
compiler?: RuleSetCondition; compiler?: RuleSetCondition;
/**
* Match dependency type.
*/
dependency?: string;
/** /**
* Match values of properties in the description file (usually package.json). * Match values of properties in the description file (usually package.json).
*/ */
@ -8990,6 +8995,7 @@ declare namespace exports {
export let system: string; export let system: string;
export let hasOwnProperty: string; export let hasOwnProperty: string;
export let systemContext: string; export let systemContext: string;
export let baseURI: string;
} }
export const UsageState: Readonly<{ export const UsageState: Readonly<{
Unused: 0; Unused: 0;