1 | import webpack from 'webpack';
|
2 | import { join } from 'path';
|
3 | import { mergeWith, flatten } from 'lodash';
|
4 | import { writeFileSync, realpathSync } from 'fs';
|
5 | import { compile, registerHelper } from 'handlebars';
|
6 | import jsStringEscape from 'js-string-escape';
|
7 | import { BundleDependencies } from './splitter';
|
8 | import { BundlerHook, BuildResult } from './bundler';
|
9 | import BundleConfig from './bundle-config';
|
10 | import { ensureDirSync } from 'fs-extra';
|
11 |
|
12 | registerHelper('js-string-escape', jsStringEscape);
|
13 |
|
14 | const entryTemplate = compile(`
|
15 | if (typeof document !== 'undefined') {
|
16 | {{#if publicAssetURL}}
|
17 | __webpack_public_path__ = '{{js-string-escape publicAssetURL}}';
|
18 | {{else}}
|
19 | {{!
|
20 | locate the webpack lazy loaded chunks relative to the currently executing
|
21 | script. The last <script> in DOM should be us, assuming that we are being
|
22 | synchronously loaded, which is the normal thing to do. If people are doing
|
23 | weirder things than that, they may need to explicitly set a publicAssetURL
|
24 | instead.
|
25 | }}
|
26 | __webpack_public_path__ = (function(){
|
27 | var scripts = document.querySelectorAll('script');
|
28 | return scripts[scripts.length - 1].src.replace(/\\/[^/]*$/, '/');
|
29 | })();
|
30 | {{/if}}
|
31 | }
|
32 |
|
33 | module.exports = (function(){
|
34 | var d = _eai_d;
|
35 | var r = _eai_r;
|
36 | window.emberAutoImportDynamic = function(specifier) {
|
37 | return r('_eai_dyn_' + specifier);
|
38 | };
|
39 | {{#each staticImports as |module|}}
|
40 | d('{{js-string-escape module.specifier}}', [], function() { return require('{{js-string-escape module.entrypoint}}'); });
|
41 | {{/each}}
|
42 | {{#each dynamicImports as |module|}}
|
43 | d('_eai_dyn_{{js-string-escape module.specifier}}', [], function() { return import('{{js-string-escape module.entrypoint}}'); });
|
44 | {{/each}}
|
45 | })();
|
46 | `);
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 | const loader = `
|
56 | window._eai_r = require;
|
57 | window._eai_d = define;
|
58 | `;
|
59 |
|
60 | export default class WebpackBundler implements BundlerHook {
|
61 | private stagingDir: string;
|
62 | private webpack: webpack.Compiler;
|
63 | private outputDir: string;
|
64 |
|
65 | constructor(
|
66 | bundles : BundleConfig,
|
67 | environment: 'production' | 'development' | 'test',
|
68 | extraWebpackConfig: webpack.Configuration | undefined,
|
69 | private consoleWrite: (message: string) => void,
|
70 | private publicAssetURL: string | undefined,
|
71 | tempArea: string
|
72 | ) {
|
73 |
|
74 |
|
75 | tempArea = realpathSync(tempArea);
|
76 |
|
77 | this.stagingDir = join(tempArea, 'staging');
|
78 | ensureDirSync(this.stagingDir);
|
79 | this.outputDir = join(tempArea, 'output');
|
80 | ensureDirSync(this.outputDir);
|
81 | let entry: { [name: string]: string[] } = {};
|
82 | bundles.names.forEach(bundle => {
|
83 | entry[bundle] = [join(this.stagingDir, 'l.js'), join(this.stagingDir, `${bundle}.js`)];
|
84 | });
|
85 |
|
86 | let config: webpack.Configuration = {
|
87 | mode: environment === 'production' ? 'production' : 'development',
|
88 | entry,
|
89 | output: {
|
90 | path: this.outputDir,
|
91 | filename: `chunk.[chunkhash].js`,
|
92 | chunkFilename: `chunk.[chunkhash].js`,
|
93 | libraryTarget: 'var',
|
94 | library: '__ember_auto_import__'
|
95 | },
|
96 | optimization: {
|
97 | splitChunks: {
|
98 | chunks: 'all'
|
99 | }
|
100 | },
|
101 | module: {
|
102 | noParse: (file) => file === join(this.stagingDir, 'l.js'),
|
103 | rules: []
|
104 | },
|
105 | };
|
106 | if (extraWebpackConfig) {
|
107 | mergeConfig(config, extraWebpackConfig);
|
108 | }
|
109 | this.webpack = webpack(config);
|
110 | }
|
111 |
|
112 | async build(bundleDeps: Map<string, BundleDependencies>) {
|
113 | for (let [bundle, deps] of bundleDeps.entries()) {
|
114 | this.writeEntryFile(bundle, deps);
|
115 | }
|
116 | this.writeLoaderFile();
|
117 | let stats = await this.runWebpack();
|
118 | return this.summarizeStats(stats);
|
119 | }
|
120 |
|
121 | private summarizeStats(_stats: webpack.Stats): BuildResult {
|
122 | let stats = _stats.toJson();
|
123 | let output = {
|
124 | entrypoints: new Map(),
|
125 | lazyAssets: [] as string[],
|
126 | dir: this.outputDir
|
127 | };
|
128 | let nonLazyAssets: Set<string> = new Set();
|
129 | for (let id of Object.keys(stats.entrypoints)) {
|
130 | let entrypoint = stats.entrypoints[id];
|
131 | output.entrypoints.set(id, entrypoint.assets);
|
132 | entrypoint.assets.forEach((asset: string) => nonLazyAssets.add(asset));
|
133 | }
|
134 | for (let asset of stats.assets) {
|
135 | if (!nonLazyAssets.has(asset.name)) {
|
136 | output.lazyAssets.push(asset.name);
|
137 | }
|
138 | }
|
139 | return output;
|
140 | }
|
141 |
|
142 | private writeEntryFile(name: string, deps: BundleDependencies) {
|
143 | writeFileSync(
|
144 | join(this.stagingDir, `${name}.js`),
|
145 | entryTemplate({
|
146 | staticImports: deps.staticImports,
|
147 | dynamicImports: deps.dynamicImports,
|
148 | publicAssetURL: this.publicAssetURL
|
149 | })
|
150 | );
|
151 | }
|
152 |
|
153 | private writeLoaderFile() {
|
154 | writeFileSync(
|
155 | join(this.stagingDir, `l.js`),
|
156 | loader
|
157 | );
|
158 | }
|
159 |
|
160 | private async runWebpack(): Promise<webpack.Stats> {
|
161 | return new Promise((resolve, reject) => {
|
162 | this.webpack.run((err, stats) => {
|
163 | if (err) {
|
164 | this.consoleWrite(stats.toString());
|
165 | reject(err);
|
166 | return;
|
167 | }
|
168 | if (stats.hasErrors()) {
|
169 | this.consoleWrite(stats.toString());
|
170 | reject(new Error('webpack returned errors to ember-auto-import'));
|
171 | return;
|
172 | }
|
173 | if (stats.hasWarnings() || process.env.AUTO_IMPORT_VERBOSE) {
|
174 | this.consoleWrite(stats.toString());
|
175 | }
|
176 | resolve(stats);
|
177 | });
|
178 | }) as Promise<webpack.Stats>;
|
179 | }
|
180 | }
|
181 |
|
182 | export function mergeConfig(dest: object, ...srcs: object[]) {
|
183 | return mergeWith(dest, ...srcs, combine);
|
184 | }
|
185 |
|
186 | function combine(objValue: any, srcValue: any, key: string) {
|
187 | if (key === 'noParse') {
|
188 | return eitherPattern(objValue, srcValue);
|
189 | }
|
190 |
|
191 |
|
192 | if (Array.isArray(objValue)) {
|
193 | return objValue.concat(srcValue);
|
194 | }
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | function eitherPattern(...patterns: any[]): (resource: string) => boolean {
|
205 | let flatPatterns = flatten(patterns);
|
206 | return function(resource) {
|
207 | for (let pattern of flatPatterns) {
|
208 | if (pattern instanceof RegExp) {
|
209 | if (pattern.test(resource)) {
|
210 | return true;
|
211 | }
|
212 | } else if (typeof pattern === 'string') {
|
213 | if (pattern === resource) {
|
214 | return true;
|
215 | }
|
216 | } else if (typeof pattern === 'function') {
|
217 | if (pattern(resource)) {
|
218 | return true;
|
219 | }
|
220 | }
|
221 | }
|
222 | return false;
|
223 | };
|
224 | }
|