Skip to content

Commit

Permalink
Merge pull request #8864 from joeldenning/issue-8833
Browse files Browse the repository at this point in the history
Adding support for output.libraryTarget "system"
  • Loading branch information
sokra committed Apr 10, 2019
2 parents 7dfddc2 + f7d0c25 commit de81dd0
Show file tree
Hide file tree
Showing 19 changed files with 387 additions and 38 deletions.
3 changes: 2 additions & 1 deletion declarations/WebpackOptions.d.ts
Expand Up @@ -1122,7 +1122,8 @@ export interface OutputOptions {
| "amd-require"
| "umd"
| "umd2"
| "jsonp";
| "jsonp"
| "system";
/**
* The output directory as **absolute path** (required).
*/
Expand Down
1 change: 1 addition & 0 deletions lib/ExternalModule.js
Expand Up @@ -137,6 +137,7 @@ class ExternalModule extends Module {
case "amd-require":
case "umd":
case "umd2":
case "system":
return this.getSourceForAmdOrUmdExternal(
this.id,
this.optional,
Expand Down
7 changes: 7 additions & 0 deletions lib/LibraryTemplatePlugin.js
Expand Up @@ -169,6 +169,13 @@ class LibraryTemplatePlugin {
new JsonpExportMainTemplatePlugin(this.name).apply(compilation);
break;
}
case "system": {
const SystemMainTemplatePlugin = require("./SystemMainTemplatePlugin");
new SystemMainTemplatePlugin({
name: this.name
}).apply(compilation);
break;
}
default:
throw new Error(`${this.target} is not a valid Library target`);
}
Expand Down
131 changes: 131 additions & 0 deletions lib/SystemMainTemplatePlugin.js
@@ -0,0 +1,131 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Joel Denning @joeldenning
*/

"use strict";

const { ConcatSource } = require("webpack-sources");
const Template = require("./Template");

/** @typedef {import("./Compilation")} Compilation */

/**
* @typedef {Object} SystemMainTemplatePluginOptions
* @param {string=} name the library name
*/

class SystemMainTemplatePlugin {
/**
* @param {SystemMainTemplatePluginOptions} options the plugin options
*/
constructor(options) {
this.name = options.name;
}

/**
* @param {Compilation} compilation the compilation instance
* @returns {void}
*/
apply(compilation) {
const { mainTemplate, chunkTemplate } = compilation;

const onRenderWithEntry = (source, chunk, hash) => {
const externals = chunk.getModules().filter(m => m.external);

// The name this bundle should be registered as with System
const name = this.name ? `${JSON.stringify(this.name)}, ` : "";

// The array of dependencies that are external to webpack and will be provided by System
const systemDependencies = JSON.stringify(
externals.map(m =>
typeof m.request === "object" ? m.request.amd : m.request
)
);

// The name of the variable provided by System for exporting
const dynamicExport = "__WEBPACK_DYNAMIC_EXPORT__";

// An array of the internal variable names for the webpack externals
const externalWebpackNames = externals.map(
m => `__WEBPACK_EXTERNAL_MODULE_${Template.toIdentifier(`${m.id}`)}__`
);

// Declaring variables for the internal variable names for the webpack externals
const externalVarDeclarations =
externalWebpackNames.length > 0
? `var ${externalWebpackNames.join(", ")};`
: "";

// The system.register format requires an array of setter functions for externals.
const setters =
externalWebpackNames.length === 0
? ""
: Template.asString([
"setters: [",
Template.indent(
externalWebpackNames
.map(external =>
Template.asString([
"function(module) {",
Template.indent(`${external} = module;`),
"}"
])
)
.join(",\n")
),
"],"
]);

return new ConcatSource(
Template.asString([
`System.register(${name}${systemDependencies}, function(${dynamicExport}) {`,
Template.indent([
externalVarDeclarations,
"return {",
Template.indent([
setters,
"execute: function() {",
Template.indent(`${dynamicExport}(`)
])
])
]) + "\n",
source,
"\n" +
Template.asString([
Template.indent([
Template.indent([Template.indent([");"]), "}"]),
"};"
]),
"})"
])
);
};

for (const template of [mainTemplate, chunkTemplate]) {
template.hooks.renderWithEntry.tap(
"SystemMainTemplatePlugin",
onRenderWithEntry
);
}

mainTemplate.hooks.globalHashPaths.tap(
"SystemMainTemplatePlugin",
paths => {
if (this.name) {
paths.push(this.name);
}
return paths;
}
);

mainTemplate.hooks.hash.tap("SystemMainTemplatePlugin", hash => {
hash.update("exports system");
if (this.name) {
hash.update(this.name);
}
});
}
}

module.exports = SystemMainTemplatePlugin;
3 changes: 2 additions & 1 deletion schemas/WebpackOptions.json
Expand Up @@ -986,7 +986,8 @@
"amd-require",
"umd",
"umd2",
"jsonp"
"jsonp",
"system"
]
},
"path": {
Expand Down
74 changes: 38 additions & 36 deletions test/ConfigTestCases.test.js
Expand Up @@ -174,7 +174,6 @@ describe("ConfigTestCases", () => {

function _require(currentDirectory, module) {
if (Array.isArray(module) || /^\.\.?\//.test(module)) {
let fn;
let content;
let p;
if (Array.isArray(module)) {
Expand All @@ -189,45 +188,47 @@ describe("ConfigTestCases", () => {
p = path.join(currentDirectory, module);
content = fs.readFileSync(p, "utf-8");
}
if (
options.target === "web" ||
options.target === "webworker"
) {
fn = vm.runInNewContext(
"(function(require, module, exports, __dirname, __filename, it, beforeEach, afterEach, expect, jest, window) {" +
'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' +
content +
"\n})",
globalContext,
p
);
} else {
fn = vm.runInThisContext(
"(function(require, module, exports, __dirname, __filename, it, beforeEach, afterEach, expect, jest) {" +
"global.expect = expect; " +
'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' +
content +
"\n})",
p
);
}
const m = {
exports: {}
};
fn.call(
m.exports,
_require.bind(null, path.dirname(p)),
m,
m.exports,
path.dirname(p),
p,
_it,
_beforeEach,
_afterEach,
let runInNewContext = false;
const moduleScope = {
require: _require.bind(null, path.dirname(p)),
module: m,
exports: m.exports,
__dirname: path.dirname(p),
__filename: p,
it: _it,
beforeEach: _beforeEach,
afterEach: _afterEach,
expect,
jest,
globalContext
);
_globalAssign: { expect },
nsObj: m => {
Object.defineProperty(m, Symbol.toStringTag, {
value: "Module"
});
return m;
}
};
if (
options.target === "web" ||
options.target === "webworker"
) {
moduleScope.window = globalContext;
runInNewContext = true;
}
if (testConfig.moduleScope) {
testConfig.moduleScope(moduleScope);
}
const args = Object.keys(moduleScope).join(", ");
if (!runInNewContext)
content = `Object.assign(global, _globalAssign); ${content}`;
const code = `(function({${args}}) {${content}\n})`;
const fn = runInNewContext
? vm.runInNewContext(code, globalContext, p)
: vm.runInThisContext(code, p);
fn.call(m.exports, moduleScope);
return m.exports;
} else if (
testConfig.modules &&
Expand All @@ -239,6 +240,7 @@ describe("ConfigTestCases", () => {
let filesCount = 0;

if (testConfig.noTests) return process.nextTick(done);
if (testConfig.beforeExecute) testConfig.beforeExecute();
for (let i = 0; i < optionsArr.length; i++) {
const bundlePath = testConfig.findBundle(i, optionsArr[i]);
if (bundlePath) {
Expand All @@ -256,9 +258,9 @@ describe("ConfigTestCases", () => {
"Should have found at least one bundle file per webpack config"
)
);
if (testConfig.afterExecute) testConfig.afterExecute();
if (getNumberOfTests() < filesCount)
return done(new Error("No tests exported by test case"));
if (testConfig.afterExecute) testConfig.afterExecute();
done();
});
})
Expand Down
11 changes: 11 additions & 0 deletions test/configCases/externals/externals-system/index.js
@@ -0,0 +1,11 @@
/* This test verifies that webpack externals are properly indicated as dependencies to System.
* Also that when System provides the external variables to webpack that the variables get plumbed
* through correctly and are usable by the webpack bundle.
*/
it("should get an external from System", function() {
const external1 = require("external1");
expect(external1).toBe("the external1 value");

const external2 = require("external2");
expect(external2).toBe("the external2 value");
});
16 changes: 16 additions & 0 deletions test/configCases/externals/externals-system/test.config.js
@@ -0,0 +1,16 @@
const System = require("../../../helpers/fakeSystem");

module.exports = {
beforeExecute: () => {
System.init({
external1: "the external1 value",
external2: "the external2 value"
});
},
moduleScope(scope) {
scope.System = System;
},
afterExecute: () => {
System.execute("(anonym)");
}
};
9 changes: 9 additions & 0 deletions test/configCases/externals/externals-system/webpack.config.js
@@ -0,0 +1,9 @@
module.exports = {
output: {
libraryTarget: "system"
},
externals: {
external1: "external1",
external2: "external2"
}
};
13 changes: 13 additions & 0 deletions test/configCases/target/system-export/index.js
@@ -0,0 +1,13 @@
// This test verifies that values exported by a webpack bundle are consumable by systemjs.

export const namedThing = {
hello: "there"
};

export default "the default export";

it("should successfully export values to System", function() {
const exports = eval("System").registry["(anonym)"].exports;
expect(exports["default"]).toBe("the default export");
expect(exports.namedThing).toBe(namedThing);
});
13 changes: 13 additions & 0 deletions test/configCases/target/system-export/test.config.js
@@ -0,0 +1,13 @@
const System = require("../../../helpers/fakeSystem");

module.exports = {
beforeExecute: () => {
System.init();
},
moduleScope(scope) {
scope.System = System;
},
afterExecute: () => {
System.execute("(anonym)");
}
};
9 changes: 9 additions & 0 deletions test/configCases/target/system-export/webpack.config.js
@@ -0,0 +1,9 @@
module.exports = {
output: {
libraryTarget: "system"
},
node: {
__dirname: false,
__filename: false
}
};
5 changes: 5 additions & 0 deletions test/configCases/target/system-named/index.js
@@ -0,0 +1,5 @@
/* This test verifies that when output.library is specified that the compiled bundle provides
* the library name to System during the System.register
*/

it("should call System.register with a name", function() {});
13 changes: 13 additions & 0 deletions test/configCases/target/system-named/test.config.js
@@ -0,0 +1,13 @@
const System = require("../../../helpers/fakeSystem");

module.exports = {
beforeExecute: () => {
System.init();
},
moduleScope(scope) {
scope.System = System;
},
afterExecute: () => {
System.execute("named-system-module");
}
};
10 changes: 10 additions & 0 deletions test/configCases/target/system-named/webpack.config.js
@@ -0,0 +1,10 @@
module.exports = {
output: {
library: "named-system-module",
libraryTarget: "system"
},
node: {
__dirname: false,
__filename: false
}
};
5 changes: 5 additions & 0 deletions test/configCases/target/system-unnamed/index.js
@@ -0,0 +1,5 @@
/* This test verifies that when there is no output.library specified that the call to
* System.register does not include a name argument.
*/

it("should call System.register without a name", function() {});

0 comments on commit de81dd0

Please sign in to comment.