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.
*/
compiler?: RuleSetConditionOrConditions;
/**
* Match dependency type.
*/
dependency?: string;
/**
* 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([
new BasicMatcherRulePlugin("test", "resource"),
new BasicMatcherRulePlugin("mimetype"),
new BasicMatcherRulePlugin("dependency"),
new BasicMatcherRulePlugin("include", "resource"),
new BasicMatcherRulePlugin("exclude", "resource", true),
new BasicMatcherRulePlugin("resource"),

View File

@ -278,3 +278,8 @@ exports.hasOwnProperty = "__webpack_require__.o";
* the System.register context object
*/
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 WarnCaseSensitiveModulesPlugin = require("./WarnCaseSensitiveModulesPlugin");
const BaseURIPlugin = require("./BaseURIPlugin");
const URLPlugin = require("./dependencies/URLPlugin");
const DataUriPlugin = require("./schemes/DataUriPlugin");
const FileUriPlugin = require("./schemes/FileUriPlugin");
@ -313,6 +315,8 @@ class WebpackOptionsApply extends OptionsApply {
: true
}).apply(compiler);
new BaseURIPlugin(options.target).apply(compiler);
new URLPlugin().apply(compiler);
new DataUriPlugin().apply(compiler);
new FileUriPlugin().apply(compiler);

View File

@ -356,6 +356,13 @@ const applyModuleDefaults = (
mimetype: "application/node",
type: "javascript/auto"
},
{
test: /\.js$/,
dependency: "url",
resolve: {
conditionNames: ["esm", "..."]
}
},
{
test: /\.json$/i,
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": {
"description": "Match values of properties in the description file (usually package.json).",
"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 {
constructor() {
this.head = this.createElement("head");
this.baseURI = "https://test.cases/path/index.html";
this._elementsByTagName = new Map([["head", [this.head]]]);
}

6
types.d.ts vendored
View File

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