Adding support for output libraryTarget 'system'. Resolves #8833.

Tests, prettier

Self review

Add comments to System tests.

Adding typescript definition

Guy Bedford's feedback

Self review

Improving code coverage
This commit is contained in:
Joel Denning 2019-03-01 14:26:36 -07:00
parent b41e2c68a5
commit 97d58d31c0
13 changed files with 302 additions and 2 deletions

View File

@ -1122,7 +1122,8 @@ export interface OutputOptions {
| "amd-require"
| "umd"
| "umd2"
| "jsonp";
| "jsonp"
| "system";
/**
* The output directory as **absolute path** (required).
*/

View File

@ -137,6 +137,7 @@ class ExternalModule extends Module {
case "amd-require":
case "umd":
case "umd2":
case "system":
return this.getSourceForAmdOrUmdExternal(
this.id,
this.optional,

View File

@ -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`);
}

View File

@ -0,0 +1,113 @@
/*
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 ? `"${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
? `\n var ${externalWebpackNames.join(", ")};`
: ``;
// The system.register format requires an array of setter functions for externals.
const setters =
externalWebpackNames.length === 0
? ""
: `\n setters: [` +
externalWebpackNames
.map(
external =>
`\n function(module) {` +
`\n ${external} = module;` +
`\n }`
)
.join(",") +
`\n ],`;
return new ConcatSource(
`System.register(${name}${systemDependencies}, function(${dynamicExport}) {` +
externalVarDeclarations +
`\n return {` +
setters +
`\n execute: function() {` +
`\n ${dynamicExport}(`,
source,
`\n )\n }\n };\n});`
);
};
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;

View File

@ -986,7 +986,8 @@
"amd-require",
"umd",
"umd2",
"jsonp"
"jsonp",
"system"
]
},
"path": {

View File

@ -0,0 +1,16 @@
/* 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.
*/
afterEach(function(done) {
delete global.System;
done()
})
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');
});

View File

@ -0,0 +1,31 @@
const webpack = require("../../../../");
module.exports = {
output: {
libraryTarget: "system"
},
externals: {
external1: "external1",
external2: "external2"
},
plugins: [
new webpack.BannerPlugin({
raw: true,
banner: `
System = {
register: function(deps, fn) {
function dynamicExport() {}
var mod = fn(dynamicExport);
deps.forEach((dep, i) => {
mod.setters[i](System.registry[dep]);
})
mod.execute();
},
registry: {
external1: 'the external1 value',
external2: 'the external2 value',
},
}
`
})
]
};

View File

@ -0,0 +1,22 @@
// 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(done) {
var fs = require("fs");
var source = fs.readFileSync(__filename, "utf-8");
// set timeout allows the webpack bundle to finish exporting, which exports to System at the very
// end of its execution.
setTimeout(() => {
expect(global.SystemExports['default']).toBe('the default export')
expect(global.SystemExports.namedThing).toBe(namedThing)
delete global.System;
delete global.SystemExports
done()
})
});

View File

@ -0,0 +1,28 @@
const webpack = require("../../../../");
module.exports = {
output: {
libraryTarget: "system"
},
node: {
__dirname: false,
__filename: false
},
plugins: [
new webpack.BannerPlugin({
raw: true,
banner: `
global.SystemExports = {
'export': function(exports) {
Object.assign(global.SystemExports, exports);
}
};
global.System = {
register: function(deps, fn) {
var mod = fn(global.SystemExports['export']);
mod.execute();
}
};
`
})
]
};

View File

@ -0,0 +1,15 @@
/* This test verifies that when output.library is specified that the compiled bundle provides
* the library name to System during the System.register
*/
afterEach(function(done) {
delete global.System;
done()
})
it("should call System.register without a name", function() {
var fs = require("fs");
var source = fs.readFileSync(__filename, "utf-8");
expect(source).toMatch(/.*System\.register\("named-system-module", \[[^\]]*\]/);
});

View File

@ -0,0 +1,25 @@
const webpack = require("../../../../");
module.exports = {
output: {
library: "named-system-module",
libraryTarget: "system"
},
node: {
__dirname: false,
__filename: false
},
plugins: [
new webpack.BannerPlugin({
raw: true,
banner: `
System = {
register: function(name, deps, fn) {
function dynamicExport() {}
var mod = fn(dynamicExport);
mod.execute();
}
}
`
})
]
};

View File

@ -0,0 +1,15 @@
/* This test verifies that when there is no output.library specified that the call to
* System.register does not include a name argument.
*/
afterEach(function(done) {
delete global.System;
done()
})
it("should call System.register without a name", function() {
var fs = require("fs");
var source = fs.readFileSync(__filename, "utf-8");
expect(source).toMatch(/.*System\.register\(\[[^\]]*\], function\(__WEBPACK_DYNAMIC_EXPORT__\) {\s+(var .*;)?\s*return \{\s+setters: [^]+,[^]+execute: function\(\) {/);
});

View File

@ -0,0 +1,25 @@
const webpack = require("../../../../");
module.exports = {
output: {
libraryTarget: "system"
},
node: {
__dirname: false,
__filename: false
},
plugins: [
new webpack.BannerPlugin({
raw: true,
banner: `
System = {
register: function(deps, fn) {
function dynamicExport() {}
var mod = fn(dynamicExport)
mod.execute()
}
}
`
})
]
};