@angular-devkit/build-angular 17.0.2 → 17.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@angular-devkit/build-angular",
3
- "version": "17.0.2",
3
+ "version": "17.0.4",
4
4
  "description": "Angular Webpack Build Facade",
5
5
  "main": "src/index.js",
6
6
  "typings": "src/index.d.ts",
7
7
  "builders": "builders.json",
8
8
  "dependencies": {
9
9
  "@ampproject/remapping": "2.2.1",
10
- "@angular-devkit/architect": "0.1700.2",
11
- "@angular-devkit/build-webpack": "0.1700.2",
12
- "@angular-devkit/core": "17.0.2",
10
+ "@angular-devkit/architect": "0.1700.4",
11
+ "@angular-devkit/build-webpack": "0.1700.4",
12
+ "@angular-devkit/core": "17.0.4",
13
13
  "@babel/core": "7.23.2",
14
14
  "@babel/generator": "7.23.0",
15
15
  "@babel/helper-annotate-as-pure": "7.22.5",
@@ -20,7 +20,7 @@
20
20
  "@babel/preset-env": "7.23.2",
21
21
  "@babel/runtime": "7.23.2",
22
22
  "@discoveryjs/json-ext": "0.5.7",
23
- "@ngtools/webpack": "17.0.2",
23
+ "@ngtools/webpack": "17.0.4",
24
24
  "@vitejs/plugin-basic-ssl": "1.0.1",
25
25
  "ansi-colors": "4.1.3",
26
26
  "autoprefixer": "10.4.16",
@@ -74,7 +74,7 @@ async function* runEsBuildBuildAction(action, options) {
74
74
  // Ignore all node modules directories to avoid excessive file watchers.
75
75
  // Package changes are handled below by watching manifest and lock files.
76
76
  '**/node_modules/**',
77
- '**/.*/**',
77
+ `${workspaceRoot.replace(/\\/g, '/')}/**/.*/**`,
78
78
  ],
79
79
  });
80
80
  // Setup abort support
