@angular/build 18.0.0-rc.1 → 18.0.0-rc.3

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "18.0.0-rc.1",
3
+ "version": "18.0.0-rc.3",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,7 +23,7 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.1800.0-rc.1",
26
+ "@angular-devkit/architect": "0.1800.0-rc.3",
27
27
  "@babel/core": "7.24.5",
28
28
  "@babel/helper-annotate-as-pure": "7.22.5",
29
29
  "@babel/helper-split-export-declaration": "7.24.5",
@@ -31,20 +31,20 @@
31
31
  "ansi-colors": "4.1.3",
32
32
  "browserslist": "^4.23.0",
33
33
  "critters": "0.0.22",
34
- "esbuild": "0.20.2",
34
+ "esbuild": "0.21.3",
35
35
  "fast-glob": "3.3.2",
36
36
  "https-proxy-agent": "7.0.4",
37
- "inquirer": "9.2.20",
37
+ "inquirer": "9.2.22",
38
+ "lmdb": "3.0.8",
38
39
  "magic-string": "0.30.10",
39
40
  "mrmime": "2.0.0",
40
41
  "ora": "5.4.1",
41
42
  "picomatch": "4.0.2",
42
- "piscina": "4.4.0",
43
+ "piscina": "4.5.0",
43
44
  "parse5-html-rewriting-stream": "7.0.0",
44
- "postcss": "8.4.38",
45
- "sass": "1.76.0",
46
- "semver": "7.6.0",
47
- "undici": "6.15.0",
45
+ "sass": "1.77.2",
46
+ "semver": "7.6.2",
47
+ "undici": "6.18.0",
48
48
  "vite": "5.2.11",
49
49
  "watchpack": "2.4.1"
50
50
  },
