-
Notifications
You must be signed in to change notification settings - Fork 12k
/
Copy pathwebpack-loader.ts
250 lines (223 loc) · 9.48 KB
/
webpack-loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import remapping from '@ampproject/remapping';
import { custom } from 'babel-loader';
import { ScriptTarget } from 'typescript';
import { loadEsmModule } from '../utils/load-esm';
import { ApplicationPresetOptions, I18nPluginCreators } from './presets/application';
interface AngularCustomOptions extends Omit<ApplicationPresetOptions, 'instrumentCode'> {
instrumentCode?: {
/** node_modules and test files are always excluded. */
excludedPaths: Set<String>;
includedBasePath: string;
};
}
export type AngularBabelLoaderOptions = AngularCustomOptions & Record<string, unknown>;
// Extract Sourcemap input type from the remapping function since it is not currently exported
type SourceMapInput = Exclude<Parameters<typeof remapping>[0], unknown[]>;
/**
* Cached instance of the compiler-cli linker's needsLinking function.
*/
let needsLinking: typeof import('@angular/compiler-cli/linker').needsLinking | undefined;
/**
* Cached instance of the compiler-cli linker's Babel plugin factory function.
*/
let linkerPluginCreator:
| typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin
| undefined;
/**
* Cached instance of the localize Babel plugins factory functions.
*/
let i18nPluginCreators: I18nPluginCreators | undefined;
async function requiresLinking(path: string, source: string): Promise<boolean> {
// @angular/core and @angular/compiler will cause false positives
// Also, TypeScript files do not require linking
if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) {
return false;
}
if (!needsLinking) {
// Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const linkerModule = await loadEsmModule<typeof import('@angular/compiler-cli/linker')>(
'@angular/compiler-cli/linker',
);
needsLinking = linkerModule.needsLinking;
}
return needsLinking(path, source);
}
export default custom<ApplicationPresetOptions>(() => {
const baseOptions = Object.freeze({
babelrc: false,
configFile: false,
compact: false,
cacheCompression: false,
sourceType: 'unambiguous',
inputSourceMap: false,
});
return {
async customOptions(options, { source }) {
const { i18n, scriptTarget, aot, optimize, instrumentCode, ...rawOptions } =
options as AngularBabelLoaderOptions;
// Must process file if plugins are added
let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
const customOptions: ApplicationPresetOptions = {
forceAsyncTransformation: false,
forceES5: false,
angularLinker: undefined,
i18n: undefined,
instrumentCode: undefined,
};
// Analyze file for linking
if (await requiresLinking(this.resourcePath, source)) {
if (!linkerPluginCreator) {
// Load ESM `@angular/compiler-cli/linker/babel` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
const linkerBabelModule = await loadEsmModule<
typeof import('@angular/compiler-cli/linker/babel')
>('@angular/compiler-cli/linker/babel');
linkerPluginCreator = linkerBabelModule.createEs2015LinkerPlugin;
}
customOptions.angularLinker = {
shouldLink: true,
jitMode: aot !== true,
linkerPluginCreator,
};
shouldProcess = true;
}
// Analyze for ES target processing
const esTarget = scriptTarget as ScriptTarget | undefined;
if (esTarget !== undefined) {
if (esTarget < ScriptTarget.ES2015) {
customOptions.forceES5 = true;
} else if (esTarget >= ScriptTarget.ES2017 || /\.[cm]?js$/.test(this.resourcePath)) {
// Application code (TS files) will only contain native async if target is ES2017+.
// However, third-party libraries can regardless of the target option.
// APF packages with code in [f]esm2015 directories is downlevelled to ES2015 and
// will not have native async.
customOptions.forceAsyncTransformation =
!/[\\/][_f]?esm2015[\\/]/.test(this.resourcePath) && source.includes('async');
}
shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5 || false;
}
// Analyze for i18n inlining
if (
i18n &&
!/[\\/]@angular[\\/](?:compiler|localize)/.test(this.resourcePath) &&
source.includes('$localize')
) {
// Load the i18n plugin creators from the new `@angular/localize/tools` entry point.
// This may fail during the transition to ESM due to the entry point not yet existing.
// During the transition, this will always attempt to load the entry point for each file.
// This will only occur during prerelease and will be automatically corrected once the new
// entry point exists.
// TODO_ESM: Make import failure an error once the `tools` entry point exists.
if (i18nPluginCreators === undefined) {
// Load ESM `@angular/localize/tools` using the TypeScript dynamic import workaround.
// Once TypeScript provides support for keeping the dynamic import this workaround can be
// changed to a direct dynamic import.
try {
i18nPluginCreators = await loadEsmModule<I18nPluginCreators>('@angular/localize/tools');
} catch {}
}
customOptions.i18n = {
...(i18n as NonNullable<ApplicationPresetOptions['i18n']>),
pluginCreators: i18nPluginCreators,
};
shouldProcess = true;
}
if (optimize) {
const angularPackage = /[\\/]node_modules[\\/]@angular[\\/]/.test(this.resourcePath);
customOptions.optimize = {
// Angular packages provide additional tested side effects guarantees and can use
// otherwise unsafe optimizations.
looseEnums: angularPackage,
pureTopLevel: angularPackage,
// JavaScript modules that are marked as side effect free are considered to have
// no decorators that contain non-local effects.
wrapDecorators: !!this._module?.factoryMeta?.sideEffectFree,
};
shouldProcess = true;
}
if (
instrumentCode &&
!instrumentCode.excludedPaths.has(this.resourcePath) &&
!/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(this.resourcePath) &&
this.resourcePath.startsWith(instrumentCode.includedBasePath)
) {
// `babel-plugin-istanbul` has it's own includes but we do the below so that we avoid running the the loader.
customOptions.instrumentCode = {
includedBasePath: instrumentCode.includedBasePath,
};
shouldProcess = true;
}
// Add provided loader options to default base options
const loaderOptions: Record<string, unknown> = {
...baseOptions,
...rawOptions,
cacheIdentifier: JSON.stringify({
buildAngular: require('../../package.json').version,
customOptions,
baseOptions,
rawOptions,
}),
};
// Skip babel processing if no actions are needed
if (!shouldProcess) {
// Force the current file to be ignored
loaderOptions.ignore = [() => true];
}
return { custom: customOptions, loader: loaderOptions };
},
config(configuration, { customOptions }) {
return {
...configuration.options,
// Using `false` disables babel from attempting to locate sourcemaps or process any inline maps.
// The babel types do not include the false option even though it is valid
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputSourceMap: false as any,
presets: [
...(configuration.options.presets || []),
[
require('./presets/application').default,
{
...customOptions,
diagnosticReporter: (type, message) => {
switch (type) {
case 'error':
this.emitError(message);
break;
case 'info':
// Webpack does not currently have an informational diagnostic
case 'warning':
this.emitWarning(message);
break;
}
},
} as ApplicationPresetOptions,
],
],
};
},
result(result, { map: inputSourceMap }) {
if (result.map && inputSourceMap) {
// Merge the intermediate sourcemap generated by babel with the input source map.
// The casting is required due to slight differences in the types for babel and
// `@ampproject/remapping` source map objects but both are compatible with Webpack.
// This method for merging is used because it provides more accurate output
// and is faster while using less memory.
result.map = remapping(
[result.map as SourceMapInput, inputSourceMap as SourceMapInput],
() => null,
) as typeof result.map;
}
return result;
},
};
});