@@ -71,7 +71,9 @@ async function executePostBundleSteps(options, outputFiles, assetFiles, initialF
71
71
  // If localization is enabled, service worker is handled in the inlining process.
72
72
  if (serviceWorker) {
73
73
  try {
74
- const serviceWorkerResult = await (0, service_worker_1.augmentAppWithServiceWorkerEsbuild)(workspaceRoot, serviceWorker, options.baseHref || '/', outputFiles, assetFiles);
74
+ const serviceWorkerResult = await (0, service_worker_1.augmentAppWithServiceWorkerEsbuild)(workspaceRoot, serviceWorker, options.baseHref || '/',
75
+ // Ensure additional files recently added are used
76
+ [...outputFiles, ...additionalOutputFiles], assetFiles);
75
77
  additionalOutputFiles.push((0, utils_1.createOutputFileFromText)('ngsw.json', serviceWorkerResult.manifest, bundler_context_1.BuildOutputFileType.Browser));
76
78
  additionalAssets.push(...serviceWorkerResult.assetFiles);
77
79
  }
@@ -41,7 +41,10 @@ async function inlineI18n(options, executionResult, initialFiles) {
41
41
  try {
42
42
  for (const locale of options.i18nOptions.inlineLocales) {
43
43
  // A locale specific set of files is returned from the inliner.
44
- const localeOutputFiles = await inliner.inlineForLocale(locale, options.i18nOptions.locales[locale].translation);
44
+ const localeInlineResult = await inliner.inlineForLocale(locale, options.i18nOptions.locales[locale].translation);
45
+ const localeOutputFiles = localeInlineResult.outputFiles;
46
+ inlineResult.errors.push(...localeInlineResult.errors);
47
+ inlineResult.warnings.push(...localeInlineResult.warnings);
45
48
  const baseHref = getLocaleBaseHref(options.baseHref, options.i18nOptions, locale) ?? options.baseHref;
46
49
  const { errors, warnings, additionalAssets, additionalOutputFiles, prerenderedRoutes: generatedRoutes, } = await (0, execute_post_bundle_1.executePostBundleSteps)({
47
50
  ...options,
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.normalizeOptions = void 0;
14
+ const node_fs_1 = require("node:fs");
14
15
  const promises_1 = require("node:fs/promises");
15
16
  const node_module_1 = require("node:module");
16
17
  const node_path_1 = __importDefault(require("node:path"));
@@ -35,7 +36,15 @@ const schema_1 = require("./schema");
35
36
  */
36
37
  // eslint-disable-next-line max-lines-per-function
37
38
  async function normalizeOptions(context, projectName, options, plugins) {
38
- const workspaceRoot = context.workspaceRoot;
39
+ // If not explicitly set, default to the Node.js process argument
40
+ const preserveSymlinks = options.preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks');
41
+ // Setup base paths based on workspace root and project information
42
+ const workspaceRoot = preserveSymlinks
43
+ ? context.workspaceRoot
44
+ : // NOTE: promises.realpath should not be used here since it uses realpath.native which
45
+ // can cause case conversion and other undesirable behavior on Windows systems.
46
+ // ref: https://github.com/nodejs/node/issues/7726
47
+ (0, node_fs_1.realpathSync)(context.workspaceRoot);
39
48
  const projectMetadata = await context.getProjectMetadata(projectName);
40
49
  const projectRoot = normalizeDirectoryPath(node_path_1.default.join(workspaceRoot, projectMetadata.root ?? ''));
41
50
  const projectSourceRoot = normalizeDirectoryPath(node_path_1.default.join(workspaceRoot, projectMetadata.sourceRoot ?? 'src'));
@@ -156,7 +165,7 @@ async function normalizeOptions(context, projectName, options, plugins) {
156
165
  };
157
166
  }
158
167
  // Initial options to keep
159
- const { allowedCommonJsDependencies, aot, baseHref, crossOrigin, externalDependencies, extractLicenses, inlineStyleLanguage = 'css', outExtension, serviceWorker, poll, polyfills, preserveSymlinks, statsJson, stylePreprocessorOptions, subresourceIntegrity, verbose, watch, progress = true, externalPackages, deleteOutputPath, namedChunks, budgets, deployUrl, } = options;
168
+ const { allowedCommonJsDependencies, aot, baseHref, crossOrigin, externalDependencies, extractLicenses, inlineStyleLanguage = 'css', outExtension, serviceWorker, poll, polyfills, statsJson, stylePreprocessorOptions, subresourceIntegrity, verbose, watch, progress = true, externalPackages, deleteOutputPath, namedChunks, budgets, deployUrl, } = options;
160
169
  // Return all the normalized options
161
170
  return {
162
171
  advancedOptimizations: !!aot,
@@ -174,8 +183,7 @@ async function normalizeOptions(context, projectName, options, plugins) {
174
183
  poll,
175
184
  progress,
176
185
  externalPackages,
177
- // If not explicitly set, default to the Node.js process argument
178
- preserveSymlinks: preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'),
186
+ preserveSymlinks,
179
187
  stylePreprocessorOptions,
180
188
  subresourceIntegrity,
181
189
  serverEntryPoint,
@@ -246,17 +246,17 @@ function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logg
246
246
  };
247
247
  }),
248
248
  });
249
- logger.info('HMR update sent to client(s)...');
249
+ logger.info('HMR update sent to client(s).');
250
250
  return;
251
251
  }
252
252
  }
253
253
  // Send reload command to clients
254
254
  if (serverOptions.liveReload) {
255
- logger.info('Reloading client(s)...');
256
255
  server.ws.send({
257
256
  type: 'full-reload',
258
257
  path: '*',
259
258
  });
259
+ logger.info('Page reload sent to client(s).');
260
260
  }
261
261
  }
262
262
  function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generatedFiles) {
@@ -329,6 +329,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
329
329
  css: {
330
330
  devSourcemap: true,
331
331
  },
332
+ // Vite will normalize the `base` option by adding a leading and trailing forward slash.
332
333
  base: serverOptions.servePath,
333
334
  resolve: {
334
335
  mainFields: ['es2020', 'browser', 'module', 'main'],
@@ -443,14 +444,14 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
443
444
  }
444
445
  // Parse the incoming request.
445
446
  // The base of the URL is unused but required to parse the URL.
446
- const pathname = pathnameWithoutServePath(req.url, serverOptions);
447
+ const pathname = pathnameWithoutBasePath(req.url, server.config.base);
447
448
  const extension = (0, node_path_1.extname)(pathname);
448
449
  // Rewrite all build assets to a vite raw fs URL
449
450
  const assetSourcePath = assets.get(pathname);
450
451
  if (assetSourcePath !== undefined) {
451
452
  // The encoding needs to match what happens in the vite static middleware.
452
453
  // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
453
- req.url = `/@fs/${encodeURI(assetSourcePath)}`;
454
+ req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
454
455
  next();
455
456
  return;
456
457
  }
@@ -526,7 +527,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
526
527
  }