@@ -136,6 +136,8 @@ async function* runEsBuildBuildAction(action, options) {
136
136
  if (verbose) {
137
137
  logger.info(changes.toDebugString());
138
138
  }
139
+ // Clear removed files from current watch files
140
+ changes.removed.forEach((removedPath) => currentWatchFiles.delete(removedPath));
139
141
  result = await withProgress('Changes detected. Rebuilding...', () => action(result.createRebuildState(changes)));
140
142
  // Log all diagnostic (error/warning/logs) messages
141
143
  await (0, utils_1.logMessages)(logger, result, colors, jsonLogs);
@@ -133,7 +133,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
133
133
  // If server is active, send an error notification
134
134
  if (result.errors?.length && server) {
135
135
  hadError = true;
136
- server.ws.send({
136
+ server.hot.send({
137
137
  type: 'error',
138
138
  err: {
139
139
  message: result.errors[0].text,
@@ -147,7 +147,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
147
147
  else if (hadError && server) {
148
148
  hadError = false;
149
149
  // Send an empty update to clear the error overlay
150
- server.ws.send({
150
+ server.hot.send({
151
151
  'type': 'update',
152
152
  updates: [],
153
153
  });
@@ -189,15 +189,13 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
189
189
  if (!projectName) {
190
190
  throw new Error('The builder requires a target.');
191
191
  }
192
+ context.logger.info('NOTE: Raw file sizes do not reflect development server per-request transformations.');
192
193
  const { root = '' } = await context.getProjectMetadata(projectName);
193
194
  const projectRoot = (0, node_path_1.join)(context.workspaceRoot, root);
194
195
  const browsers = (0, internal_1.getSupportedBrowsers)(projectRoot, context.logger);
195
196
  const target = (0, internal_1.transformSupportedBrowsersToTargets)(browsers);
196
- const polyfills = Array.isArray((browserOptions.polyfills ??= []))
197
- ? browserOptions.polyfills
198
- : [browserOptions.polyfills];
199
197
  // Setup server and start listening
200
- const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, !!browserOptions.ssr, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), browserOptions.loader, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
198
+ const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, !!browserOptions.ssr, prebundleTransformer, target, (0, internal_1.isZonelessApp)(browserOptions.polyfills), browserOptions.loader, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
201
199
  server = await createServer(serverConfiguration);
202
200
  await server.listen();
203
201
  if (serverConfiguration.ssr?.optimizeDeps?.disabled === false) {
@@ -224,7 +222,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
224
222
  key: 'r',
225
223
  description: 'force reload browser',
226
224
  action(server) {
227
- server.ws.send({
225
+ server.hot.send({
228
226
  type: 'full-reload',
229
227
  path: '*',
230
228
  });
@@ -354,6 +352,14 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
354
352
  css: {
355
353
  devSourcemap: true,
356
354
  },
355
+ // Ensure custom 'file' loader build option entries are handled by Vite in application code that
356
+ // reference third-party libraries. Relative usage is handled directly by the build and not Vite.
357
+ // Only 'file' loader entries are currently supported directly by Vite.
358
+ assetsInclude: prebundleLoaderExtensions &&
359
+ Object.entries(prebundleLoaderExtensions)
360
+ .filter(([, value]) => value === 'file')
361
+ // Create a file extension glob for each key
362
+ .map(([key]) => '*' + key),
357
363
  // Vite will normalize the `base` option by adding a leading slash.
358
364
  base: serverOptions.servePath,
359
365
  resolve: {
@@ -216,77 +216,10 @@ function default_1() {
216
216
  const { wrapStatementPaths, hasPotentialSideEffects } = exportDefaultAnalysis.get(classNode) ??
217
217
  analyzeClassSiblings(origin, classNode.id, wrapDecorators);
218
218
  visitedClasses.add(classNode);
219
- if (hasPotentialSideEffects) {
220
- return;
221
- }
222
219
  // If no statements to wrap, check for static class properties.
223
- // Static class properties may be downleveled at later stages in the build pipeline
224
- // which results in additional function calls outside the class body. These calls
225
- // then cause the class to be referenced and not eligible for removal. Since it is
226
- // not known at this stage whether the class needs to be downleveled, the transform
227
- // wraps classes preemptively to allow for potential removal within the optimization
228
- // stages.
229
- if (wrapStatementPaths.length === 0) {
230
- let shouldWrap = false;
231
- for (const element of path.get('body').get('body')) {
232
- if (element.isClassProperty()) {
233
- // Only need to analyze static properties
234
- if (!element.node.static) {
235
- continue;
236
- }
237
- // Check for potential side effects.
238
- // These checks are conservative and could potentially be expanded in the future.
239
- const elementKey = element.get('key');
240
- const elementValue = element.get('value');
241
- if (elementKey.isIdentifier() &&
242
- (!elementValue.isExpression() ||
243
- canWrapProperty(elementKey.node.name, elementValue))) {
244
- shouldWrap = true;
245
- }
246
- else {
247
- // Not safe to wrap
248
- shouldWrap = false;
249
- break;
250
- }
251
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
252
- }
253
- else if (element.isStaticBlock()) {
254
- // Only need to analyze static blocks
255
- const body = element.get('body');
256
- if (Array.isArray(body) && body.length > 1) {
257
- // Not safe to wrap
258
- shouldWrap = false;
259
- break;
260
- }
261
- const expression = body.find((n) => n.isExpressionStatement());
262
- const assignmentExpression = expression?.get('expression');
263
- if (assignmentExpression?.isAssignmentExpression()) {
264
- const left = assignmentExpression.get('left');
265
- if (!left.isMemberExpression()) {
266
- continue;
267
- }
268
- if (!left.get('object').isThisExpression()) {
269
- // Not safe to wrap
270
- shouldWrap = false;
271
- break;
272
- }
273
- const element = left.get('property');
274
- const right = assignmentExpression.get('right');
275
- if (element.isIdentifier() &&
276
- (!right.isExpression() || canWrapProperty(element.node.name, right))) {
277
- shouldWrap = true;
278
- }
279
- else {
280
- // Not safe to wrap
281
- shouldWrap = false;
282
- break;
283
- }
284
- }
285
- }
286
- }
287
- if (!shouldWrap) {
288
- return;
289
- }
220
+ if (hasPotentialSideEffects ||
221
+ (wrapStatementPaths.length === 0 && !analyzeClassStaticProperties(path).shouldWrap)) {
222
+ return;
290
223
  }
291
224
  const wrapStatementNodes = [];
292
225
  for (const statementPath of wrapStatementPaths) {
@@ -310,9 +243,7 @@ function default_1() {
310
243
  ClassExpression(path, state) {
311
244
  const { node: classNode, parentPath } = path;
312
245
  const { wrapDecorators } = state.opts;
313
- // Class expressions are used by TypeScript to represent downlevel class/constructor decorators.
314
- // If not wrapping decorators, they do not need to be processed.
315
- if (!wrapDecorators || visitedClasses.has(classNode)) {
246
+ if (visitedClasses.has(classNode)) {
316
247
  return;
317
248
  }
318
249
  if (!parentPath.isVariableDeclarator() || !core_1.types.isIdentifier(parentPath.node.id)) {
@@ -324,7 +255,9 @@ function default_1() {
324
255
  }
325
256
  const { wrapStatementPaths, hasPotentialSideEffects } = analyzeClassSiblings(origin, parentPath.node.id, wrapDecorators);
326
257
  visitedClasses.add(classNode);
327
- if (hasPotentialSideEffects || wrapStatementPaths.length === 0) {
258
+ // If no statements to wrap, check for static class properties.
259
+ if (hasPotentialSideEffects ||
260
+ (wrapStatementPaths.length === 0 && !analyzeClassStaticProperties(path).shouldWrap)) {
328
261
  return;
329
262
  }
330
263
  const wrapStatementNodes = [];
@@ -349,3 +282,69 @@ function default_1() {
349
282
  };
350
283
  }
351
284
  exports.default = default_1;
285
+ /**
286
+ * Static class properties may be downleveled at later stages in the build pipeline
287
+ * which results in additional function calls outside the class body. These calls
288
+ * then cause the class to be referenced and not eligible for removal. Since it is
289
+ * not known at this stage whether the class needs to be downleveled, the transform
290
+ * wraps classes preemptively to allow for potential removal within the optimization stages.
291
+ */
292
+ function analyzeClassStaticProperties(path) {
293
+ let shouldWrap = false;
294
+ for (const element of path.get('body').get('body')) {
295
+ if (element.isClassProperty()) {
296
+ // Only need to analyze static properties
297
+ if (!element.node.static) {
298
+ continue;
299
+ }
300
+ // Check for potential side effects.
301
+ // These checks are conservative and could potentially be expanded in the future.
302
+ const elementKey = element.get('key');
303
+ const elementValue = element.get('value');
304
+ if (elementKey.isIdentifier() &&
305
+ (!elementValue.isExpression() || canWrapProperty(elementKey.node.name, elementValue))) {
306
+ shouldWrap = true;
307
+ }
308
+ else {
309
+ // Not safe to wrap
310
+ shouldWrap = false;
311
+ break;
312
+ }
313
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
314
+ }
315
+ else if (element.isStaticBlock()) {
316
+ // Only need to analyze static blocks
317
+ const body = element.get('body');
318
+ if (Array.isArray(body) && body.length > 1) {
319
+ // Not safe to wrap
320
+ shouldWrap = false;
321
+ break;
322
+ }
323
+ const expression = body.find((n) => n.isExpressionStatement());
324
+ const assignmentExpression = expression?.get('expression');
325
+ if (assignmentExpression?.isAssignmentExpression()) {
326
+ const left = assignmentExpression.get('left');
327
+ if (!left.isMemberExpression()) {
328
+ continue;
329
+ }
330
+ if (!left.get('object').isThisExpression()) {
331
+ // Not safe to wrap
332
+ shouldWrap = false;
333
+ break;
334
+ }
335
+ const element = left.get('property');
336
+ const right = assignmentExpression.get('right');
337
+ if (element.isIdentifier() &&
338
+ (!right.isExpression() || canWrapProperty(element.node.name, right))) {
339
+ shouldWrap = true;
340
+ }
341
+ else {
342
+ // Not safe to wrap
343
+ shouldWrap = false;
344
+ break;
345
+ }
346
+ }
347
+ }
348
+ }
349
+ return { shouldWrap };
350
+ }
@@ -38,6 +38,7 @@ const node_assert_1 = __importDefault(require("node:assert"));
38
38
  const path = __importStar(require("node:path"));
39
39
  const environment_options_1 = require("../../../utils/environment-options");
40
40
  const javascript_transformer_1 = require("../javascript-transformer");
41
+ const lmdb_cache_store_1 = require("../lmdb-cache-store");
41
42
  const load_result_cache_1 = require("../load-result-cache");
42
43
  const profiling_1 = require("../profiling");
43
44
  const compilation_1 = require("./compilation");
@@ -54,7 +55,11 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
54
55
  let setupWarnings = [];
55
56
  const preserveSymlinks = build.initialOptions.preserveSymlinks;
56
57
  // Initialize a worker pool for JavaScript transformations
57
- const javascriptTransformer = new javascript_transformer_1.JavaScriptTransformer(pluginOptions, environment_options_1.maxWorkers);
58
+ let cacheStore;
59
+ if (pluginOptions.sourceFileCache?.persistentCachePath) {
60
+ cacheStore = new lmdb_cache_store_1.LmbdCacheStore(path.join(pluginOptions.sourceFileCache.persistentCachePath, 'angular-compiler.db'));
61
+ }
62
+ const javascriptTransformer = new javascript_transformer_1.JavaScriptTransformer(pluginOptions, environment_options_1.maxWorkers, cacheStore?.createCache('jstransformer'));
58
63
  // Setup defines based on the values used by the Angular compiler-cli
59
64
  build.initialOptions.define ??= {};
60
65
  build.initialOptions.define['ngI18nClosureMode'] ??= 'false';
@@ -320,6 +325,7 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
320
325
  sharedTSCompilationState?.dispose();
321
326
  void stylesheetBundler.dispose();
322
327
  void compilation.close?.();
328
+ void cacheStore?.close();
323
329
  });
324
330
  /**
325
331
  * Checks if the file has side-effects when `advancedOptimizations` is enabled.
@@ -143,7 +143,7 @@ function createCommonJSModuleError(request, importer) {
143
143
  notes: [
144
144
  {
145
145
  text: 'CommonJS or AMD dependencies can cause optimization bailouts.\n' +
146
- 'For more information see: https://angular.io/guide/build#configuring-commonjs-dependencies',
146
+ 'For more information see: https://angular.dev/tools/cli/build#configuring-commonjs-dependencies',
147
147
  },
148
148
  ],
149
149
  };
@@ -22,7 +22,12 @@ function createExternalPackagesPlugin(options) {
22
22
  return {
23
23
  name: 'angular-external-packages',
24
24
  setup(build) {
25
- const loaderOptionKeys = build.initialOptions.loader && Object.keys(build.initialOptions.loader);
25
+ // Find all loader keys that are not using the 'file' loader.
26
+ // The 'file' loader is automatically handled by Vite and does not need exclusion.
27
+ const loaderOptionKeys = build.initialOptions.loader &&
28
+ Object.entries(build.initialOptions.loader)
29
+ .filter(([, value]) => value !== 'file')
30
+ .map(([key]) => key);
26
31
  // Safe to use native packages external option if no loader options or exclusions present
27
32
  if (!exclusions && !loaderOptionKeys?.length) {
28
33
  build.initialOptions.packages = 'external';
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.io/license
7
+ */
8
+ import { Cache, CacheStore } from './cache';
9
+ export declare class LmbdCacheStore implements CacheStore<unknown> {
10
+ #private;
11
+ readonly cachePath: string;
12
+ constructor(cachePath: string);
13
+ get(key: string): Promise<any>;
14
+ has(key: string): boolean;
15
+ set(key: string, value: unknown): Promise<this>;
16
+ createCache<V = unknown>(namespace: string): Cache<V>;
17
+ close(): Promise<void>;
18
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.io/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LmbdCacheStore = void 0;
11
+ const lmdb_1 = require("lmdb");
12
+ const cache_1 = require("./cache");
13
+ class LmbdCacheStore {
14
+ cachePath;
15
+ #cacheFileUrl;
16
+ #db;
17
+ constructor(cachePath) {
18
+ this.cachePath = cachePath;
19
+ this.#cacheFileUrl = cachePath;
20
+ }
21
+ #ensureCacheFile() {
22
+ this.#db ??= (0, lmdb_1.open)({
23
+ path: this.#cacheFileUrl,
24
+ compression: true,
25
+ });
26
+ return this.#db;
27
+ }
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ async get(key) {
30
+ const db = this.#ensureCacheFile();
31
+ const value = db.get(key);
32
+ return value;
33
+ }
34
+ has(key) {
35
+ return this.#ensureCacheFile().doesExist(key);
36
+ }
37
+ async set(key, value) {
38
+ const db = this.#ensureCacheFile();
39
+ await db.put(key, value);
40
+ return this;
41
+ }
42
+ createCache(namespace) {
43
+ return new cache_1.Cache(this, namespace);
44
+ }
45
+ async close() {
46
+ try {
47
+ await this.#db?.close();
48
+ }
49
+ catch {
50
+ // Failure to close should not be fatal
51
+ }
52
+ }
53
+ }
54
+ exports.LmbdCacheStore = LmbdCacheStore;
@@ -45,7 +45,7 @@ function createCssInlineFontsPlugin({ cache, cacheOptions, }) {
45
45
  errors: [
46
46
  {
47
47
  text: `Failed to inline external stylesheet '${args.path}'.`,
48
- detail: error,
48
+ notes: error instanceof Error ? [{ text: error.toString() }] : undefined,
49
49
  },
50
50
  ],
51
51
  };
@@ -41,7 +41,7 @@ function isSassException(error) {
41
41
  }
42
42
  function shutdownSassWorkerPool() {
43
43
  if (sassWorkerPool) {
44
- sassWorkerPool.close();
44
+ void sassWorkerPool.close();
45
45
  sassWorkerPool = undefined;
46
46
  }
47
47
  else if (sassWorkerPoolPromise) {
@@ -15,6 +15,7 @@ const magic_string_1 = __importDefault(require("magic-string"));
15
15
  const node_fs_1 = require("node:fs");
16
16
  const node_path_1 = require("node:path");
17
17
  const node_url_1 = require("node:url");
18
+ const error_1 = require("../../utils/error");
18
19
  const lexer_1 = require("./lexer");
19
20
  /**
20
21
  * Ensures that a bare specifier URL path that is intended to be treated as
@@ -70,7 +71,11 @@ class UrlRebasingImporter {
70
71
  continue;
71
72
  }
72
73
  // Skip if root-relative, absolute or protocol relative url
73
- if (/^((?:\w+:)?\/\/|data:|chrome:|#|\/)/.test(value)) {
74
+ if (/^((?:\w+:)?\/\/|data:|chrome:|\/)/.test(value)) {
75
+ continue;
76
+ }
77
+ // Skip if a fragment identifier but not a Sass interpolation
78
+ if (value[0] === '#' && value[1] !== '{') {
74
79
  continue;
75
80
  }
76
81
  // Sass variable usage either starts with a `$` or contains a namespace and a `.$`
@@ -195,8 +200,15 @@ class RelativeUrlRebasingImporter extends UrlRebasingImporter {
195
200
  try {
196
201
  entries = (0, node_fs_1.readdirSync)(directory, { withFileTypes: true });
197
202
  }
198
- catch {
199
- return null;
203
+ catch (error) {
204
+ (0, error_1.assertIsError)(error);
205
+ // If the containing directory does not exist return null to indicate it cannot be resolved
206
+ if (error.code === 'ENOENT') {
207
+ return null;
208
+ }
209
+ throw new Error(`Error reading directory ["${directory}"] while resolving Sass import`, {
210
+ cause: error,
211
+ });
200
212
  }
201
213
  foundDefaults = [];
202
214
  foundImports = [];
@@ -205,7 +217,7 @@ class RelativeUrlRebasingImporter extends UrlRebasingImporter {
205
217
  let isDirectory;
206
218
  let isFile;
207
219
  if (entry.isSymbolicLink()) {
208
- const stats = (0, node_fs_1.statSync)((0, node_path_1.join)(entry.path, entry.name));
220
+ const stats = (0, node_fs_1.statSync)((0, node_path_1.join)(directory, entry.name));
209
221
  isDirectory = stats.isDirectory();
210
222
  isFile = stats.isFile();
211
223
  }
@@ -5,7 +5,7 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.io/license
7
7
  */
8
- import { CompileResult, Deprecation, SourceSpan, StringOptions } from 'sass';
8
+ import type { CompileResult, Deprecation, SourceSpan, StringOptions } from 'sass';
9
9
  export interface SerializableVersion {
10
10
  major: number;
11
11
  minor: number;
@@ -36,14 +36,10 @@ export type SerializableWarningMessage = ({
36
36
  * the worker which can be up to two times faster than the asynchronous variant.
37
37
  */
38
38
  export declare class SassWorkerImplementation {
39
- private rebase;
40
- private readonly workers;
41
- private readonly availableWorkers;
42
- private readonly requests;
43
- private readonly workerPath;
44
- private idCounter;
45
- private nextWorkerIndex;
46
- constructor(rebase?: boolean);
39
+ #private;
40
+ private readonly rebase;
41
+ readonly maxThreads: number;
42
+ constructor(rebase?: boolean, maxThreads?: number);
47
43
  /**
48
44
  * Provides information about the Sass implementation.
49
45
  * This mimics enough of the `dart-sass` value to be used with the `sass-loader`.
@@ -63,10 +59,9 @@ export declare class SassWorkerImplementation {
63
59
  /**
64
60
  * Shutdown the Sass render worker.
65
61
  * Executing this method will stop any pending render requests.
62
+ * @returns A void promise that resolves when closing is complete.
66
63
  */
67
- close(): void;
68
- private createWorker;
64
+ close(): Promise<void>;
69
65
  private processImporters;
70
- private createRequest;
71
66
  private isFileImporter;
72
67
  }
@@ -6,12 +6,64 @@
6
6
  * Use of this source code is governed by an MIT-style license that can be
7
7
  * found in the LICENSE file at https://angular.io/license
8
8
  */
9
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
10
+ if (value !== null && value !== void 0) {
11
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
12
+ var dispose;
13
+ if (async) {
14
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
15
+ dispose = value[Symbol.asyncDispose];
16
+ }
17
+ if (dispose === void 0) {
18
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
19
+ dispose = value[Symbol.dispose];
20
+ }
21
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
22
+ env.stack.push({ value: value, dispose: dispose, async: async });
23
+ }
24
+ else if (async) {
25
+ env.stack.push({ async: true });
26
+ }
27
+ return value;
28
+ };
29
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
30
+ return function (env) {
31
+ function fail(e) {
32
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
33
+ env.hasError = true;
34
+ }
35
+ function next() {
36
+ while (env.stack.length) {
37
+ var rec = env.stack.pop();
38
+ try {
39
+ var result = rec.dispose && rec.dispose.call(rec.value);
40
+ if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
41
+ }
42
+ catch (e) {
43
+ fail(e);
44
+ }
45
+ }
46
+ if (env.hasError) throw env.error;
47
+ }
48
+ return next();
49
+ };
50
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
51
+ var e = new Error(message);
52
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
+ });
54
+ var __importDefault = (this && this.__importDefault) || function (mod) {
55
+ return (mod && mod.__esModule) ? mod : { "default": mod };
56
+ };
9
57
  Object.defineProperty(exports, "__esModule", { value: true });
10
58
  exports.SassWorkerImplementation = void 0;
11
- const node_path_1 = require("node:path");
59
+ const node_assert_1 = __importDefault(require("node:assert"));
12
60
  const node_url_1 = require("node:url");
13
61
  const node_worker_threads_1 = require("node:worker_threads");
62
+ const piscina_1 = require("piscina");
14
63
  const environment_options_1 = require("../../utils/environment-options");
64
+ // Polyfill Symbol.dispose if not present
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ Symbol.dispose ??= Symbol('Symbol Dispose');
15
67
  /**
16
68
  * The maximum number of Workers that will be created to execute render requests.
17
69
  */
@@ -24,14 +76,22 @@ const MAX_RENDER_WORKERS = environment_options_1.maxWorkers;
24
76
  */
25
77
  class SassWorkerImplementation {
26
78
  rebase;
27
- workers = [];
28
- availableWorkers = [];
29
- requests = new Map();
30
- workerPath = (0, node_path_1.join)(__dirname, './worker.js');
31
- idCounter = 1;
32
- nextWorkerIndex = 0;
33
- constructor(rebase = false) {
79
+ maxThreads;
80
+ #workerPool;
81
+ constructor(rebase = false, maxThreads = MAX_RENDER_WORKERS) {
34
82
  this.rebase = rebase;
83
+ this.maxThreads = maxThreads;
84
+ }
85
+ #ensureWorkerPool() {
86
+ this.#workerPool ??= new piscina_1.Piscina({
87
+ filename: require.resolve('./worker'),
88
+ minThreads: 1,
89
+ maxThreads: this.maxThreads,
90
+ // Shutdown idle threads after 1 second of inactivity
91
+ idleTimeout: 1000,
92
+ recordTiming: false,
93
+ });
94
+ return this.#workerPool;
35
95
  }
36
96
  /**
37
97
  * Provides information about the Sass implementation.
@@ -52,49 +112,20 @@ class SassWorkerImplementation {
52
112
  * @param source The contents to compile.
53
113
  * @param options The `dart-sass` options to use when rendering the stylesheet.
54
114
  */
55
- compileStringAsync(source, options) {
56
- // The `functions`, `logger` and `importer` options are JavaScript functions that cannot be transferred.
57
- // If any additional function options are added in the future, they must be excluded as well.
58
- const { functions, importers, url, logger, ...serializableOptions } = options;
59
- // The CLI's configuration does not use or expose the ability to defined custom Sass functions
60
- if (functions && Object.keys(functions).length > 0) {
61
- throw new Error('Sass custom functions are not supported.');
62
- }
63
- return new Promise((resolve, reject) => {
64
- let workerIndex = this.availableWorkers.pop();
65
- if (workerIndex === undefined) {
66
- if (this.workers.length < MAX_RENDER_WORKERS) {
67
- workerIndex = this.workers.length;
68
- this.workers.push(this.createWorker());
69
- }
70
- else {
71
- workerIndex = this.nextWorkerIndex++;
72
- if (this.nextWorkerIndex >= this.workers.length) {
73
- this.nextWorkerIndex = 0;
74
- }
75
- }
115
+ async compileStringAsync(source, options) {
116
+ const env_1 = { stack: [], error: void 0, hasError: false };
117
+ try {
118
+ // The `functions`, `logger` and `importer` options are JavaScript functions that cannot be transferred.
119
+ // If any additional function options are added in the future, they must be excluded as well.
120
+ const { functions, importers, url, logger, ...serializableOptions } = options;
121
+ // The CLI's configuration does not use or expose the ability to define custom Sass functions
122
+ if (functions && Object.keys(functions).length > 0) {
123
+ throw new Error('Sass custom functions are not supported.');
76
124
  }
77
- const callback = (error, result) => {
78
- if (error) {
79
- const url = error.span?.url;
80
- if (url) {
81
- error.span.url = (0, node_url_1.pathToFileURL)(url);
82
- }
83
- reject(error);
84
- return;
85
- }
86
- if (!result) {
87
- reject(new Error('No result.'));
88
- return;
89
- }
90
- resolve(result);
91
- };
92
- const request = this.createRequest(workerIndex, callback, logger, importers);
93
- this.requests.set(request.id, request);
94
- this.workers[workerIndex].postMessage({
95
- id: request.id,
125
+ const importerChannel = __addDisposableResource(env_1, importers?.length ? this.#createImporterChannel(importers) : undefined, false);
126
+ const response = (await this.#ensureWorkerPool().run({
96
127
  source,
97
- hasImporter: !!importers?.length,
128
+ importerChannel,
98
129
  hasLogger: !!logger,
99
130
  rebase: this.rebase,
100
131
  options: {
@@ -102,39 +133,13 @@ class SassWorkerImplementation {
102
133
  // URL is not serializable so to convert to string here and back to URL in the worker.
103
134
  url: url ? (0, node_url_1.fileURLToPath)(url) : undefined,
104
135
  },
105
- });
106
- });
107
- }
108
- /**
109
- * Shutdown the Sass render worker.
110
- * Executing this method will stop any pending render requests.
111
- */
112
- close() {
113
- for (const worker of this.workers) {
114
- try {
115
- void worker.terminate();
116
- }
117
- catch { }
118
- }
119
- this.requests.clear();
120
- }
121
- createWorker() {
122
- const { port1: mainImporterPort, port2: workerImporterPort } = new node_worker_threads_1.MessageChannel();
123
- const importerSignal = new Int32Array(new SharedArrayBuffer(4));
124
- const worker = new node_worker_threads_1.Worker(this.workerPath, {
125
- workerData: { workerImporterPort, importerSignal },
126
- transferList: [workerImporterPort],
127
- });
128
- worker.on('message', (response) => {
129
- const request = this.requests.get(response.id);
130
- if (!request) {
131
- return;
132
- }
133
- this.requests.delete(response.id);
134
- this.availableWorkers.push(request.workerIndex);
135
- if (response.warnings && request.logger?.warn) {
136
- for (const { message, span, ...options } of response.warnings) {
137
- request.logger.warn(message, {
136
+ }, {
137
+ transferList: importerChannel ? [importerChannel.port] : undefined,
138
+ }));
139
+ const { result, error, warnings } = response;
140
+ if (warnings && logger?.warn) {
141
+ for (const { message, span, ...options } of warnings) {
142
+ logger.warn(message, {
138
143
  ...options,
139
144
  span: span && {
140
145
  ...span,
@@ -143,26 +148,49 @@ class SassWorkerImplementation {
143
148
  });
144
149
  }
145
150
  }
146
- if (response.result) {
147
- request.callback(undefined, {
148
- ...response.result,
149
- // URL is not serializable so in the worker we convert to string and here back to URL.
150
- loadedUrls: response.result.loadedUrls.map((p) => (0, node_url_1.pathToFileURL)(p)),
151
- });
151
+ if (error) {
152
+ // Convert stringified url value required for cloning back to a URL object
153
+ const url = error.span?.url;
154
+ if (url) {
155
+ error.span.url = (0, node_url_1.pathToFileURL)(url);
156
+ }
157
+ throw error;
152
158
  }
153
- else {
154
- request.callback(response.error);
159
+ (0, node_assert_1.default)(result, 'Sass render worker should always return a result or an error');
160
+ return {
161
+ ...result,
162
+ // URL is not serializable so in the worker we convert to string and here back to URL.
163
+ loadedUrls: result.loadedUrls.map((p) => (0, node_url_1.pathToFileURL)(p)),
164
+ };
165
+ }
166
+ catch (e_1) {
167
+ env_1.error = e_1;
168
+ env_1.hasError = true;
169
+ }
170
+ finally {
171
+ __disposeResources(env_1);
172
+ }
173
+ }
174
+ /**
175
+ * Shutdown the Sass render worker.
176
+ * Executing this method will stop any pending render requests.
177
+ * @returns A void promise that resolves when closing is complete.
178
+ */
179
+ async close() {
180
+ if (this.#workerPool) {
181
+ try {
182
+ await this.#workerPool.destroy();
155
183
  }
156
- });
157
- mainImporterPort.on('message', ({ id, url, options }) => {
158
- const request = this.requests.get(id);
159
- if (!request?.importers) {
160
- mainImporterPort.postMessage(null);
161
- Atomics.store(importerSignal, 0, 1);
162
- Atomics.notify(importerSignal, 0);
163
- return;
184
+ finally {
185
+ this.#workerPool = undefined;
164
186
  }
165
- this.processImporters(request.importers, url, {
187
+ }
188
+ }
189
+ #createImporterChannel(importers) {
190
+ const { port1: mainImporterPort, port2: workerImporterPort } = new node_worker_threads_1.MessageChannel();
191
+ const importerSignal = new Int32Array(new SharedArrayBuffer(4));
192
+ mainImporterPort.on('message', ({ url, options }) => {
193
+ this.processImporters(importers, url, {
166
194
  ...options,
167
195
  // URL is not serializable so in the worker we convert to string and here back to URL.
168
196
  containingUrl: options.containingUrl
@@ -181,7 +209,13 @@ class SassWorkerImplementation {
181
209
  });
182
210
  });
183
211
  mainImporterPort.unref();
184
- return worker;
212
+ return {
213
+ port: workerImporterPort,
214
+ signal: importerSignal,
215
+ [Symbol.dispose]() {
216
+ mainImporterPort.close();
217
+ },
218
+ };
185
219
  }
186
220
  async processImporters(importers, url, options) {
187
221
  for (const importer of importers) {
@@ -197,15 +231,6 @@ class SassWorkerImplementation {
197
231
  }
198
232
  return null;
199
233
  }
200
- createRequest(workerIndex, callback, logger, importers) {
201
- return {
202
- id: this.idCounter++,
203
- workerIndex,
204
- callback,
205
- logger,
206
- importers,
207
- };
208
- }
209
234
  isFileImporter(value) {
210
235
  return 'findFileUrl' in value;
211
236
  }
@@ -5,4 +5,80 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.io/license
7
7
  */
8
+ /// <reference types="node" />
9
+ /// <reference types="source-map-js/source-map" />
10
+ import { MessagePort } from 'node:worker_threads';
11
+ import { SourceSpan, StringOptions } from 'sass';
12
+ import type { SerializableWarningMessage } from './sass-service';
13
+ /**
14
+ * A request to render a Sass stylesheet using the supplied options.
15
+ */
16
+ interface RenderRequestMessage {
17
+ /**
18
+ * The contents to compile.
19
+ */
20
+ source: string;
21
+ /**
22
+ * The Sass options to provide to the `dart-sass` compile function.
23
+ */
24
+ options: Omit<StringOptions<'sync'>, 'url'> & {
25
+ url: string;
26
+ };
27
+ /**
28
+ * Indicates the request has a custom importer function on the main thread.
29
+ */
30
+ importerChannel?: {
31
+ port: MessagePort;
32
+ signal: Int32Array;
33
+ };
34
+ /**
35
+ * Indicates the request has a custom logger for warning messages.
36
+ */
37
+ hasLogger: boolean;
38
+ /**
39
+ * Indicates paths within url() CSS functions should be rebased.
40
+ */
41
+ rebase: boolean;
42
+ }
43
+ export default function renderSassStylesheet(request: RenderRequestMessage): Promise<{
44
+ warnings: SerializableWarningMessage[] | undefined;
45
+ result: {
46
+ loadedUrls: string[];
47
+ css: string;
48
+ sourceMap?: import("source-map-js").RawSourceMap | undefined;
49
+ };
50
+ error?: undefined;
51
+ } | {
52
+ warnings: SerializableWarningMessage[] | undefined;
53
+ error: {
54
+ span: Omit<SourceSpan, "url"> & {
55
+ url?: string | undefined;
56
+ };
57
+ message: string;
58
+ stack: string | undefined;
59
+ sassMessage: string;
60
+ sassStack: string;
61
+ };
62
+ result?: undefined;
63
+ } | {
64
+ warnings: SerializableWarningMessage[] | undefined;
65
+ error: {
66
+ message: string;
67
+ stack: string | undefined;
68
+ span?: undefined;
69
+ sassMessage?: undefined;
70
+ sassStack?: undefined;
71
+ };
72
+ result?: undefined;
73
+ } | {
74
+ warnings: SerializableWarningMessage[] | undefined;
75
+ error: {
76
+ message: string;
77
+ span?: undefined;
78
+ stack?: undefined;
79
+ sassMessage?: undefined;
80
+ sassStack?: undefined;
81
+ };
82
+ result?: undefined;
83
+ }>;
8
84
  export {};
@@ -16,22 +16,14 @@ const node_url_1 = require("node:url");
16
16
  const node_worker_threads_1 = require("node:worker_threads");
17
17
  const sass_1 = require("sass");
18
18
  const rebasing_importer_1 = require("./rebasing-importer");
19
- if (!node_worker_threads_1.parentPort || !node_worker_threads_1.workerData) {
20
- throw new Error('Sass worker must be executed as a Worker.');
21
- }
22
- // The importer variables are used to proxy import requests to the main thread
23
- const { workerImporterPort, importerSignal } = node_worker_threads_1.workerData;
24
- node_worker_threads_1.parentPort.on('message', (message) => {
25
- if (!node_worker_threads_1.parentPort) {
26
- throw new Error('"parentPort" is not defined. Sass worker must be executed as a Worker.');
27
- }
28
- const { id, hasImporter, hasLogger, source, options, rebase } = message;
19
+ async function renderSassStylesheet(request) {
20
+ const { importerChannel, hasLogger, source, options, rebase } = request;
29
21
  const entryDirectory = (0, node_path_1.dirname)(options.url);
30
22
  let warnings;
31
23
  try {
32
24
  const directoryCache = new Map();
33
25
  const rebaseSourceMaps = options.sourceMap ? new Map() : undefined;
34
- if (hasImporter) {
26
+ if (importerChannel) {
35
27
  // When a custom importer function is present, the importer request must be proxied
36
28
  // back to the main thread where it can be executed.
37
29
  // This process must be synchronous from the perspective of dart-sass. The `Atomics`
@@ -39,17 +31,16 @@ node_worker_threads_1.parentPort.on('message', (message) => {
39
31
  // `receiveMessageOnPort` function are used to ensure synchronous behavior.
40
32
  const proxyImporter = {
41
33
  findFileUrl: (url, { fromImport, containingUrl }) => {
42
- Atomics.store(importerSignal, 0, 0);
43
- workerImporterPort.postMessage({
44
- id,
34
+ Atomics.store(importerChannel.signal, 0, 0);
35
+ importerChannel.port.postMessage({
45
36
  url,
46
37
  options: {
47
38
  fromImport,
48
39
  containingUrl: containingUrl ? (0, node_url_1.fileURLToPath)(containingUrl) : null,
49
40
  },
50
41
  });
51
- Atomics.wait(importerSignal, 0, 0);
52
- const result = (0, node_worker_threads_1.receiveMessageOnPort)(workerImporterPort)?.message;
42
+ Atomics.wait(importerChannel.signal, 0, 0);
43
+ const result = (0, node_worker_threads_1.receiveMessageOnPort)(importerChannel.port)?.message;
53
44
  return result ? (0, node_url_1.pathToFileURL)(result) : null;
54
45
  },
55
46
  };
@@ -99,22 +90,20 @@ node_worker_threads_1.parentPort.on('message', (message) => {
99
90
  // is referencing its original self.
100
91
  (file, context) => (file !== context.importer ? rebaseSourceMaps.get(file) : null));
101
92
  }
102
- node_worker_threads_1.parentPort.postMessage({
103
- id,
93
+ return {
104
94
  warnings,
105
95
  result: {
106
96
  ...result,
107
97
  // URL is not serializable so to convert to string here and back to URL in the parent.
108
98
  loadedUrls: result.loadedUrls.map((p) => (0, node_url_1.fileURLToPath)(p)),
109
99
  },
110
- });
100
+ };
111
101
  }
112
102
  catch (error) {
113
103
  // Needed because V8 will only serialize the message and stack properties of an Error instance.
114
104
  if (error instanceof sass_1.Exception) {
115
105
  const { span, message, stack, sassMessage, sassStack } = error;
116
- node_worker_threads_1.parentPort.postMessage({
117
- id,
106
+ return {
118
107
  warnings,
119
108
  error: {
120
109
  span: convertSourceSpan(span),
@@ -123,21 +112,21 @@ node_worker_threads_1.parentPort.on('message', (message) => {
123
112
  sassMessage,
124
113
  sassStack,
125
114
  },
126
- });
115
+ };
127
116
  }
128
117
  else if (error instanceof Error) {
129
118
  const { message, stack } = error;
130
- node_worker_threads_1.parentPort.postMessage({ id, warnings, error: { message, stack } });
119
+ return { warnings, error: { message, stack } };
131
120
  }
132
121
  else {
133
- node_worker_threads_1.parentPort.postMessage({
134
- id,
122
+ return {
135
123
  warnings,
136
124
  error: { message: 'An unknown error has occurred.' },
137
- });
125
+ };
138
126
  }
139
127
  }
140
- });
128
+ }
129
+ exports.default = renderSassStylesheet;
141
130
  /**
142
131
  * Converts a Sass SourceSpan object into a serializable form.
143
132
  * The SourceSpan object contains a URL property which must be converted into a string.
@@ -13,8 +13,8 @@ function formatSize(size) {
13
13
  return '0 bytes';
14
14
  }
15
15
  const abbreviations = ['bytes', 'kB', 'MB', 'GB'];
16
- const index = Math.floor(Math.log(size) / Math.log(1024));
17
- const roundedSize = size / Math.pow(1024, index);
16
+ const index = Math.floor(Math.log(size) / Math.log(1000));
17
+ const roundedSize = size / Math.pow(1000, index);
18
18
  // bytes don't have a fraction
19
19
  const fractionDigits = index === 0 ? 0 : 2;
20
20
  return `${roundedSize.toFixed(fractionDigits)} ${abbreviations[index]}`;
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.normalizeCacheOptions = void 0;
11
11
  const node_path_1 = require("node:path");
12
12
  /** Version placeholder is replaced during the build process with actual package version */
13
- const VERSION = '18.0.0-rc.1';
13
+ const VERSION = '18.0.0-rc.3';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&
@@ -36,7 +36,8 @@ function patchFetchToLoadInMemoryAssets() {
36
36
  else {
37
37
  return originalFetch(input, init);
38
38
  }
39
- const { pathname, protocol } = url;
39
+ const { protocol } = url;
40
+ const pathname = decodeURIComponent(url.pathname);
40
41
  if (protocol !== RESOLVE_PROTOCOL || !assetFiles[pathname]) {
41
42
  // Only handle relative requests or files that are in assets.
42
43
  return originalFetch(input, init);
@@ -58,7 +58,7 @@ function assertCompatibleAngularVersion(projectRoot) {
58
58
  if (!(0, semver_1.satisfies)(angularVersion, supportedAngularSemver, { includePrerelease: true })) {
59
59
  console.error(`This version of CLI is only compatible with Angular versions ${supportedAngularSemver},\n` +
60
60
  `but Angular version ${angularVersion} was found instead.\n` +
61
- 'Please visit the link below to find instructions on how to update Angular.\nhttps://update.angular.io/');
61
+ 'Please visit the link below to find instructions on how to update Angular.\nhttps://update.angular.dev/');
62
62
  process.exit(3);
63
63
  }
64
64
  }
@@ -1,12 +0,0 @@
1
- /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.io/license
7
- */
8
- /**
9
- * Finds the `ngCspNonce` value and copies it to all inline `<style>` and `<script> `tags.
10
- * @param html Markup that should be processed.
11
- */
12
- export declare function addNonce(html: string): Promise<string>;
@@ -1,55 +0,0 @@
1
- "use strict";
2
- /**
3
- * @license
4
- * Copyright Google LLC All Rights Reserved.
5
- *
6
- * Use of this source code is governed by an MIT-style license that can be
7
- * found in the LICENSE file at https://angular.io/license
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.addNonce = void 0;
11
- const html_rewriting_stream_1 = require("./html-rewriting-stream");
12
- /**
13
- * Pattern matching the name of the Angular nonce attribute. Note that this is
14
- * case-insensitive, because HTML attribute names are case-insensitive as well.
15
- */
16
- const NONCE_ATTR_PATTERN = /ngCspNonce/i;
17
- /**
18
- * Finds the `ngCspNonce` value and copies it to all inline `<style>` and `<script> `tags.
19
- * @param html Markup that should be processed.
20
- */
21
- async function addNonce(html) {
22
- const nonce = await findNonce(html);
23
- if (!nonce) {
24
- return html;
25
- }
26
- const { rewriter, transformedContent } = await (0, html_rewriting_stream_1.htmlRewritingStream)(html);
27
- rewriter.on('startTag', (tag) => {
28
- if ((tag.tagName === 'style' ||
29
- (tag.tagName === 'script' && !tag.attrs.some((attr) => attr.name === 'src'))) &&
30
- !tag.attrs.some((attr) => attr.name === 'nonce')) {
31
- tag.attrs.push({ name: 'nonce', value: nonce });
32
- }
33
- rewriter.emitStartTag(tag);
34
- });
35
- return transformedContent();
36
- }
37
- exports.addNonce = addNonce;
38
- /** Finds the Angular nonce in an HTML string. */
39
- async function findNonce(html) {
40
- // Inexpensive check to avoid parsing the HTML when we're sure there's no nonce.
41
- if (!NONCE_ATTR_PATTERN.test(html)) {
42
- return null;
43
- }
44
- const { rewriter, transformedContent } = await (0, html_rewriting_stream_1.htmlRewritingStream)(html);
45
- let nonce = null;
46
- rewriter.on('startTag', (tag) => {
47
- const nonceAttr = tag.attrs.find((attr) => NONCE_ATTR_PATTERN.test(attr.name));
48
- if (nonceAttr?.value) {
49
- nonce = nonceAttr.value;
50
- rewriter.stop(); // Stop parsing since we've found the nonce.
51
- }
52
- });
53
- await transformedContent();
54
- return nonce;
55
- }