@angular-devkit/build-angular 17.0.0-rc.2 → 17.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 +9 -8
- package/src/builders/app-shell/render-worker.d.ts +1 -1
- package/src/builders/app-shell/render-worker.js +16 -9
- package/src/builders/application/build-action.js +5 -2
- package/src/builders/application/execute-build.js +26 -16
- package/src/builders/dev-server/vite-server.js +40 -14
- package/src/builders/extract-i18n/application-extraction.js +1 -0
- package/src/builders/prerender/routes-extractor-worker.js +1 -1
- package/src/tools/babel/plugins/elide-angular-metadata.js +14 -2
- package/src/tools/esbuild/angular/compiler-plugin.js +2 -1
- package/src/tools/esbuild/application-code-bundle.d.ts +3 -2
- package/src/tools/esbuild/application-code-bundle.js +6 -3
- package/src/tools/esbuild/bundler-execution-result.d.ts +2 -0
- package/src/tools/esbuild/bundler-execution-result.js +14 -0
- package/src/tools/esbuild/commonjs-checker.js +3 -3
- package/src/tools/esbuild/javascript-transformer.d.ts +1 -0
- package/src/tools/esbuild/javascript-transformer.js +25 -13
- package/src/tools/esbuild/utils.d.ts +1 -1
- package/src/tools/esbuild/utils.js +18 -4
- package/src/utils/environment-options.d.ts +1 -0
- package/src/utils/environment-options.js +3 -1
- package/src/utils/routes-extractor/extractor.d.ts +1 -1
- package/src/utils/routes-extractor/extractor.js +2 -2
- package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js +3 -2
- package/src/utils/server-rendering/fetch-patch.d.ts +8 -0
- package/src/utils/server-rendering/fetch-patch.js +66 -0
- package/src/utils/server-rendering/prerender.js +25 -30
- package/src/utils/server-rendering/render-worker.d.ts +4 -2
- package/src/utils/server-rendering/render-worker.js +8 -4
- package/src/utils/server-rendering/routes-extractor-worker.d.ts +5 -3
- package/src/utils/server-rendering/routes-extractor-worker.js +10 -4
- package/src/utils/server-rendering/prerender-server.d.ts +0 -21
- package/src/utils/server-rendering/prerender-server.js +0 -102
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-devkit/build-angular",
|
|
3
|
-
"version": "17.0.0-rc.
|
|
3
|
+
"version": "17.0.0-rc.3",
|
|
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.0-rc.
|
|
11
|
-
"@angular-devkit/build-webpack": "0.1700.0-rc.
|
|
12
|
-
"@angular-devkit/core": "17.0.0-rc.
|
|
10
|
+
"@angular-devkit/architect": "0.1700.0-rc.3",
|
|
11
|
+
"@angular-devkit/build-webpack": "0.1700.0-rc.3",
|
|
12
|
+
"@angular-devkit/core": "17.0.0-rc.3",
|
|
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.0-rc.
|
|
23
|
+
"@ngtools/webpack": "17.0.0-rc.3",
|
|
24
24
|
"@vitejs/plugin-basic-ssl": "1.0.1",
|
|
25
25
|
"ansi-colors": "4.1.3",
|
|
26
26
|
"autoprefixer": "10.4.16",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"open": "8.4.2",
|
|
50
50
|
"ora": "5.4.1",
|
|
51
51
|
"parse5-html-rewriting-stream": "7.0.0",
|
|
52
|
-
"picomatch": "
|
|
52
|
+
"picomatch": "3.0.1",
|
|
53
53
|
"piscina": "4.1.0",
|
|
54
54
|
"postcss": "8.4.31",
|
|
55
55
|
"postcss-loader": "7.3.3",
|
|
@@ -60,10 +60,11 @@
|
|
|
60
60
|
"semver": "7.5.4",
|
|
61
61
|
"source-map-loader": "4.0.1",
|
|
62
62
|
"source-map-support": "0.5.21",
|
|
63
|
-
"terser": "5.
|
|
63
|
+
"terser": "5.24.0",
|
|
64
64
|
"text-table": "0.2.0",
|
|
65
65
|
"tree-kill": "1.2.2",
|
|
66
66
|
"tslib": "2.6.2",
|
|
67
|
+
"undici": "5.27.0",
|
|
67
68
|
"vite": "4.5.0",
|
|
68
69
|
"webpack": "5.89.0",
|
|
69
70
|
"webpack-dev-middleware": "6.1.1",
|
|
@@ -128,7 +129,7 @@
|
|
|
128
129
|
"url": "https://github.com/angular/angular-cli.git"
|
|
129
130
|
},
|
|
130
131
|
"engines": {
|
|
131
|
-
"node": "
|
|
132
|
+
"node": "^18.13.0 || >=20.9.0",
|
|
132
133
|
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
|
133
134
|
"yarn": ">= 1.13.0"
|
|
134
135
|
},
|
|
@@ -20,7 +20,7 @@ interface RenderRequest {
|
|
|
20
20
|
/**
|
|
21
21
|
* An optional URL path that represents the Angular route that should be rendered.
|
|
22
22
|
*/
|
|
23
|
-
url: string
|
|
23
|
+
url: string;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Renders an application based on a provided server bundle path, initial document, and optional URL route.
|
|
@@ -54,23 +54,30 @@ async function render({ serverBundlePath, document, url }) {
|
|
|
54
54
|
useValue: 'app-shell',
|
|
55
55
|
},
|
|
56
56
|
];
|
|
57
|
+
let renderAppPromise;
|
|
57
58
|
// Render platform server module
|
|
58
59
|
if (isBootstrapFn(bootstrapAppFn)) {
|
|
59
60
|
(0, node_assert_1.default)(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`);
|
|
60
|
-
|
|
61
|
+
renderAppPromise = renderApplication(bootstrapAppFn, {
|
|
61
62
|
document,
|
|
62
63
|
url,
|
|
63
64
|
platformProviders,
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
else {
|
|
68
|
+
(0, node_assert_1.default)(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);
|
|
69
|
+
const moduleClass = bootstrapAppFn || AppServerModule;
|
|
70
|
+
(0, node_assert_1.default)(moduleClass, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
|
|
71
|
+
renderAppPromise = renderModule(moduleClass, {
|
|
72
|
+
document,
|
|
73
|
+
url,
|
|
74
|
+
extraProviders: platformProviders,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// The below should really handled by the framework!!!.
|
|
78
|
+
let timer;
|
|
79
|
+
const renderingTimeout = new Promise((_, reject) => (timer = setTimeout(() => reject(new Error(`Page ${new URL(url, 'resolve://').pathname} did not render in 30 seconds.`)), 30000)));
|
|
80
|
+
return Promise.race([renderAppPromise, renderingTimeout]).finally(() => clearTimeout(timer));
|
|
74
81
|
}
|
|
75
82
|
function isBootstrapFn(value) {
|
|
76
83
|
// We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
|
|
@@ -38,6 +38,7 @@ const promises_1 = __importDefault(require("node:fs/promises"));
|
|
|
38
38
|
const node_path_1 = __importDefault(require("node:path"));
|
|
39
39
|
const sass_language_1 = require("../../tools/esbuild/stylesheets/sass-language");
|
|
40
40
|
const utils_1 = require("../../tools/esbuild/utils");
|
|
41
|
+
const environment_options_1 = require("../../utils/environment-options");
|
|
41
42
|
const error_1 = require("../../utils/error");
|
|
42
43
|
async function* runEsBuildBuildAction(action, options) {
|
|
43
44
|
const { writeToFileSystemFilter, writeToFileSystem = true, watch, poll, logger, deleteOutputPath, cacheOptions, outputPath, verbose, projectRoot, workspaceRoot, progress, } = options;
|
|
@@ -95,8 +96,10 @@ async function* runEsBuildBuildAction(action, options) {
|
|
|
95
96
|
});
|
|
96
97
|
// Setup abort support
|
|
97
98
|
options.signal?.addEventListener('abort', () => void watcher?.close());
|
|
98
|
-
//
|
|
99
|
-
|
|
99
|
+
// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
|
|
100
|
+
if (environment_options_1.shouldWatchRoot) {
|
|
101
|
+
watcher.add(projectRoot);
|
|
102
|
+
}
|
|
100
103
|
// Watch workspace for package manager changes
|
|
101
104
|
const packageWatchFiles = [
|
|
102
105
|
// manifest can affect module resolution
|
|
@@ -19,6 +19,7 @@ const global_styles_1 = require("../../tools/esbuild/global-styles");
|
|
|
19
19
|
const license_extractor_1 = require("../../tools/esbuild/license-extractor");
|
|
20
20
|
const utils_1 = require("../../tools/esbuild/utils");
|
|
21
21
|
const bundle_calculator_1 = require("../../utils/bundle-calculator");
|
|
22
|
+
const color_1 = require("../../utils/color");
|
|
22
23
|
const copy_assets_1 = require("../../utils/copy-assets");
|
|
23
24
|
const supported_browsers_1 = require("../../utils/supported-browsers");
|
|
24
25
|
const execute_post_bundle_1 = require("./execute-post-bundle");
|
|
@@ -112,13 +113,15 @@ async function executeBuild(options, context, rebuildState) {
|
|
|
112
113
|
if (options.budgets) {
|
|
113
114
|
const compatStats = (0, budget_stats_1.generateBudgetStats)(metafile, initialFiles);
|
|
114
115
|
budgetFailures = [...(0, bundle_calculator_1.checkBudgets)(options.budgets, compatStats, true)];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
if (budgetFailures.length > 0) {
|
|
117
|
+
await (0, utils_1.logMessages)(context, {
|
|
118
|
+
errors: budgetFailures
|
|
119
|
+
.filter((failure) => failure.severity === 'error')
|
|
120
|
+
.map((failure) => ({ text: failure.message, location: null })),
|
|
121
|
+
warnings: budgetFailures
|
|
122
|
+
.filter((failure) => failure.severity !== 'error')
|
|
123
|
+
.map((failure) => ({ text: failure.message, location: null })),
|
|
124
|
+
});
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
// Calculate estimated transfer size if scripts are optimized
|
|
@@ -146,11 +149,20 @@ async function executeBuild(options, context, rebuildState) {
|
|
|
146
149
|
executionResult.outputFiles.push(...result.additionalOutputFiles);
|
|
147
150
|
executionResult.assetFiles.push(...result.additionalAssets);
|
|
148
151
|
}
|
|
152
|
+
await printWarningsAndErrorsToConsole(context, warnings, errors);
|
|
149
153
|
if (prerenderOptions) {
|
|
150
154
|
executionResult.addOutputFile('prerendered-routes.json', JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2), bundler_context_1.BuildOutputFileType.Root);
|
|
155
|
+
let prerenderMsg = `Prerendered ${prerenderedRoutes.length} static route`;
|
|
156
|
+
if (prerenderedRoutes.length > 1) {
|
|
157
|
+
prerenderMsg += 's.';
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
prerenderMsg += '.';
|
|
161
|
+
}
|
|
162
|
+
context.logger.info(color_1.colors.magenta(prerenderMsg) + '\n');
|
|
151
163
|
}
|
|
152
|
-
|
|
153
|
-
(0, utils_1.logBuildStats)(context, metafile, initialFiles, budgetFailures, estimatedTransferSizes);
|
|
164
|
+
const changedFiles = rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputHashes);
|
|
165
|
+
(0, utils_1.logBuildStats)(context, metafile, initialFiles, budgetFailures, changedFiles, estimatedTransferSizes);
|
|
154
166
|
// Write metafile if stats option is enabled
|
|
155
167
|
if (options.stats) {
|
|
156
168
|
executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2), bundler_context_1.BuildOutputFileType.Root);
|
|
@@ -158,11 +170,9 @@ async function executeBuild(options, context, rebuildState) {
|
|
|
158
170
|
return executionResult;
|
|
159
171
|
}
|
|
160
172
|
exports.executeBuild = executeBuild;
|
|
161
|
-
function printWarningsAndErrorsToConsole(context, warnings, errors) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
context.logger.warn(warning);
|
|
167
|
-
}
|
|
173
|
+
async function printWarningsAndErrorsToConsole(context, warnings, errors) {
|
|
174
|
+
await (0, utils_1.logMessages)(context, {
|
|
175
|
+
errors: errors.map((text) => ({ text, location: null })),
|
|
176
|
+
warnings: warnings.map((text) => ({ text, location: null })),
|
|
177
|
+
});
|
|
168
178
|
}
|
|
@@ -37,6 +37,7 @@ exports.setupServer = exports.serveWithVite = void 0;
|
|
|
37
37
|
const remapping_1 = __importDefault(require("@ampproject/remapping"));
|
|
38
38
|
const mrmime_1 = require("mrmime");
|
|
39
39
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
41
|
const promises_1 = require("node:fs/promises");
|
|
41
42
|
const node_path_1 = __importDefault(require("node:path"));
|
|
42
43
|
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
|
|
@@ -157,7 +158,7 @@ async function* serveWithVite(serverOptions, builderName, context, plugins) {
|
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
160
|
if (server) {
|
|
160
|
-
handleUpdate(generatedFiles, server, serverOptions, context.logger);
|
|
161
|
+
handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger);
|
|
161
162
|
}
|
|
162
163
|
else {
|
|
163
164
|
const projectName = context.target?.project;
|
|
@@ -189,13 +190,13 @@ async function* serveWithVite(serverOptions, builderName, context, plugins) {
|
|
|
189
190
|
await new Promise((resolve) => (deferred = resolve));
|
|
190
191
|
}
|
|
191
192
|
exports.serveWithVite = serveWithVite;
|
|
192
|
-
function handleUpdate(generatedFiles, server, serverOptions, logger) {
|
|
193
|
+
function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logger) {
|
|
193
194
|
const updatedFiles = [];
|
|
194
195
|
// Invalidate any updated files
|
|
195
196
|
for (const [file, record] of generatedFiles) {
|
|
196
197
|
if (record.updated) {
|
|
197
198
|
updatedFiles.push(file);
|
|
198
|
-
const updatedModules = server.moduleGraph.getModulesByFile(file);
|
|
199
|
+
const updatedModules = server.moduleGraph.getModulesByFile(normalizePath(node_path_1.default.join(server.config.root, file)));
|
|
199
200
|
updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
|
|
200
201
|
}
|
|
201
202
|
}
|
|
@@ -207,8 +208,7 @@ function handleUpdate(generatedFiles, server, serverOptions, logger) {
|
|
|
207
208
|
const timestamp = Date.now();
|
|
208
209
|
server.ws.send({
|
|
209
210
|
type: 'update',
|
|
210
|
-
updates: updatedFiles.map((
|
|
211
|
-
const filePath = f.slice(1); // Remove leading slash.
|
|
211
|
+
updates: updatedFiles.map((filePath) => {
|
|
212
212
|
return {
|
|
213
213
|
type: 'css-update',
|
|
214
214
|
timestamp,
|
|
@@ -240,7 +240,7 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
|
|
|
240
240
|
filePath = '/index.html';
|
|
241
241
|
}
|
|
242
242
|
else {
|
|
243
|
-
filePath =
|
|
243
|
+
filePath = normalizePath(file.path);
|
|
244
244
|
}
|
|
245
245
|
seen.add(filePath);
|
|
246
246
|
// Skip analysis of sourcemaps
|
|
@@ -282,11 +282,13 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
|
|
|
282
282
|
const proxy = await (0, load_proxy_config_1.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig, true);
|
|
283
283
|
// dynamically import Vite for ESM compatibility
|
|
284
284
|
const { normalizePath } = await Promise.resolve().then(() => __importStar(require('vite')));
|
|
285
|
+
// Path will not exist on disk and only used to provide separate path for Vite requests
|
|
286
|
+
const virtualProjectRoot = normalizePath(node_path_1.default.join(serverOptions.workspaceRoot, `.angular/vite-root/${(0, node_crypto_1.randomUUID)()}/`));
|
|
285
287
|
const configuration = {
|
|
286
288
|
configFile: false,
|
|
287
289
|
envFile: false,
|
|
288
290
|
cacheDir: node_path_1.default.join(serverOptions.cacheOptions.path, 'vite'),
|
|
289
|
-
root:
|
|
291
|
+
root: virtualProjectRoot,
|
|
290
292
|
publicDir: false,
|
|
291
293
|
esbuild: false,
|
|
292
294
|
mode: 'development',
|
|
@@ -316,7 +318,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
|
|
|
316
318
|
},
|
|
317
319
|
ssr: {
|
|
318
320
|
// Exclude any provided dependencies (currently build defined externals)
|
|
319
|
-
external: externalMetadata.
|
|
321
|
+
external: externalMetadata.explicit,
|
|
320
322
|
},
|
|
321
323
|
plugins: [
|
|
322
324
|
(0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)(),
|
|
@@ -331,24 +333,31 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
|
|
|
331
333
|
// `/@id/${source}` but is currently closer to a raw external than a resolved file path.
|
|
332
334
|
return source;
|
|
333
335
|
}
|
|
334
|
-
if (importer && source.startsWith(
|
|
336
|
+
if (importer && source[0] === '.' && importer.startsWith(virtualProjectRoot)) {
|
|
335
337
|
// Remove query if present
|
|
336
338
|
const [importerFile] = importer.split('?', 1);
|
|
337
|
-
source = normalizePath(node_path_1.default.join(node_path_1.default.dirname(importerFile), source));
|
|
339
|
+
source = normalizePath(node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.relative(virtualProjectRoot, importerFile)), source));
|
|
340
|
+
}
|
|
341
|
+
if (source[0] === '/') {
|
|
342
|
+
source = source.slice(1);
|
|
338
343
|
}
|
|
339
344
|
const [file] = source.split('?', 1);
|
|
340
345
|
if (outputFiles.has(file)) {
|
|
341
|
-
return source;
|
|
346
|
+
return node_path_1.default.join(virtualProjectRoot, source);
|
|
342
347
|
}
|
|
343
348
|
},
|
|
344
349
|
load(id) {
|
|
345
350
|
const [file] = id.split('?', 1);
|
|
346
|
-
const
|
|
351
|
+
const relativeFile = normalizePath(node_path_1.default.relative(virtualProjectRoot, file));
|
|
352
|
+
const codeContents = outputFiles.get(relativeFile)?.contents;
|
|
347
353
|
if (codeContents === undefined) {
|
|
354
|
+
if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
|
|
355
|
+
return loadViteClientCode(file);
|
|
356
|
+
}
|
|
348
357
|
return;
|
|
349
358
|
}
|
|
350
359
|
const code = Buffer.from(codeContents).toString('utf-8');
|
|
351
|
-
const mapContents = outputFiles.get(
|
|
360
|
+
const mapContents = outputFiles.get(relativeFile + '.map')?.contents;
|
|
352
361
|
return {
|
|
353
362
|
// Remove source map URL comments from the code if a sourcemap is present.
|
|
354
363
|
// Vite will inline and add an additional sourcemap URL for the sourcemap.
|
|
@@ -422,7 +431,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
|
|
|
422
431
|
next();
|
|
423
432
|
return;
|
|
424
433
|
}
|
|
425
|
-
const rawHtml = outputFiles.get('
|
|
434
|
+
const rawHtml = outputFiles.get('index.server.html')?.contents;
|
|
426
435
|
if (!rawHtml) {
|
|
427
436
|
next();
|
|
428
437
|
return;
|
|
@@ -538,6 +547,23 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
|
|
|
538
547
|
return configuration;
|
|
539
548
|
}
|
|
540
549
|
exports.setupServer = setupServer;
|
|
550
|
+
/**
|
|
551
|
+
* Reads the resolved Vite client code from disk and updates the content to remove
|
|
552
|
+
* an unactionable suggestion to update the Vite configuration file to disable the
|
|
553
|
+
* error overlay. The Vite configuration file is not present when used in the Angular
|
|
554
|
+
* CLI.
|
|
555
|
+
* @param file The absolute path to the Vite client code.
|
|
556
|
+
* @returns
|
|
557
|
+
*/
|
|
558
|
+
async function loadViteClientCode(file) {
|
|
559
|
+
const originalContents = await (0, promises_1.readFile)(file, 'utf-8');
|
|
560
|
+
let contents = originalContents.replace('You can also disable this overlay by setting', '');
|
|
561
|
+
contents = contents.replace(
|
|
562
|
+
// eslint-disable-next-line max-len
|
|
563
|
+
'<code part="config-option-name">server.hmr.overlay</code> to <code part="config-option-value">false</code> in <code part="config-file-name">vite.config.js.</code>', '');
|
|
564
|
+
(0, node_assert_1.default)(originalContents !== contents, 'Failed to update Vite client error overlay text.');
|
|
565
|
+
return contents;
|
|
566
|
+
}
|
|
541
567
|
function pathnameWithoutServePath(url, serverOptions) {
|
|
542
568
|
const parsedUrl = new URL(url, 'http://localhost');
|
|
543
569
|
let pathname = decodeURIComponent(parsedUrl.pathname);
|
|
@@ -22,6 +22,7 @@ async function extractMessages(options, builderName, context, extractorConstruct
|
|
|
22
22
|
buildOptions.optimization = false;
|
|
23
23
|
buildOptions.sourceMap = { scripts: true, vendor: true };
|
|
24
24
|
buildOptions.localize = false;
|
|
25
|
+
buildOptions.budgets = undefined;
|
|
25
26
|
let build;
|
|
26
27
|
if (builderName === '@angular-devkit/build-angular:application') {
|
|
27
28
|
build = application_1.buildApplicationInternal;
|
|
@@ -45,7 +45,7 @@ async function extract() {
|
|
|
45
45
|
const bootstrapAppFnOrModule = bootstrapAppFn || AppServerModule;
|
|
46
46
|
(0, node_assert_1.default)(bootstrapAppFnOrModule, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
|
|
47
47
|
const routes = [];
|
|
48
|
-
for await (const { route, success } of extractRoutes(bootstrapAppFnOrModule, document
|
|
48
|
+
for await (const { route, success } of extractRoutes(bootstrapAppFnOrModule, document)) {
|
|
49
49
|
if (success) {
|
|
50
50
|
routes.push(route);
|
|
51
51
|
}
|
|
@@ -17,6 +17,10 @@ const SET_CLASS_METADATA_NAME = 'ɵsetClassMetadata';
|
|
|
17
17
|
* Name of the asynchronous Angular class metadata function created by the Angular compiler.
|
|
18
18
|
*/
|
|
19
19
|
const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync';
|
|
20
|
+
/**
|
|
21
|
+
* Name of the function that sets debug information on classes.
|
|
22
|
+
*/
|
|
23
|
+
const SET_CLASS_DEBUG_INFO_NAME = 'ɵsetClassDebugInfo';
|
|
20
24
|
/**
|
|
21
25
|
* Provides one or more keywords that if found within the content of a source file indicate
|
|
22
26
|
* that this plugin should be used with a source file.
|
|
@@ -24,7 +28,7 @@ const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync';
|
|
|
24
28
|
* @returns An a string iterable containing one or more keywords.
|
|
25
29
|
*/
|
|
26
30
|
function getKeywords() {
|
|
27
|
-
return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME];
|
|
31
|
+
return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME, SET_CLASS_DEBUG_INFO_NAME];
|
|
28
32
|
}
|
|
29
33
|
exports.getKeywords = getKeywords;
|
|
30
34
|
/**
|
|
@@ -48,7 +52,8 @@ function default_1() {
|
|
|
48
52
|
}
|
|
49
53
|
if (calleeName !== undefined &&
|
|
50
54
|
(isRemoveClassMetadataCall(calleeName, callArguments) ||
|
|
51
|
-
isRemoveClassmetadataAsyncCall(calleeName, callArguments)
|
|
55
|
+
isRemoveClassmetadataAsyncCall(calleeName, callArguments) ||
|
|
56
|
+
isSetClassDebugInfoCall(calleeName, callArguments))) {
|
|
52
57
|
// The metadata function is always emitted inside a function expression
|
|
53
58
|
const parent = path.getFunctionParent();
|
|
54
59
|
if (parent && (parent.isFunctionExpression() || parent.isArrowFunctionExpression())) {
|
|
@@ -84,6 +89,13 @@ function isRemoveClassmetadataAsyncCall(name, args) {
|
|
|
84
89
|
isInlineFunction(args[1]) &&
|
|
85
90
|
isInlineFunction(args[2]));
|
|
86
91
|
}
|
|
92
|
+
/** Determines if a function call is a call to `setClassDebugInfo`. */
|
|
93
|
+
function isSetClassDebugInfoCall(name, args) {
|
|
94
|
+
return (name === SET_CLASS_DEBUG_INFO_NAME &&
|
|
95
|
+
args.length === 2 &&
|
|
96
|
+
core_1.types.isIdentifier(args[0]) &&
|
|
97
|
+
core_1.types.isObjectExpression(args[1]));
|
|
98
|
+
}
|
|
87
99
|
/** Determines if a node is an inline function expression. */
|
|
88
100
|
function isInlineFunction(node) {
|
|
89
101
|
return core_1.types.isFunctionExpression(node) || core_1.types.isArrowFunctionExpression(node);
|
|
@@ -172,7 +172,8 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
|
|
|
172
172
|
// Return bundled worker file entry name to be used in the built output
|
|
173
173
|
const workerCodeFile = workerResult.outputFiles.find((file) => file.path.endsWith('.js'));
|
|
174
174
|
(0, node_assert_1.default)(workerCodeFile, 'Web Worker bundled code file should always be present.');
|
|
175
|
-
|
|
175
|
+
const workerCodePath = path.relative(build.initialOptions.outdir ?? '', workerCodeFile.path);
|
|
176
|
+
return workerCodePath.replaceAll('\\', '/');
|
|
176
177
|
},
|
|
177
178
|
};
|
|
178
179
|
// Initialize the Angular compilation for the current build.
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import type { BuildOptions } from 'esbuild';
|
|
9
9
|
import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
|
|
10
10
|
import { SourceFileCache } from './angular/source-file-cache';
|
|
11
|
+
import { BundlerOptionsFactory } from './bundler-context';
|
|
11
12
|
export declare function createBrowserCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions;
|
|
12
|
-
export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | undefined;
|
|
13
|
+
export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | BundlerOptionsFactory | undefined;
|
|
13
14
|
/**
|
|
14
15
|
* Create an esbuild 'build' options object for the server bundle.
|
|
15
16
|
* @param options The builder's user-provider normalized options.
|
|
16
17
|
* @returns An esbuild BuildOptions object.
|
|
17
18
|
*/
|
|
18
19
|
export declare function createServerCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;
|
|
19
|
-
export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache):
|
|
20
|
+
export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BundlerOptionsFactory | undefined;
|
|
@@ -63,6 +63,7 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
const { outputNames, polyfills } = options;
|
|
66
|
+
const hasTypeScriptEntries = polyfills?.some((entry) => /\.[cm]?tsx?$/.test(entry));
|
|
66
67
|
const buildOptions = {
|
|
67
68
|
...polyfillBundleOptions,
|
|
68
69
|
platform: 'browser',
|
|
@@ -78,7 +79,6 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
|
|
|
78
79
|
},
|
|
79
80
|
};
|
|
80
81
|
// Only add the Angular TypeScript compiler if TypeScript files are provided in the polyfills
|
|
81
|
-
const hasTypeScriptEntries = polyfills?.some((entry) => /\.[cm]?tsx?$/.test(entry));
|
|
82
82
|
if (hasTypeScriptEntries) {
|
|
83
83
|
buildOptions.plugins ??= [];
|
|
84
84
|
const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
|
|
@@ -88,7 +88,10 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
|
|
|
88
88
|
// Component stylesheet options are unused for polyfills but required by the plugin
|
|
89
89
|
styleOptions));
|
|
90
90
|
}
|
|
91
|
-
|
|
91
|
+
// Use an options factory to allow fully incremental bundling when no TypeScript files are present.
|
|
92
|
+
// The TypeScript compilation is not currently integrated into the bundler invalidation so
|
|
93
|
+
// cannot be used with fully incremental bundling yet.
|
|
94
|
+
return hasTypeScriptEntries ? buildOptions : () => buildOptions;
|
|
92
95
|
}
|
|
93
96
|
exports.createBrowserPolyfillBundleOptions = createBrowserPolyfillBundleOptions;
|
|
94
97
|
/**
|
|
@@ -232,7 +235,7 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
|
|
|
232
235
|
}));
|
|
233
236
|
}
|
|
234
237
|
buildOptions.plugins.push((0, rxjs_esm_resolution_plugin_1.createRxjsEsmResolutionPlugin)());
|
|
235
|
-
return buildOptions;
|
|
238
|
+
return () => buildOptions;
|
|
236
239
|
}
|
|
237
240
|
exports.createServerPolyfillBundleOptions = createServerPolyfillBundleOptions;
|
|
238
241
|
function getEsBuildCommonOptions(options) {
|
|
@@ -17,6 +17,7 @@ export interface RebuildState {
|
|
|
17
17
|
rebuildContexts: BundlerContext[];
|
|
18
18
|
codeBundleCache?: SourceFileCache;
|
|
19
19
|
fileChanges: ChangedFiles;
|
|
20
|
+
previousOutputHashes: Map<string, string>;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* Represents the result of a single builder execute call.
|
|
@@ -57,5 +58,6 @@ export declare class ExecutionResult {
|
|
|
57
58
|
};
|
|
58
59
|
get watchFiles(): string[];
|
|
59
60
|
createRebuildState(fileChanges: ChangedFiles): RebuildState;
|
|
61
|
+
findChangedFiles(previousOutputHashes: Map<string, string>): Set<string>;
|
|
60
62
|
dispose(): Promise<void>;
|
|
61
63
|
}
|
|
@@ -60,6 +60,9 @@ class ExecutionResult {
|
|
|
60
60
|
if (this.codeBundleCache?.referencedFiles) {
|
|
61
61
|
files.push(...this.codeBundleCache.referencedFiles);
|
|
62
62
|
}
|
|
63
|
+
if (this.codeBundleCache?.loadResultCache) {
|
|
64
|
+
files.push(...this.codeBundleCache.loadResultCache.watchFiles);
|
|
65
|
+
}
|
|
63
66
|
return files;
|
|
64
67
|
}
|
|
65
68
|
createRebuildState(fileChanges) {
|
|
@@ -68,8 +71,19 @@ class ExecutionResult {
|
|
|
68
71
|
rebuildContexts: this.rebuildContexts,
|
|
69
72
|
codeBundleCache: this.codeBundleCache,
|
|
70
73
|
fileChanges,
|
|
74
|
+
previousOutputHashes: new Map(this.outputFiles.map((file) => [file.path, file.hash])),
|
|
71
75
|
};
|
|
72
76
|
}
|
|
77
|
+
findChangedFiles(previousOutputHashes) {
|
|
78
|
+
const changed = new Set();
|
|
79
|
+
for (const file of this.outputFiles) {
|
|
80
|
+
const previousHash = previousOutputHashes.get(file.path);
|
|
81
|
+
if (previousHash === undefined || previousHash !== file.hash) {
|
|
82
|
+
changed.add(file.path);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return changed;
|
|
86
|
+
}
|
|
73
87
|
async dispose() {
|
|
74
88
|
await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose()));
|
|
75
89
|
}
|
|
@@ -84,11 +84,11 @@ function checkCommonJSModules(metafile, allowedCommonJsDependencies) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
if (notAllowed) {
|
|
87
|
-
// Issue a diagnostic message
|
|
88
|
-
// likely not ESM but solved by addressing this import.
|
|
87
|
+
// Issue a diagnostic message for CommonJS module
|
|
89
88
|
messages.push(createCommonJSModuleError(request, currentFile));
|
|
90
|
-
continue;
|
|
91
89
|
}
|
|
90
|
+
// Skip all descendants since they are also most likely not ESM but solved by addressing this import
|
|
91
|
+
continue;
|
|
92
92
|
}
|
|
93
93
|
// Add the path so that its imports can be checked
|
|
94
94
|
files.push(imported.path);
|
|
@@ -23,6 +23,7 @@ export interface JavaScriptTransformerOptions {
|
|
|
23
23
|
*/
|
|
24
24
|
export declare class JavaScriptTransformer {
|
|
25
25
|
#private;
|
|
26
|
+
readonly maxThreads: number;
|
|
26
27
|
constructor(options: JavaScriptTransformerOptions, maxThreads: number);
|
|
27
28
|
/**
|
|
28
29
|
* Performs JavaScript transformations on a file from the filesystem.
|
|
@@ -20,16 +20,11 @@ const piscina_1 = __importDefault(require("piscina"));
|
|
|
20
20
|
* and advanced optimizations.
|
|
21
21
|
*/
|
|
22
22
|
class JavaScriptTransformer {
|
|
23
|
+
maxThreads;
|
|
23
24
|
#workerPool;
|
|
24
25
|
#commonOptions;
|
|
25
26
|
constructor(options, maxThreads) {
|
|
26
|
-
this
|
|
27
|
-
filename: require.resolve('./javascript-transformer-worker'),
|
|
28
|
-
minThreads: 1,
|
|
29
|
-
maxThreads,
|
|
30
|
-
// Shutdown idle threads after 1 second of inactivity
|
|
31
|
-
idleTimeout: 1000,
|
|
32
|
-
});
|
|
27
|
+
this.maxThreads = maxThreads;
|
|
33
28
|
// Extract options to ensure only the named options are serialized and sent to the worker
|
|
34
29
|
const { sourcemap, thirdPartySourcemaps = false, advancedOptimizations = false, jit = false, } = options;
|
|
35
30
|
this.#commonOptions = {
|
|
@@ -39,6 +34,16 @@ class JavaScriptTransformer {
|
|
|
39
34
|
jit,
|
|
40
35
|
};
|
|
41
36
|
}
|
|
37
|
+
#ensureWorkerPool() {
|
|
38
|
+
this.#workerPool ??= new piscina_1.default({
|
|
39
|
+
filename: require.resolve('./javascript-transformer-worker'),
|
|
40
|
+
minThreads: 1,
|
|
41
|
+
maxThreads: this.maxThreads,
|
|
42
|
+
// Shutdown idle threads after 1 second of inactivity
|
|
43
|
+
idleTimeout: 1000,
|
|
44
|
+
});
|
|
45
|
+
return this.#workerPool;
|
|
46
|
+
}
|
|
42
47
|
/**
|
|
43
48
|
* Performs JavaScript transformations on a file from the filesystem.
|
|
44
49
|
* If no transformations are required, the data for the original file will be returned.
|
|
@@ -49,7 +54,7 @@ class JavaScriptTransformer {
|
|
|
49
54
|
transformFile(filename, skipLinker) {
|
|
50
55
|
// Always send the request to a worker. Files are almost always from node modules which means
|
|
51
56
|
// they may need linking. The data is also not yet available to perform most transformation checks.
|
|
52
|
-
return this.#
|
|
57
|
+
return this.#ensureWorkerPool().run({
|
|
53
58
|
filename,
|
|
54
59
|
skipLinker,
|
|
55
60
|
...this.#commonOptions,
|
|
@@ -71,7 +76,7 @@ class JavaScriptTransformer {
|
|
|
71
76
|
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
|
|
72
77
|
return Buffer.from(keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), 'utf-8');
|
|
73
78
|
}
|
|
74
|
-
return this.#
|
|
79
|
+
return this.#ensureWorkerPool().run({
|
|
75
80
|
filename,
|
|
76
81
|
data,
|
|
77
82
|
skipLinker,
|
|
@@ -82,10 +87,17 @@ class JavaScriptTransformer {
|
|
|
82
87
|
* Stops all active transformation tasks and shuts down all workers.
|
|
83
88
|
* @returns A void promise that resolves when closing is complete.
|
|
84
89
|
*/
|
|
85
|
-
close() {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
async close() {
|
|
91
|
+
if (this.#workerPool) {
|
|
92
|
+
// Workaround piscina bug where a worker thread will be recreated after destroy to meet the minimum.
|
|
93
|
+
this.#workerPool.options.minThreads = 0;
|
|
94
|
+
try {
|
|
95
|
+
await this.#workerPool.destroy();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
this.#workerPool = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
103
|
exports.JavaScriptTransformer = JavaScriptTransformer;
|
|
@@ -10,7 +10,7 @@ import { BuildOptions, Metafile, OutputFile, PartialMessage } from 'esbuild';
|
|
|
10
10
|
import { BudgetCalculatorResult } from '../../utils/bundle-calculator';
|
|
11
11
|
import { BuildOutputFile, BuildOutputFileType, InitialFileRecord } from './bundler-context';
|
|
12
12
|
import { BuildOutputAsset } from './bundler-execution-result';
|
|
13
|
-
export declare function logBuildStats(context: BuilderContext, metafile: Metafile, initial: Map<string, InitialFileRecord>, budgetFailures: BudgetCalculatorResult[] | undefined, estimatedTransferSizes?: Map<string, number>): void;
|
|
13
|
+
export declare function logBuildStats(context: BuilderContext, metafile: Metafile, initial: Map<string, InitialFileRecord>, budgetFailures: BudgetCalculatorResult[] | undefined, changedFiles?: Set<string>, estimatedTransferSizes?: Map<string, number>): void;
|
|
14
14
|
export declare function calculateEstimatedTransferSizes(outputFiles: OutputFile[]): Promise<Map<string, number>>;
|
|
15
15
|
export declare function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T>;
|
|
16
16
|
export declare function withNoProgress<T>(text: string, action: () => T | Promise<T>): Promise<T>;
|
|
@@ -46,8 +46,9 @@ const spinner_1 = require("../../utils/spinner");
|
|
|
46
46
|
const stats_1 = require("../webpack/utils/stats");
|
|
47
47
|
const bundler_context_1 = require("./bundler-context");
|
|
48
48
|
const compressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliCompress);
|
|
49
|
-
function logBuildStats(context, metafile, initial, budgetFailures, estimatedTransferSizes) {
|
|
49
|
+
function logBuildStats(context, metafile, initial, budgetFailures, changedFiles, estimatedTransferSizes) {
|
|
50
50
|
const stats = [];
|
|
51
|
+
let unchangedCount = 0;
|
|
51
52
|
for (const [file, output] of Object.entries(metafile.outputs)) {
|
|
52
53
|
// Only display JavaScript and CSS files
|
|
53
54
|
if (!file.endsWith('.js') && !file.endsWith('.css')) {
|
|
@@ -58,6 +59,11 @@ function logBuildStats(context, metafile, initial, budgetFailures, estimatedTran
|
|
|
58
59
|
if (output['ng-component']) {
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
62
|
+
// Show only changed files if a changed list is provided
|
|
63
|
+
if (changedFiles && !changedFiles.has(file)) {
|
|
64
|
+
++unchangedCount;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
61
67
|
let name = initial.get(file)?.name;
|
|
62
68
|
if (name === undefined && output.entryPoint) {
|
|
63
69
|
name = node_path_1.default
|
|
@@ -70,8 +76,16 @@ function logBuildStats(context, metafile, initial, budgetFailures, estimatedTran
|
|
|
70
76
|
stats: [file, name ?? '-', output.bytes, estimatedTransferSizes?.get(file) ?? '-'],
|
|
71
77
|
});
|
|
72
78
|
}
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
if (stats.length > 0) {
|
|
80
|
+
const tableText = (0, stats_1.generateBuildStatsTable)(stats, true, unchangedCount === 0, !!estimatedTransferSizes, budgetFailures);
|
|
81
|
+
context.logger.info('\n' + tableText + '\n');
|
|
82
|
+
}
|
|
83
|
+
else if (changedFiles !== undefined) {
|
|
84
|
+
context.logger.info('\nNo output file changes.\n');
|
|
85
|
+
}
|
|
86
|
+
if (unchangedCount > 0) {
|
|
87
|
+
context.logger.info(`Unchanged output files: ${unchangedCount}`);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
exports.logBuildStats = logBuildStats;
|
|
77
91
|
async function calculateEstimatedTransferSizes(outputFiles) {
|
|
@@ -327,7 +341,7 @@ function transformSupportedBrowsersToTargets(supportedBrowsers) {
|
|
|
327
341
|
return transformed;
|
|
328
342
|
}
|
|
329
343
|
exports.transformSupportedBrowsersToTargets = transformSupportedBrowsersToTargets;
|
|
330
|
-
const SUPPORTED_NODE_VERSIONS = '
|
|
344
|
+
const SUPPORTED_NODE_VERSIONS = '^18.13.0 || >=20.9.0';
|
|
331
345
|
/**
|
|
332
346
|
* Transform supported Node.js versions to esbuild target.
|
|
333
347
|
* @see https://esbuild.github.io/api/#target
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* found in the LICENSE file at https://angular.io/license
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.debugPerformance = exports.useLegacySass = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0;
|
|
10
|
+
exports.shouldWatchRoot = exports.debugPerformance = exports.useLegacySass = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0;
|
|
11
11
|
const color_1 = require("./color");
|
|
12
12
|
function isDisabled(variable) {
|
|
13
13
|
return variable === '0' || variable.toLowerCase() === 'false';
|
|
@@ -81,3 +81,5 @@ exports.useLegacySass = (() => {
|
|
|
81
81
|
})();
|
|
82
82
|
const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF'];
|
|
83
83
|
exports.debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable);
|
|
84
|
+
const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT'];
|
|
85
|
+
exports.shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable);
|
|
@@ -11,5 +11,5 @@ interface RouterResult {
|
|
|
11
11
|
success: boolean;
|
|
12
12
|
redirect: boolean;
|
|
13
13
|
}
|
|
14
|
-
export declare function extractRoutes(bootstrapAppFnOrModule: (() => Promise<ApplicationRef>) | Type<unknown>, document: string
|
|
14
|
+
export declare function extractRoutes(bootstrapAppFnOrModule: (() => Promise<ApplicationRef>) | Type<unknown>, document: string): AsyncIterableIterator<RouterResult>;
|
|
15
15
|
export {};
|
|
@@ -38,11 +38,11 @@ async function* getRoutesFromRouterConfig(routes, compiler, parentInjector, pare
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export async function* extractRoutes(bootstrapAppFnOrModule, document
|
|
41
|
+
export async function* extractRoutes(bootstrapAppFnOrModule, document) {
|
|
42
42
|
const platformRef = createPlatformFactory(platformCore, 'server', [
|
|
43
43
|
{
|
|
44
44
|
provide: INITIAL_CONFIG,
|
|
45
|
-
useValue: { document, url },
|
|
45
|
+
useValue: { document, url: '' },
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
provide: ɵConsole,
|
|
@@ -47,7 +47,9 @@ function resolve(specifier, context, nextResolve) {
|
|
|
47
47
|
}
|
|
48
48
|
exports.resolve = resolve;
|
|
49
49
|
async function load(url, context, nextLoad) {
|
|
50
|
-
|
|
50
|
+
const { format } = context;
|
|
51
|
+
// CommonJs modules require no transformations and are not in memory.
|
|
52
|
+
if (format !== 'commonjs' && isFileProtocol(url)) {
|
|
51
53
|
const filePath = (0, url_1.fileURLToPath)(url);
|
|
52
54
|
// Remove '/' or drive letter for Windows that was added in the above 'resolve'.
|
|
53
55
|
let source = outputFiles[(0, node_path_1.relative)('/', filePath)] ?? TRANSFORMED_FILES[filePath];
|
|
@@ -55,7 +57,6 @@ async function load(url, context, nextLoad) {
|
|
|
55
57
|
source = TRANSFORMED_FILES[filePath] = Buffer.from(await javascriptTransformer.transformFile(filePath)).toString('utf-8');
|
|
56
58
|
}
|
|
57
59
|
if (source !== undefined) {
|
|
58
|
-
const { format } = context;
|
|
59
60
|
return {
|
|
60
61
|
format,
|
|
61
62
|
shortCircuit: true,
|
|
@@ -0,0 +1,8 @@
|
|
|
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
|
+
export declare function patchFetchToLoadInMemoryAssets(): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
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.patchFetchToLoadInMemoryAssets = void 0;
|
|
11
|
+
const mrmime_1 = require("mrmime");
|
|
12
|
+
const promises_1 = require("node:fs/promises");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
15
|
+
const undici_1 = require("undici");
|
|
16
|
+
/**
|
|
17
|
+
* This is passed as workerData when setting up the worker via the `piscina` package.
|
|
18
|
+
*/
|
|
19
|
+
const { assetFiles } = node_worker_threads_1.workerData;
|
|
20
|
+
const assetsCache = new Map();
|
|
21
|
+
const RESOLVE_PROTOCOL = 'resolve:';
|
|
22
|
+
function patchFetchToLoadInMemoryAssets() {
|
|
23
|
+
const global = globalThis;
|
|
24
|
+
const originalFetch = global.fetch;
|
|
25
|
+
const patchedFetch = async (input, init) => {
|
|
26
|
+
let url;
|
|
27
|
+
if (input instanceof URL) {
|
|
28
|
+
url = input;
|
|
29
|
+
}
|
|
30
|
+
else if (typeof input === 'string') {
|
|
31
|
+
url = new URL(input, RESOLVE_PROTOCOL + '//');
|
|
32
|
+
}
|
|
33
|
+
else if (typeof input === 'object' && 'url' in input) {
|
|
34
|
+
url = new URL(input.url, RESOLVE_PROTOCOL + '//');
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return originalFetch(input, init);
|
|
38
|
+
}
|
|
39
|
+
const { pathname, protocol } = url;
|
|
40
|
+
if (protocol !== RESOLVE_PROTOCOL || !assetFiles[pathname]) {
|
|
41
|
+
// Only handle relative requests or files that are in assets.
|
|
42
|
+
return originalFetch(input, init);
|
|
43
|
+
}
|
|
44
|
+
const cachedAsset = assetsCache.get(pathname);
|
|
45
|
+
if (cachedAsset) {
|
|
46
|
+
const { content, headers } = cachedAsset;
|
|
47
|
+
return new undici_1.Response(content, {
|
|
48
|
+
headers,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const extension = (0, node_path_1.extname)(pathname);
|
|
52
|
+
const mimeType = (0, mrmime_1.lookup)(extension);
|
|
53
|
+
const content = await (0, promises_1.readFile)(assetFiles[pathname]);
|
|
54
|
+
const headers = mimeType
|
|
55
|
+
? {
|
|
56
|
+
'Content-Type': mimeType,
|
|
57
|
+
}
|
|
58
|
+
: undefined;
|
|
59
|
+
assetsCache.set(pathname, { headers, content });
|
|
60
|
+
return new undici_1.Response(content, {
|
|
61
|
+
headers,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
global.fetch = patchedFetch;
|
|
65
|
+
}
|
|
66
|
+
exports.patchFetchToLoadInMemoryAssets = patchFetchToLoadInMemoryAssets;
|
|
@@ -16,7 +16,6 @@ const node_path_1 = require("node:path");
|
|
|
16
16
|
const piscina_1 = __importDefault(require("piscina"));
|
|
17
17
|
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
|
|
18
18
|
const node_18_utils_1 = require("./esm-in-memory-loader/node-18-utils");
|
|
19
|
-
const prerender_server_1 = require("./prerender-server");
|
|
20
19
|
async function prerenderPages(workspaceRoot, appShellOptions = {}, prerenderOptions = {}, outputFiles, assets, document, sourcemap = false, inlineCriticalCss = false, maxThreads = 1, verbose = false) {
|
|
21
20
|
const outputFilesForWorker = {};
|
|
22
21
|
const serverBundlesSourceMaps = new Map();
|
|
@@ -45,37 +44,33 @@ async function prerenderPages(workspaceRoot, appShellOptions = {}, prerenderOpti
|
|
|
45
44
|
}
|
|
46
45
|
}
|
|
47
46
|
serverBundlesSourceMaps.clear();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
errors,
|
|
60
|
-
warnings,
|
|
61
|
-
output: {},
|
|
62
|
-
prerenderedRoutes: allRoutes,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// Render routes
|
|
66
|
-
const { warnings: renderingWarnings, errors: renderingErrors, output, } = await renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, inlineCriticalCss, document, assetsServerAddress, appShellOptions);
|
|
67
|
-
errors.push(...renderingErrors);
|
|
68
|
-
warnings.push(...renderingWarnings);
|
|
47
|
+
const assetsReversed = {};
|
|
48
|
+
for (const { source, destination } of assets) {
|
|
49
|
+
assetsReversed[addLeadingSlash(destination.replace(/\\/g, node_path_1.posix.sep))] = source;
|
|
50
|
+
}
|
|
51
|
+
// Get routes to prerender
|
|
52
|
+
const { routes: allRoutes, warnings: routesWarnings } = await getAllRoutes(workspaceRoot, outputFilesForWorker, assetsReversed, document, appShellOptions, prerenderOptions, sourcemap, verbose);
|
|
53
|
+
if (routesWarnings?.length) {
|
|
54
|
+
warnings.push(...routesWarnings);
|
|
55
|
+
}
|
|
56
|
+
if (allRoutes.size < 1) {
|
|
69
57
|
return {
|
|
70
58
|
errors,
|
|
71
59
|
warnings,
|
|
72
|
-
output,
|
|
60
|
+
output: {},
|
|
73
61
|
prerenderedRoutes: allRoutes,
|
|
74
62
|
};
|
|
75
63
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
64
|
+
// Render routes
|
|
65
|
+
const { warnings: renderingWarnings, errors: renderingErrors, output, } = await renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, assetsReversed, inlineCriticalCss, document, appShellOptions);
|
|
66
|
+
errors.push(...renderingErrors);
|
|
67
|
+
warnings.push(...renderingWarnings);
|
|
68
|
+
return {
|
|
69
|
+
errors,
|
|
70
|
+
warnings,
|
|
71
|
+
output,
|
|
72
|
+
prerenderedRoutes: allRoutes,
|
|
73
|
+
};
|
|
79
74
|
}
|
|
80
75
|
exports.prerenderPages = prerenderPages;
|
|
81
76
|
class RoutesSet extends Set {
|
|
@@ -83,7 +78,7 @@ class RoutesSet extends Set {
|
|
|
83
78
|
return super.add(addLeadingSlash(value));
|
|
84
79
|
}
|
|
85
80
|
}
|
|
86
|
-
async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, inlineCriticalCss, document,
|
|
81
|
+
async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, assetFilesForWorker, inlineCriticalCss, document, appShellOptions) {
|
|
87
82
|
const output = {};
|
|
88
83
|
const warnings = [];
|
|
89
84
|
const errors = [];
|
|
@@ -97,9 +92,9 @@ async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outp
|
|
|
97
92
|
workerData: {
|
|
98
93
|
workspaceRoot,
|
|
99
94
|
outputFiles: outputFilesForWorker,
|
|
95
|
+
assetFiles: assetFilesForWorker,
|
|
100
96
|
inlineCriticalCss,
|
|
101
97
|
document,
|
|
102
|
-
baseUrl,
|
|
103
98
|
},
|
|
104
99
|
execArgv: workerExecArgv,
|
|
105
100
|
});
|
|
@@ -139,7 +134,7 @@ async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outp
|
|
|
139
134
|
output,
|
|
140
135
|
};
|
|
141
136
|
}
|
|
142
|
-
async function getAllRoutes(workspaceRoot, outputFilesForWorker, document, appShellOptions, prerenderOptions, sourcemap, verbose
|
|
137
|
+
async function getAllRoutes(workspaceRoot, outputFilesForWorker, assetFilesForWorker, document, appShellOptions, prerenderOptions, sourcemap, verbose) {
|
|
143
138
|
const { routesFile, discoverRoutes } = prerenderOptions;
|
|
144
139
|
const routes = new RoutesSet();
|
|
145
140
|
const { route: appShellRoute } = appShellOptions;
|
|
@@ -165,9 +160,9 @@ async function getAllRoutes(workspaceRoot, outputFilesForWorker, document, appSh
|
|
|
165
160
|
workerData: {
|
|
166
161
|
workspaceRoot,
|
|
167
162
|
outputFiles: outputFilesForWorker,
|
|
163
|
+
assetFiles: assetFilesForWorker,
|
|
168
164
|
document,
|
|
169
165
|
verbose,
|
|
170
|
-
url: assetsServerAddress,
|
|
171
166
|
},
|
|
172
167
|
execArgv: workerExecArgv,
|
|
173
168
|
});
|
|
@@ -10,11 +10,13 @@ import { RenderResult, ServerContext } from './render-page';
|
|
|
10
10
|
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
|
|
11
11
|
document: string;
|
|
12
12
|
inlineCriticalCss?: boolean;
|
|
13
|
-
|
|
13
|
+
assetFiles: Record</** Destination */ string, /** Source */ string>;
|
|
14
14
|
}
|
|
15
15
|
export interface RenderOptions {
|
|
16
16
|
route: string;
|
|
17
17
|
serverContext: ServerContext;
|
|
18
18
|
}
|
|
19
19
|
/** Renders an application based on a provided options. */
|
|
20
|
-
|
|
20
|
+
declare function render(options: RenderOptions): Promise<RenderResult>;
|
|
21
|
+
declare const _default: typeof render;
|
|
22
|
+
export default _default;
|
|
@@ -8,19 +8,23 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
const node_worker_threads_1 = require("node:worker_threads");
|
|
11
|
+
const fetch_patch_1 = require("./fetch-patch");
|
|
11
12
|
const render_page_1 = require("./render-page");
|
|
12
13
|
/**
|
|
13
14
|
* This is passed as workerData when setting up the worker via the `piscina` package.
|
|
14
15
|
*/
|
|
15
|
-
const { outputFiles, document, inlineCriticalCss
|
|
16
|
+
const { outputFiles, document, inlineCriticalCss } = node_worker_threads_1.workerData;
|
|
16
17
|
/** Renders an application based on a provided options. */
|
|
17
|
-
function
|
|
18
|
+
async function render(options) {
|
|
18
19
|
return (0, render_page_1.renderPage)({
|
|
19
20
|
...options,
|
|
20
|
-
route: baseUrl + options.route,
|
|
21
21
|
outputFiles,
|
|
22
22
|
document,
|
|
23
23
|
inlineCriticalCss,
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
function initialize() {
|
|
27
|
+
(0, fetch_patch_1.patchFetchToLoadInMemoryAssets)();
|
|
28
|
+
return render;
|
|
29
|
+
}
|
|
30
|
+
exports.default = initialize();
|
|
@@ -9,11 +9,13 @@ import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loa
|
|
|
9
9
|
export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData {
|
|
10
10
|
document: string;
|
|
11
11
|
verbose: boolean;
|
|
12
|
-
|
|
13
|
-
assetsServerAddress: string;
|
|
12
|
+
assetFiles: Record</** Destination */ string, /** Source */ string>;
|
|
14
13
|
}
|
|
15
14
|
export interface RoutersExtractorWorkerResult {
|
|
16
15
|
routes: string[];
|
|
17
16
|
warnings?: string[];
|
|
18
17
|
}
|
|
19
|
-
|
|
18
|
+
/** Renders an application based on a provided options. */
|
|
19
|
+
declare function extractRoutes(): Promise<RoutersExtractorWorkerResult>;
|
|
20
|
+
declare const _default: typeof extractRoutes;
|
|
21
|
+
export default _default;
|
|
@@ -9,17 +9,19 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
const node_worker_threads_1 = require("node:worker_threads");
|
|
11
11
|
const load_esm_1 = require("../load-esm");
|
|
12
|
+
const fetch_patch_1 = require("./fetch-patch");
|
|
12
13
|
/**
|
|
13
14
|
* This is passed as workerData when setting up the worker via the `piscina` package.
|
|
14
15
|
*/
|
|
15
|
-
const { document, verbose
|
|
16
|
-
|
|
16
|
+
const { document, verbose } = node_worker_threads_1.workerData;
|
|
17
|
+
/** Renders an application based on a provided options. */
|
|
18
|
+
async function extractRoutes() {
|
|
17
19
|
const { extractRoutes } = await (0, load_esm_1.loadEsmModule)('./render-utils.server.mjs');
|
|
18
20
|
const { default: bootstrapAppFnOrModule } = await (0, load_esm_1.loadEsmModule)('./main.server.mjs');
|
|
19
21
|
const skippedRedirects = [];
|
|
20
22
|
const skippedOthers = [];
|
|
21
23
|
const routes = [];
|
|
22
|
-
for await (const { route, success, redirect } of extractRoutes(bootstrapAppFnOrModule, document
|
|
24
|
+
for await (const { route, success, redirect } of extractRoutes(bootstrapAppFnOrModule, document)) {
|
|
23
25
|
if (success) {
|
|
24
26
|
routes.push(route);
|
|
25
27
|
continue;
|
|
@@ -44,4 +46,8 @@ async function default_1() {
|
|
|
44
46
|
}
|
|
45
47
|
return { routes, warnings };
|
|
46
48
|
}
|
|
47
|
-
|
|
49
|
+
function initialize() {
|
|
50
|
+
(0, fetch_patch_1.patchFetchToLoadInMemoryAssets)();
|
|
51
|
+
return extractRoutes;
|
|
52
|
+
}
|
|
53
|
+
exports.default = initialize();
|
|
@@ -1,21 +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
|
-
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
|
|
9
|
-
/**
|
|
10
|
-
* Start a server that can handle HTTP requests to assets.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* httpClient.get('/assets/content.json');
|
|
15
|
-
* ```
|
|
16
|
-
* @returns the server address.
|
|
17
|
-
*/
|
|
18
|
-
export declare function startServer(assets: Readonly<BuildOutputAsset[]>): Promise<{
|
|
19
|
-
address: string;
|
|
20
|
-
close?: () => void;
|
|
21
|
-
}>;
|
|
@@ -1,102 +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.startServer = void 0;
|
|
11
|
-
const mrmime_1 = require("mrmime");
|
|
12
|
-
const promises_1 = require("node:fs/promises");
|
|
13
|
-
const node_http_1 = require("node:http");
|
|
14
|
-
const node_path_1 = require("node:path");
|
|
15
|
-
/**
|
|
16
|
-
* Start a server that can handle HTTP requests to assets.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```ts
|
|
20
|
-
* httpClient.get('/assets/content.json');
|
|
21
|
-
* ```
|
|
22
|
-
* @returns the server address.
|
|
23
|
-
*/
|
|
24
|
-
async function startServer(assets) {
|
|
25
|
-
if (Object.keys(assets).length === 0) {
|
|
26
|
-
return {
|
|
27
|
-
address: '',
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
const assetsReversed = {};
|
|
31
|
-
for (const { source, destination } of assets) {
|
|
32
|
-
assetsReversed[addLeadingSlash(destination.replace(/\\/g, node_path_1.posix.sep))] = source;
|
|
33
|
-
}
|
|
34
|
-
const assetsCache = new Map();
|
|
35
|
-
const server = (0, node_http_1.createServer)(requestHandler(assetsReversed, assetsCache));
|
|
36
|
-
await new Promise((resolve) => {
|
|
37
|
-
server.listen(0, '127.0.0.1', resolve);
|
|
38
|
-
});
|
|
39
|
-
const serverAddress = server.address();
|
|
40
|
-
let address;
|
|
41
|
-
if (!serverAddress) {
|
|
42
|
-
address = '';
|
|
43
|
-
}
|
|
44
|
-
else if (typeof serverAddress === 'string') {
|
|
45
|
-
address = serverAddress;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
const { port, address: host } = serverAddress;
|
|
49
|
-
address = `http://${host}:${port}`;
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
address,
|
|
53
|
-
close: () => {
|
|
54
|
-
assetsCache.clear();
|
|
55
|
-
server.unref();
|
|
56
|
-
server.close();
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
exports.startServer = startServer;
|
|
61
|
-
function requestHandler(assetsReversed, assetsCache) {
|
|
62
|
-
return (req, res) => {
|
|
63
|
-
if (!req.url) {
|
|
64
|
-
res.destroy(new Error('Request url was empty.'));
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const { pathname } = new URL(req.url, 'resolve://');
|
|
68
|
-
const asset = assetsReversed[pathname];
|
|
69
|
-
if (!asset) {
|
|
70
|
-
res.statusCode = 404;
|
|
71
|
-
res.statusMessage = 'Asset not found.';
|
|
72
|
-
res.end();
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const cachedAsset = assetsCache.get(pathname);
|
|
76
|
-
if (cachedAsset) {
|
|
77
|
-
const { content, mimeType } = cachedAsset;
|
|
78
|
-
if (mimeType) {
|
|
79
|
-
res.setHeader('Content-Type', mimeType);
|
|
80
|
-
}
|
|
81
|
-
res.end(content);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
(0, promises_1.readFile)(asset)
|
|
85
|
-
.then((content) => {
|
|
86
|
-
const extension = (0, node_path_1.extname)(pathname);
|
|
87
|
-
const mimeType = (0, mrmime_1.lookup)(extension);
|
|
88
|
-
assetsCache.set(pathname, {
|
|
89
|
-
mimeType,
|
|
90
|
-
content,
|
|
91
|
-
});
|
|
92
|
-
if (mimeType) {
|
|
93
|
-
res.setHeader('Content-Type', mimeType);
|
|
94
|
-
}
|
|
95
|
-
res.end(content);
|
|
96
|
-
})
|
|
97
|
-
.catch((e) => res.destroy(e));
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
function addLeadingSlash(value) {
|
|
101
|
-
return value.charAt(0) === '/' ? value : '/' + value;
|
|
102
|
-
}
|