527
528
  // Parse the incoming request.
528
529
  // The base of the URL is unused but required to parse the URL.
529
- const pathname = pathnameWithoutServePath(req.url, serverOptions);
530
+ const pathname = pathnameWithoutBasePath(req.url, server.config.base);
530
531
  if (pathname === '/' || pathname === `/index.html`) {
531
532
  const rawHtml = outputFiles.get('/index.html')?.contents;
532
533
  if (rawHtml) {
@@ -610,16 +611,13 @@ async function loadViteClientCode(file) {
610
611
  (0, node_assert_1.default)(originalContents !== contents, 'Failed to update Vite client error overlay text.');
611
612
  return contents;
612
613
  }
613
- function pathnameWithoutServePath(url, serverOptions) {
614
+ function pathnameWithoutBasePath(url, basePath) {
614
615
  const parsedUrl = new URL(url, 'http://localhost');
615
- let pathname = decodeURIComponent(parsedUrl.pathname);
616
- if (serverOptions.servePath && pathname.startsWith(serverOptions.servePath)) {
617
- pathname = pathname.slice(serverOptions.servePath.length);
618
- if (pathname[0] !== '/') {
619
- pathname = '/' + pathname;
620
- }
621
- }
622
- return pathname;
616
+ const pathname = decodeURIComponent(parsedUrl.pathname);
617
+ // slice(basePath.length - 1) to retain the trailing slash
618
+ return basePath !== '/' && pathname.startsWith(basePath)
619
+ ? pathname.slice(basePath.length - 1)
620
+ : pathname;
623
621
  }
624
622
  function getDepOptimizationConfig({ disabled, exclude, include, target, prebundleTransformer, ssr, thirdPartySourcemaps, }) {
625
623
  const plugins = [
@@ -35,7 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createCompilerPlugin = void 0;
37
37
  const node_assert_1 = __importDefault(require("node:assert"));
38
- const promises_1 = require("node:fs/promises");
38
+ const node_fs_1 = require("node:fs");
39
39
  const path = __importStar(require("node:path"));
40
40
  const environment_options_1 = require("../../../utils/environment-options");
41
41
  const javascript_transformer_1 = require("../javascript-transformer");
@@ -58,8 +58,11 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
58
58
  if (!preserveSymlinks) {
59
59
  // Use the real path of the tsconfig if not preserving symlinks.
60
60
  // This ensures the TS source file paths are based on the real path of the configuration.
61
+ // NOTE: promises.realpath should not be used here since it uses realpath.native which
62
+ // can cause case conversion and other undesirable behavior on Windows systems.
63
+ // ref: https://github.com/nodejs/node/issues/7726
61
64
  try {
62
- tsconfigPath = await (0, promises_1.realpath)(tsconfigPath);
65
+ tsconfigPath = (0, node_fs_1.realpathSync)(tsconfigPath);
63
66
  }
64
67
  catch { }
65
68
  }
@@ -283,7 +286,8 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
283
286
  }
284
287
  else if (typeof contents === 'string') {
285
288
  // A string indicates untransformed output from the TS/NG compiler
286
- contents = await javascriptTransformer.transformData(request, contents, true /* skipLinker */);
289
+ const sideEffects = await hasSideEffects(request);
290
+ contents = await javascriptTransformer.transformData(request, contents, true /* skipLinker */, sideEffects);
287
291
  // Store as the returned Uint8Array to allow caching the fully transformed code
288
292
  typeScriptFileCache.set(request, contents);
289
293
  }
@@ -294,7 +298,8 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
294
298
  });
295
299
  build.onLoad({ filter: /\.[cm]?js$/ }, (0, load_result_cache_1.createCachedLoad)(pluginOptions.loadResultCache, async (args) => {
296
300
  return (0, profiling_1.profileAsync)('NG_EMIT_JS*', async () => {
297
- const contents = await javascriptTransformer.transformFile(args.path, pluginOptions.jit);
301
+ const sideEffects = await hasSideEffects(args.path);
302
+ const contents = await javascriptTransformer.transformFile(args.path, pluginOptions.jit, sideEffects);
298
303
  return {
299
304
  contents,
300
305
  loader: 'js',
@@ -326,6 +331,19 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
326
331
  void stylesheetBundler.dispose();
327
332
  void compilation.close?.();
328
333
  });
334
+ /**
335
+ * Checks if the file has side-effects when `advancedOptimizations` is enabled.
336
+ */
337
+ async function hasSideEffects(path) {
338
+ if (!pluginOptions.advancedOptimizations) {
339
+ return undefined;
340
+ }
341
+ const { sideEffects } = await build.resolve(path, {
342
+ kind: 'import-statement',
343
+ resolveDir: build.initialOptions.absWorkingDir ?? '',
344
+ });
345
+ return sideEffects;
346
+ }
329
347
  },
330
348
  };
331
349
  }
@@ -282,17 +282,6 @@ function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfi
282
282
  // Locale data should go first so that project provided polyfill code can augment if needed.
283
283
  let needLocaleDataPlugin = false;
284
284
  if (i18nOptions.shouldInline) {
285
- // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier
286
- polyfills.unshift('angular:locale/placeholder');
287
- buildOptions.plugins?.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
288
- namespace: 'angular:locale/placeholder',
289
- entryPointOnly: false,
290
- loadContent: () => ({
291
- contents: `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n`,
292
- loader: 'js',
293
- resolveDir: workspaceRoot,
294
- }),
295
- }));
296
285
  // Add locale data for all active locales
297
286
  // TODO: Inject each individually within the inlining process itself
298
287
  for (const locale of i18nOptions.inlineLocales) {
@@ -347,8 +336,13 @@ function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfi
347
336
  let contents = polyfillPaths
348
337
  .map((file) => `import '${file.replace(/\\/g, '/')}';`)
349
338
  .join('\n');
350
- // If not inlining translations and source locale is defined, inject the locale specifier
351
- if (!i18nOptions.shouldInline && i18nOptions.hasDefinedSourceLocale) {
339
+ // The below should be done after loading `$localize` as otherwise the locale will be overridden.
340
+ if (i18nOptions.shouldInline) {
341
+ // When inlining, a placeholder is used to allow the post-processing step to inject the $localize locale identifier.
342
+ contents += '(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n';
343
+ }
344
+ else if (i18nOptions.hasDefinedSourceLocale) {
345
+ // If not inlining translations and source locale is defined, inject the locale specifier.
352
346
  contents += `(globalThis.$localize ??= {}).locale = "${i18nOptions.sourceLocale}";\n`;
353
347
  }
354
348
  return {
@@ -32,6 +32,11 @@ interface InlineRequest {
32
32
  */
33
33
  export default function inlineLocale(request: InlineRequest): Promise<{
34
34
  file: string;
35
- contents: string;
36
- }[]>;
35
+ code: string;
36
+ map: string | undefined;
37
+ messages: {
38
+ type: "error" | "warning";
39
+ message: string;
40
+ }[];
41
+ }>;
37
42
  export {};
@@ -32,13 +32,12 @@ async function inlineLocale(request) {
32
32
  const code = await data.text();
33
33
  const map = await files.get(request.filename + '.map')?.text();
34
34
  const result = await transformWithBabel(code, map && JSON.parse(map), request);
35
- // TODO: Return diagnostics
36
- // TODO: Consider buffer transfer instead of string copying
37
- const response = [{ file: request.filename, contents: result.code }];
38
- if (result.map) {
39
- response.push({ file: request.filename + '.map', contents: result.map });
40
- }
41
- return response;
35
+ return {
36
+ file: request.filename,
37
+ code: result.code,
38
+ map: result.map,
39
+ messages: result.diagnostics.messages,
40
+ };
42
41
  }
43
42
  exports.default = inlineLocale;
44
43
  /**
@@ -31,7 +31,11 @@ export declare class I18nInliner {
31
31
  * @param translation The translation messages to use when inlining.
32
32
  * @returns A promise that resolves to an array of OutputFiles representing a translated result.
33
33
  */
34
- inlineForLocale(locale: string, translation: Record<string, unknown> | undefined): Promise<BuildOutputFile[]>;
34
+ inlineForLocale(locale: string, translation: Record<string, unknown> | undefined): Promise<{
35
+ outputFiles: BuildOutputFile[];
36
+ errors: string[];
37
+ warnings: string[];
38
+ }>;
35
39
  /**
36
40
  * Stops all active transformation tasks and shuts down all workers.
37
41
  * @returns A void promise that resolves when closing is complete.
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.I18nInliner = void 0;
14
+ const node_assert_1 = __importDefault(require("node:assert"));
14
15
  const piscina_1 = __importDefault(require("piscina"));
15
16
  const bundler_context_1 = require("./bundler-context");
16
17
  const utils_1 = require("./utils");
@@ -109,12 +110,33 @@ class I18nInliner {
109
110
  // Wait for all file requests to complete
110
111
  const rawResults = await Promise.all(requests);
111
112
  // Convert raw results to output file objects and include all unmodified files
112
- return [
113
- ...rawResults.flat().map(({ file, contents }) =>
114
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
115
- (0, utils_1.createOutputFileFromText)(file, contents, this.#fileToType.get(file))),
113
+ const errors = [];
114
+ const warnings = [];
115
+ const outputFiles = [
116
+ ...rawResults.flatMap(({ file, code, map, messages }) => {
117
+ const type = this.#fileToType.get(file);
118
+ (0, node_assert_1.default)(type !== undefined, 'localized file should always have a type' + file);
119
+ const resultFiles = [(0, utils_1.createOutputFileFromText)(file, code, type)];
120
+ if (map) {
121
+ resultFiles.push((0, utils_1.createOutputFileFromText)(file + '.map', map, type));
122
+ }
123
+ for (const message of messages) {
124
+ if (message.type === 'error') {
125
+ errors.push(message.message);
126
+ }
127
+ else {
128
+ warnings.push(message.message);
129
+ }
130
+ }
131
+ return resultFiles;
132
+ }),
116
133
  ...this.#unmodifiedFiles.map((file) => file.clone()),
117
134
  ];
135
+ return {
136
+ outputFiles,
137
+ errors,
138
+ warnings,
139
+ };
118
140
  }
119
141
  /**
120
142
  * Stops all active transformation tasks and shuts down all workers.
@@ -11,7 +11,8 @@ interface JavaScriptTransformRequest {
11
11
  sourcemap: boolean;
12
12
  thirdPartySourcemaps: boolean;
13
13
  advancedOptimizations: boolean;
14
- skipLinker: boolean;
14
+ skipLinker?: boolean;
15
+ sideEffects?: boolean;
15
16
  jit: boolean;
16
17
  }
17
18
  export default function transformJavaScript(request: JavaScriptTransformRequest): Promise<Uint8Array>;
@@ -50,10 +50,8 @@ async function transformWithBabel({ filename, data, ...options }) {
50
50
  // Strip sourcemaps if they should not be used
51
51
  return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
52
52
  }
53
- // `@angular/platform-server/init` and `@angular/common/locales/global` entry-points are side effectful.
54
- const safeAngularPackage = /[\\/]node_modules[\\/]@angular[\\/]/.test(filename) &&
55
- !/@angular[\\/]platform-server[\\/]f?esm2022[\\/]init/.test(filename) &&
56
- !/@angular[\\/]common[\\/]locales[\\/]global/.test(filename);
53
+ const sideEffectFree = options.sideEffects === false;
54
+ const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename);
57
55
  // Lazy load the linker plugin only when linking is required
58
56
  if (shouldLink) {
59
57
  linkerPluginCreator ??= (await (0, load_esm_1.loadEsmModule)('@angular/compiler-cli/linker/babel')).createEs2015LinkerPlugin;
@@ -78,6 +76,7 @@ async function transformWithBabel({ filename, data, ...options }) {
78
76
  },
79
77
  optimize: options.advancedOptimizations && {
80
78
  pureTopLevel: safeAngularPackage,
79
+ wrapDecorators: sideEffectFree,
81
80
  },
82
81
  },
83
82
  ],
@@ -30,18 +30,20 @@ export declare class JavaScriptTransformer {
30
30
  * If no transformations are required, the data for the original file will be returned.
31
31
  * @param filename The full path to the file.
32
32
  * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking.
33
+ * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
33
34
  * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
34
35
  */
35
- transformFile(filename: string, skipLinker?: boolean): Promise<Uint8Array>;
36
+ transformFile(filename: string, skipLinker?: boolean, sideEffects?: boolean): Promise<Uint8Array>;
36
37
  /**
37
38
  * Performs JavaScript transformations on the provided data of a file. The file does not need
38
39
  * to exist on the filesystem.
39
40
  * @param filename The full path of the file represented by the data.
40
41
  * @param data The data of the file that should be transformed.
41
42
  * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking.
43
+ * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
42
44
  * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
43
45
  */
44
- transformData(filename: string, data: string, skipLinker: boolean): Promise<Uint8Array>;
46
+ transformData(filename: string, data: string, skipLinker: boolean, sideEffects?: boolean): Promise<Uint8Array>;
45
47
  /**
46
48
  * Stops all active transformation tasks and shuts down all workers.
47
49
  * @returns A void promise that resolves when closing is complete.
@@ -54,9 +54,10 @@ class JavaScriptTransformer {
54
54
  * If no transformations are required, the data for the original file will be returned.
55
55
  * @param filename The full path to the file.
56
56
  * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking.
57
+ * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
57
58
  * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
58
59
  */
59
- transformFile(filename, skipLinker) {
60
+ transformFile(filename, skipLinker, sideEffects) {
60
61
  const pendingKey = `${!!skipLinker}--${filename}`;
61
62
  let pending = this.#pendingfileResults?.get(pendingKey);
62
63
  if (pending === undefined) {
@@ -65,6 +66,7 @@ class JavaScriptTransformer {
65
66
  pending = this.#ensureWorkerPool().run({
66
67
  filename,
67
68
  skipLinker,
69
+ sideEffects,
68
70
  ...this.#commonOptions,
69
71
  });
70
72
  this.#pendingfileResults?.set(pendingKey, pending);
@@ -77,9 +79,10 @@ class JavaScriptTransformer {
77
79
  * @param filename The full path of the file represented by the data.
78
80
  * @param data The data of the file that should be transformed.
79
81
  * @param skipLinker If true, bypass all Angular linker processing; if false, attempt linking.
82
+ * @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
80
83
  * @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
81
84
  */
82
- async transformData(filename, data, skipLinker) {
85
+ async transformData(filename, data, skipLinker, sideEffects) {
83
86
  // Perform a quick test to determine if the data needs any transformations.
84
87
  // This allows directly returning the data without the worker communication overhead.
85
88
  if (skipLinker && !this.#commonOptions.advancedOptimizations) {
@@ -91,6 +94,7 @@ class JavaScriptTransformer {
91
94
  filename,
92
95
  data,
93
96
  skipLinker,
97
+ sideEffects,
94
98
  ...this.#commonOptions,
95
99
  });
96
100
  }
@@ -50,6 +50,10 @@ let postcss;
50
50
  * Based on https://tailwindcss.com/docs/functions-and-directives
51
51
  */
52
52
  const TAILWIND_KEYWORDS = ['@tailwind', '@layer', '@apply', '@config', 'theme(', 'screen('];
53
+ /**
54
+ * Cached postcss instances that can be re-used between various StylesheetPluginFactory instances.
55
+ */
56
+ const postcssProcessor = new Map();
53
57
  class StylesheetPluginFactory {
54
58
  options;
55
59
  cache;
@@ -73,9 +77,15 @@ class StylesheetPluginFactory {
73
77
  return this.postcssProcessor;
74
78
  }
75
79
  if (options.tailwindConfiguration) {
76
- postcss ??= (await Promise.resolve().then(() => __importStar(require('postcss')))).default;
77
- const tailwind = await Promise.resolve(`${options.tailwindConfiguration.package}`).then(s => __importStar(require(s)));
78
- this.postcssProcessor = postcss().use(tailwind.default({ config: options.tailwindConfiguration.file }));
80
+ const { package: tailwindPackage, file: config } = options.tailwindConfiguration;
81
+ const postCssInstanceKey = tailwindPackage + ':' + config;
82
+ this.postcssProcessor = postcssProcessor.get(postCssInstanceKey)?.deref();
83
+ if (!this.postcssProcessor) {
84
+ postcss ??= (await Promise.resolve().then(() => __importStar(require('postcss')))).default;
85
+ const tailwind = await Promise.resolve(`${tailwindPackage}`).then(s => __importStar(require(s)));
86
+ this.postcssProcessor = postcss().use(tailwind.default({ config }));
87
+ postcssProcessor.set(postCssInstanceKey, new WeakRef(this.postcssProcessor));
88
+ }
79
89
  }
80
90
  return this.postcssProcessor;
81
91
  };
@@ -9,6 +9,7 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.createWatcher = exports.ChangedFiles = void 0;
11
11
  const chokidar_1 = require("chokidar");
12
+ const node_path_1 = require("node:path");
12
13
  class ChangedFiles {
13
14
  added = new Set();
14
15
  modified = new Set();
@@ -37,19 +38,64 @@ function createWatcher(options) {
37
38
  const nextQueue = [];
38
39
  let currentChanges;
39
40
  let nextWaitTimeout;
40
- watcher.on('all', (event, path) => {
41
+ /**
42
+ * We group the current events in a map as on Windows with certain IDE a file contents change can trigger multiple events.
43
+ *
44
+ * Example:
45
+ * rename | 'C:/../src/app/app.component.css'
46
+ * rename | 'C:/../src/app/app.component.css'
47
+ * change | 'C:/../src/app/app.component.css'
48
+ *
49
+ */
50
+ let currentEvents;
51
+ /**
52
+ * Using `watcher.on('all')` does not capture some of events fired when using Visual studio and this does not happen all the time,
53
+ * but only after a file has been changed 3 or more times.
54
+ *
55
+ * Also, some IDEs such as Visual Studio (not VS Code) will fire a rename event instead of unlink when a file is renamed or changed.
56
+ *
57
+ * Example:
58
+ * ```
59
+ * watcher.on('raw')
60
+ * Change 1
61
+ * rename | 'C:/../src/app/app.component.css'
62
+ * rename | 'C:/../src/app/app.component.css'
63
+ * change | 'C:/../src/app/app.component.css'
64
+ *
65
+ * Change 2
66
+ * rename | 'C:/../src/app/app.component.css'
67
+ * rename | 'C:/../src/app/app.component.css'
68
+ * change | 'C:/../src/app/app.component.css'
69
+ *
70
+ * Change 3
71
+ * rename | 'C:/../src/app/app.component.css'
72
+ * rename | 'C:/../src/app/app.component.css'
73
+ * change | 'C:/../src/app/app.component.css'
74
+ *
75
+ * watcher.on('all')
76
+ * Change 1
77
+ * change | 'C:\\..\\src\\app\\app.component.css'
78
+ *
79
+ * Change 2
80
+ * unlink | 'C:\\..\\src\\app\\app.component.css'
81
+ *
82
+ * Change 3
83
+ * ... (Nothing)
84
+ * ```
85
+ */
86
+ watcher.on('raw', (event, path, { watchedPath }) => {
41
87
  switch (event) {
42
88
  case 'add':
43
- currentChanges ??= new ChangedFiles();
44
- currentChanges.added.add(path);
45
- break;
46
89
  case 'change':
47
- currentChanges ??= new ChangedFiles();
48
- currentChanges.modified.add(path);
49
- break;
90
+ // When using Visual Studio the rename event is fired before a change event when the contents of the file changed
91
+ // or instead of `unlink` when the file has been renamed.
50
92
  case 'unlink':
51
- currentChanges ??= new ChangedFiles();
52
- currentChanges.removed.add(path);
93
+ case 'rename':
94
+ // When polling is enabled `watchedPath` can be undefined.
95
+ // `path` is always normalized unlike `watchedPath`.
96
+ const changedPath = watchedPath ? (0, node_path_1.normalize)(watchedPath) : path;
97
+ currentEvents ??= new Map();
98
+ currentEvents.set(changedPath, event);
53
99
  break;
54
100
  default:
55
101
  return;
@@ -59,10 +105,25 @@ function createWatcher(options) {
59
105
  nextWaitTimeout = setTimeout(() => {
60
106
  nextWaitTimeout = undefined;
61
107
  const next = nextQueue.shift();
62
- if (next) {
63
- const value = currentChanges;
64
- currentChanges = undefined;
65
- next(value);
108
+ if (next && currentEvents) {
109
+ const events = currentEvents;
110
+ currentEvents = undefined;
111
+ const currentChanges = new ChangedFiles();
112
+ for (const [path, event] of events) {
113
+ switch (event) {
114
+ case 'add':
115
+ currentChanges.added.add(path);
116
+ break;
117
+ case 'change':
118
+ currentChanges.modified.add(path);
119
+ break;
120
+ case 'unlink':
121
+ case 'rename':
122
+ currentChanges.removed.add(path);
123
+ break;
124
+ }
125
+ }
126
+ next(currentChanges);
66
127
  }
67
128
  }, 250);
68
129
  nextWaitTimeout?.unref();