@angular/build 19.0.0-next.7 → 19.0.0-next.9
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/LICENSE +5 -5
- package/package.json +10 -9
- package/src/builders/application/execute-build.js +19 -2
- package/src/builders/application/execute-post-bundle.d.ts +2 -2
- package/src/builders/application/execute-post-bundle.js +30 -11
- package/src/builders/application/i18n.d.ts +2 -2
- package/src/builders/application/i18n.js +4 -5
- package/src/builders/application/index.js +8 -5
- package/src/builders/application/options.d.ts +25 -1
- package/src/builders/application/options.js +31 -2
- package/src/builders/application/schema.d.ts +15 -0
- package/src/builders/application/schema.js +11 -1
- package/src/builders/application/schema.json +5 -0
- package/src/builders/application/setup-bundling.js +6 -3
- package/src/builders/dev-server/vite-server.d.ts +2 -1
- package/src/builders/dev-server/vite-server.js +71 -53
- package/src/builders/extract-i18n/application-extraction.js +3 -3
- package/src/tools/angular/angular-host.d.ts +2 -1
- package/src/tools/angular/angular-host.js +20 -1
- package/src/tools/angular/compilation/angular-compilation.d.ts +1 -0
- package/src/tools/angular/compilation/aot-compilation.d.ts +1 -0
- package/src/tools/angular/compilation/aot-compilation.js +9 -1
- package/src/tools/angular/compilation/parallel-compilation.d.ts +1 -0
- package/src/tools/angular/compilation/parallel-worker.d.ts +1 -0
- package/src/tools/angular/compilation/parallel-worker.js +2 -1
- package/src/tools/babel/plugins/add-code-coverage.d.ts +14 -0
- package/src/tools/babel/plugins/add-code-coverage.js +44 -0
- package/src/tools/babel/plugins/types.d.ts +20 -0
- package/src/tools/esbuild/angular/compiler-plugin.d.ts +2 -0
- package/src/tools/esbuild/angular/compiler-plugin.js +44 -4
- package/src/tools/esbuild/angular/component-stylesheets.d.ts +8 -3
- package/src/tools/esbuild/angular/component-stylesheets.js +46 -11
- package/src/tools/esbuild/application-code-bundle.d.ts +1 -0
- package/src/tools/esbuild/application-code-bundle.js +109 -2
- package/src/tools/esbuild/budget-stats.js +1 -1
- package/src/tools/esbuild/bundler-context.d.ts +4 -3
- package/src/tools/esbuild/bundler-context.js +8 -4
- package/src/tools/esbuild/bundler-execution-result.d.ts +5 -2
- package/src/tools/esbuild/bundler-execution-result.js +7 -3
- package/src/tools/esbuild/cache.d.ts +5 -0
- package/src/tools/esbuild/cache.js +7 -0
- package/src/tools/esbuild/compiler-plugin-options.js +3 -1
- package/src/tools/esbuild/i18n-inliner.js +2 -1
- package/src/tools/esbuild/javascript-transformer-worker.d.ts +1 -0
- package/src/tools/esbuild/javascript-transformer-worker.js +5 -1
- package/src/tools/esbuild/javascript-transformer.d.ts +2 -2
- package/src/tools/esbuild/javascript-transformer.js +5 -3
- package/src/tools/esbuild/utils.js +7 -3
- package/src/tools/vite/middlewares/assets-middleware.js +2 -5
- package/src/tools/vite/middlewares/html-fallback-middleware.js +22 -6
- package/src/tools/vite/middlewares/index.d.ts +1 -1
- package/src/tools/vite/middlewares/index.js +3 -2
- package/src/tools/vite/middlewares/ssr-middleware.d.ts +2 -1
- package/src/tools/vite/middlewares/ssr-middleware.js +62 -15
- package/src/tools/vite/plugins/angular-memory-plugin.d.ts +16 -0
- package/src/tools/vite/{angular-memory-plugin.js → plugins/angular-memory-plugin.js} +19 -41
- package/src/tools/vite/{i18n-locale-plugin.d.ts → plugins/i18n-locale-plugin.d.ts} +0 -4
- package/src/tools/vite/{i18n-locale-plugin.js → plugins/i18n-locale-plugin.js} +2 -3
- package/src/tools/vite/plugins/index.d.ts +12 -0
- package/src/tools/vite/plugins/index.js +21 -0
- package/src/tools/vite/plugins/setup-middlewares-plugin.d.ts +41 -0
- package/src/tools/vite/plugins/setup-middlewares-plugin.js +62 -0
- package/src/tools/vite/plugins/ssr-transform-plugin.d.ts +9 -0
- package/src/tools/vite/plugins/ssr-transform-plugin.js +38 -0
- package/src/utils/environment-options.d.ts +2 -0
- package/src/utils/environment-options.js +5 -1
- package/src/utils/index-file/index-html-generator.js +5 -0
- package/src/utils/index-file/ngcm-attribute.d.ts +15 -0
- package/src/utils/index-file/ngcm-attribute.js +37 -0
- package/src/utils/index-file/valid-self-closing-tags.js +28 -0
- package/src/utils/normalize-cache.js +1 -1
- package/src/utils/server-rendering/fetch-patch.d.ts +1 -1
- package/src/utils/server-rendering/fetch-patch.js +2 -2
- package/src/utils/server-rendering/launch-server.d.ts +14 -0
- package/src/utils/server-rendering/launch-server.js +63 -0
- package/src/utils/server-rendering/load-esm-from-memory.d.ts +7 -0
- package/src/utils/server-rendering/manifest.d.ts +8 -2
- package/src/utils/server-rendering/manifest.js +52 -12
- package/src/utils/server-rendering/models.d.ts +27 -0
- package/src/utils/server-rendering/models.js +22 -0
- package/src/utils/server-rendering/prerender.d.ts +6 -10
- package/src/utils/server-rendering/prerender.js +102 -63
- package/src/utils/server-rendering/render-worker.d.ts +4 -1
- package/src/utils/server-rendering/render-worker.js +13 -3
- package/src/utils/server-rendering/routes-extractor-worker.d.ts +6 -10
- package/src/utils/server-rendering/routes-extractor-worker.js +14 -5
- package/src/utils/server-rendering/utils.d.ts +11 -0
- package/src/utils/server-rendering/utils.js +17 -0
- package/src/tools/vite/angular-memory-plugin.d.ts +0 -22
- /package/src/tools/vite/{id-prefix-plugin.d.ts → plugins/id-prefix-plugin.d.ts} +0 -0
- /package/src/tools/vite/{id-prefix-plugin.js → plugins/id-prefix-plugin.js} +0 -0
|
@@ -22,7 +22,7 @@ export declare class ComponentStylesheetBundler {
|
|
|
22
22
|
* @param cache A load result cache to use when bundling.
|
|
23
23
|
*/
|
|
24
24
|
constructor(options: BundleStylesheetOptions, incremental: boolean);
|
|
25
|
-
bundleFile(entry: string): Promise<{
|
|
25
|
+
bundleFile(entry: string, externalId?: string | boolean): Promise<{
|
|
26
26
|
errors: import("esbuild").Message[] | undefined;
|
|
27
27
|
warnings: import("esbuild").Message[];
|
|
28
28
|
contents: string;
|
|
@@ -30,7 +30,7 @@ export declare class ComponentStylesheetBundler {
|
|
|
30
30
|
metafile: import("esbuild").Metafile | undefined;
|
|
31
31
|
referencedFiles: Set<string> | undefined;
|
|
32
32
|
}>;
|
|
33
|
-
bundleInline(data: string, filename: string, language: string): Promise<{
|
|
33
|
+
bundleInline(data: string, filename: string, language: string, externalId?: string): Promise<{
|
|
34
34
|
errors: import("esbuild").Message[] | undefined;
|
|
35
35
|
warnings: import("esbuild").Message[];
|
|
36
36
|
contents: string;
|
|
@@ -38,7 +38,12 @@ export declare class ComponentStylesheetBundler {
|
|
|
38
38
|
metafile: import("esbuild").Metafile | undefined;
|
|
39
39
|
referencedFiles: Set<string> | undefined;
|
|
40
40
|
}>;
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Invalidates both file and inline based component style bundling state for a set of modified files.
|
|
43
|
+
* @param files The group of files that have been modified
|
|
44
|
+
* @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
|
|
45
|
+
*/
|
|
46
|
+
invalidate(files: Iterable<string>): string[] | undefined;
|
|
42
47
|
dispose(): Promise<void>;
|
|
43
48
|
private extractResult;
|
|
44
49
|
}
|
|
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
11
11
|
};
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.ComponentStylesheetBundler = void 0;
|
|
14
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
14
15
|
const node_crypto_1 = require("node:crypto");
|
|
15
16
|
const node_path_1 = __importDefault(require("node:path"));
|
|
16
17
|
const bundler_context_1 = require("../bundler-context");
|
|
@@ -35,21 +36,31 @@ class ComponentStylesheetBundler {
|
|
|
35
36
|
this.options = options;
|
|
36
37
|
this.incremental = incremental;
|
|
37
38
|
}
|
|
38
|
-
async bundleFile(entry) {
|
|
39
|
+
async bundleFile(entry, externalId) {
|
|
39
40
|
const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
|
|
40
41
|
return new bundler_context_1.BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
|
|
41
42
|
const buildOptions = (0, bundle_options_1.createStylesheetBundleOptions)(this.options, loadCache);
|
|
42
|
-
|
|
43
|
+
if (externalId) {
|
|
44
|
+
(0, node_assert_1.default)(typeof externalId === 'string', 'Initial external component stylesheets must have a string identifier');
|
|
45
|
+
buildOptions.entryPoints = { [externalId]: entry };
|
|
46
|
+
delete buildOptions.publicPath;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
buildOptions.entryPoints = [entry];
|
|
50
|
+
}
|
|
43
51
|
return buildOptions;
|
|
44
52
|
});
|
|
45
53
|
});
|
|
46
|
-
return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles);
|
|
54
|
+
return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId);
|
|
47
55
|
}
|
|
48
|
-
async bundleInline(data, filename, language) {
|
|
56
|
+
async bundleInline(data, filename, language, externalId) {
|
|
49
57
|
// Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
|
|
50
58
|
// to the actual stylesheet file path.
|
|
51
59
|
// TODO: Consider xxhash instead for hashing
|
|
52
|
-
const id = (0, node_crypto_1.createHash)('sha256')
|
|
60
|
+
const id = (0, node_crypto_1.createHash)('sha256')
|
|
61
|
+
.update(data)
|
|
62
|
+
.update(externalId ?? '')
|
|
63
|
+
.digest('hex');
|
|
53
64
|
const entry = [language, id, filename].join(';');
|
|
54
65
|
const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
|
|
55
66
|
const namespace = 'angular:styles/component';
|
|
@@ -57,7 +68,13 @@ class ComponentStylesheetBundler {
|
|
|
57
68
|
const buildOptions = (0, bundle_options_1.createStylesheetBundleOptions)(this.options, loadCache, {
|
|
58
69
|
[entry]: data,
|
|
59
70
|
});
|
|
60
|
-
|
|
71
|
+
if (externalId) {
|
|
72
|
+
buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
|
|
73
|
+
delete buildOptions.publicPath;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
buildOptions.entryPoints = [`${namespace};${entry}`];
|
|
77
|
+
}
|
|
61
78
|
buildOptions.plugins.push({
|
|
62
79
|
name: 'angular-component-styles',
|
|
63
80
|
setup(build) {
|
|
@@ -83,19 +100,29 @@ class ComponentStylesheetBundler {
|
|
|
83
100
|
});
|
|
84
101
|
});
|
|
85
102
|
// Extract the result of the bundling from the output files
|
|
86
|
-
return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles);
|
|
103
|
+
return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId);
|
|
87
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Invalidates both file and inline based component style bundling state for a set of modified files.
|
|
107
|
+
* @param files The group of files that have been modified
|
|
108
|
+
* @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
|
|
109
|
+
*/
|
|
88
110
|
invalidate(files) {
|
|
89
111
|
if (!this.incremental) {
|
|
90
112
|
return;
|
|
91
113
|
}
|
|
92
114
|
const normalizedFiles = [...files].map(node_path_1.default.normalize);
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
let entries;
|
|
116
|
+
for (const [entry, bundler] of this.#fileContexts.entries()) {
|
|
117
|
+
if (bundler.invalidate(normalizedFiles)) {
|
|
118
|
+
entries ??= [];
|
|
119
|
+
entries.push(entry);
|
|
120
|
+
}
|
|
95
121
|
}
|
|
96
122
|
for (const bundler of this.#inlineContexts.values()) {
|
|
97
123
|
bundler.invalidate(normalizedFiles);
|
|
98
124
|
}
|
|
125
|
+
return entries;
|
|
99
126
|
}
|
|
100
127
|
async dispose() {
|
|
101
128
|
const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()];
|
|
@@ -103,7 +130,7 @@ class ComponentStylesheetBundler {
|
|
|
103
130
|
this.#inlineContexts.clear();
|
|
104
131
|
await Promise.allSettled(contexts.map((context) => context.dispose()));
|
|
105
132
|
}
|
|
106
|
-
extractResult(result, referencedFiles) {
|
|
133
|
+
extractResult(result, referencedFiles, external) {
|
|
107
134
|
let contents = '';
|
|
108
135
|
let metafile;
|
|
109
136
|
const outputFiles = [];
|
|
@@ -122,7 +149,15 @@ class ComponentStylesheetBundler {
|
|
|
122
149
|
outputFiles.push(clonedOutputFile);
|
|
123
150
|
}
|
|
124
151
|
else if (filename.endsWith('.css')) {
|
|
125
|
-
|
|
152
|
+
if (external) {
|
|
153
|
+
const clonedOutputFile = outputFile.clone();
|
|
154
|
+
clonedOutputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path);
|
|
155
|
+
outputFiles.push(clonedOutputFile);
|
|
156
|
+
contents = node_path_1.default.posix.join(this.options.publicPath ?? '', filename);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
contents = outputFile.text;
|
|
160
|
+
}
|
|
126
161
|
}
|
|
127
162
|
else {
|
|
128
163
|
throw new Error(`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`);
|
|
@@ -13,3 +13,4 @@ export declare function createBrowserCodeBundleOptions(options: NormalizedApplic
|
|
|
13
13
|
export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | BundlerOptionsFactory | undefined;
|
|
14
14
|
export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BundlerOptionsFactory | undefined;
|
|
15
15
|
export declare function createServerMainCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;
|
|
16
|
+
export declare function createSsrEntryCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;
|
|
@@ -14,6 +14,7 @@ exports.createBrowserCodeBundleOptions = createBrowserCodeBundleOptions;
|
|
|
14
14
|
exports.createBrowserPolyfillBundleOptions = createBrowserPolyfillBundleOptions;
|
|
15
15
|
exports.createServerPolyfillBundleOptions = createServerPolyfillBundleOptions;
|
|
16
16
|
exports.createServerMainCodeBundleOptions = createServerMainCodeBundleOptions;
|
|
17
|
+
exports.createSsrEntryCodeBundleOptions = createSsrEntryCodeBundleOptions;
|
|
17
18
|
const node_assert_1 = __importDefault(require("node:assert"));
|
|
18
19
|
const node_crypto_1 = require("node:crypto");
|
|
19
20
|
const node_path_1 = require("node:path");
|
|
@@ -156,7 +157,7 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
|
|
|
156
157
|
return () => buildOptions;
|
|
157
158
|
}
|
|
158
159
|
function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
|
|
159
|
-
const { serverEntryPoint: mainServerEntryPoint, workspaceRoot, externalPackages, ssrOptions, polyfills, } = options;
|
|
160
|
+
const { serverEntryPoint: mainServerEntryPoint, workspaceRoot, outputMode, externalPackages, ssrOptions, polyfills, } = options;
|
|
160
161
|
(0, node_assert_1.default)(mainServerEntryPoint, 'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.');
|
|
161
162
|
const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
|
|
162
163
|
const mainServerNamespace = 'angular:main-server';
|
|
@@ -167,7 +168,8 @@ function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
|
|
|
167
168
|
'main.server': mainServerNamespace,
|
|
168
169
|
};
|
|
169
170
|
const ssrEntryPoint = ssrOptions?.entry;
|
|
170
|
-
|
|
171
|
+
const isOldBehaviour = !outputMode;
|
|
172
|
+
if (ssrEntryPoint && isOldBehaviour) {
|
|
171
173
|
// Old behavior: 'server.ts' was bundled together with the SSR (Server-Side Rendering) code.
|
|
172
174
|
// This approach combined server-side logic and rendering into a single bundle.
|
|
173
175
|
entryPoints['server'] = ssrEntryPoint;
|
|
@@ -249,6 +251,111 @@ function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
|
|
|
249
251
|
}
|
|
250
252
|
return buildOptions;
|
|
251
253
|
}
|
|
254
|
+
function createSsrEntryCodeBundleOptions(options, target, sourceFileCache) {
|
|
255
|
+
const { workspaceRoot, ssrOptions, externalPackages } = options;
|
|
256
|
+
const serverEntryPoint = ssrOptions?.entry;
|
|
257
|
+
(0, node_assert_1.default)(serverEntryPoint, 'createSsrEntryCodeBundleOptions should not be called without a defined serverEntryPoint.');
|
|
258
|
+
const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
|
|
259
|
+
const ssrEntryNamespace = 'angular:ssr-entry';
|
|
260
|
+
const ssrInjectManifestNamespace = 'angular:ssr-entry-inject-manifest';
|
|
261
|
+
const ssrInjectRequireNamespace = 'angular:ssr-entry-inject-require';
|
|
262
|
+
const buildOptions = {
|
|
263
|
+
...getEsBuildServerCommonOptions(options),
|
|
264
|
+
target,
|
|
265
|
+
entryPoints: {
|
|
266
|
+
// TODO: consider renaming to index
|
|
267
|
+
'server': ssrEntryNamespace,
|
|
268
|
+
},
|
|
269
|
+
supported: (0, utils_1.getFeatureSupport)(target, true),
|
|
270
|
+
plugins: [
|
|
271
|
+
(0, sourcemap_ignorelist_plugin_1.createSourcemapIgnorelistPlugin)(),
|
|
272
|
+
(0, compiler_plugin_1.createCompilerPlugin)(
|
|
273
|
+
// JS/TS options
|
|
274
|
+
{ ...pluginOptions, noopTypeScriptCompilation: true },
|
|
275
|
+
// Component stylesheet options
|
|
276
|
+
styleOptions),
|
|
277
|
+
],
|
|
278
|
+
inject: [ssrInjectRequireNamespace, ssrInjectManifestNamespace],
|
|
279
|
+
};
|
|
280
|
+
buildOptions.plugins ??= [];
|
|
281
|
+
if (externalPackages) {
|
|
282
|
+
buildOptions.packages = 'external';
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
buildOptions.plugins.push((0, rxjs_esm_resolution_plugin_1.createRxjsEsmResolutionPlugin)());
|
|
286
|
+
}
|
|
287
|
+
// Mark manifest file as external. As this will be generated later on.
|
|
288
|
+
(buildOptions.external ??= []).push('*/main.server.mjs', ...utils_1.SERVER_GENERATED_EXTERNALS);
|
|
289
|
+
buildOptions.plugins.push({
|
|
290
|
+
name: 'angular-ssr-metadata',
|
|
291
|
+
setup(build) {
|
|
292
|
+
build.onEnd((result) => {
|
|
293
|
+
if (result.metafile) {
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
|
+
result.metafile['ng-ssr-entry-bundle'] = true;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
}, (0, virtual_module_plugin_1.createVirtualModulePlugin)({
|
|
300
|
+
namespace: ssrInjectRequireNamespace,
|
|
301
|
+
cache: sourceFileCache?.loadResultCache,
|
|
302
|
+
loadContent: () => {
|
|
303
|
+
const contents = [
|
|
304
|
+
// Note: Needed as esbuild does not provide require shims / proxy from ESModules.
|
|
305
|
+
// See: https://github.com/evanw/esbuild/issues/1921.
|
|
306
|
+
`import { createRequire } from 'node:module';`,
|
|
307
|
+
`globalThis['require'] ??= createRequire(import.meta.url);`,
|
|
308
|
+
];
|
|
309
|
+
return {
|
|
310
|
+
contents: contents.join('\n'),
|
|
311
|
+
loader: 'js',
|
|
312
|
+
resolveDir: workspaceRoot,
|
|
313
|
+
};
|
|
314
|
+
},
|
|
315
|
+
}), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
|
|
316
|
+
namespace: ssrInjectManifestNamespace,
|
|
317
|
+
cache: sourceFileCache?.loadResultCache,
|
|
318
|
+
loadContent: () => {
|
|
319
|
+
const contents = [
|
|
320
|
+
// Configure `@angular/ssr` app engine manifest.
|
|
321
|
+
`import manifest from './${manifest_1.SERVER_APP_ENGINE_MANIFEST_FILENAME}';`,
|
|
322
|
+
`import { ɵsetAngularAppEngineManifest } from '@angular/ssr';`,
|
|
323
|
+
`ɵsetAngularAppEngineManifest(manifest);`,
|
|
324
|
+
];
|
|
325
|
+
return {
|
|
326
|
+
contents: contents.join('\n'),
|
|
327
|
+
loader: 'js',
|
|
328
|
+
resolveDir: workspaceRoot,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
}), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
|
|
332
|
+
namespace: ssrEntryNamespace,
|
|
333
|
+
cache: sourceFileCache?.loadResultCache,
|
|
334
|
+
loadContent: () => {
|
|
335
|
+
const serverEntryPointJsImport = entryFileToWorkspaceRelative(workspaceRoot, serverEntryPoint);
|
|
336
|
+
const contents = [
|
|
337
|
+
// Re-export all symbols including default export
|
|
338
|
+
`import * as server from '${serverEntryPointJsImport}';`,
|
|
339
|
+
`export * from '${serverEntryPointJsImport}';`,
|
|
340
|
+
// The below is needed to avoid
|
|
341
|
+
// `Import "default" will always be undefined because there is no matching export` warning when no default is present.
|
|
342
|
+
`const defaultExportName = 'default';`,
|
|
343
|
+
`export default server[defaultExportName]`,
|
|
344
|
+
// Add @angular/ssr exports
|
|
345
|
+
`export { AngularAppEngine } from '@angular/ssr';`,
|
|
346
|
+
];
|
|
347
|
+
return {
|
|
348
|
+
contents: contents.join('\n'),
|
|
349
|
+
loader: 'js',
|
|
350
|
+
resolveDir: workspaceRoot,
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
}));
|
|
354
|
+
if (options.plugins) {
|
|
355
|
+
buildOptions.plugins.push(...options.plugins);
|
|
356
|
+
}
|
|
357
|
+
return buildOptions;
|
|
358
|
+
}
|
|
252
359
|
function getEsBuildServerCommonOptions(options) {
|
|
253
360
|
return {
|
|
254
361
|
...getEsBuildCommonOptions(options),
|
|
@@ -28,7 +28,7 @@ function generateBudgetStats(metafile, outputFiles, initialFiles) {
|
|
|
28
28
|
continue;
|
|
29
29
|
}
|
|
30
30
|
// Exclude server bundles
|
|
31
|
-
if (type === bundler_context_1.BuildOutputFileType.
|
|
31
|
+
if (type === bundler_context_1.BuildOutputFileType.ServerApplication || type === bundler_context_1.BuildOutputFileType.ServerRoot) {
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
const initialRecord = initialFiles.get(file);
|
|
@@ -31,9 +31,10 @@ export interface InitialFileRecord {
|
|
|
31
31
|
depth: number;
|
|
32
32
|
}
|
|
33
33
|
export declare enum BuildOutputFileType {
|
|
34
|
-
Browser =
|
|
35
|
-
Media =
|
|
36
|
-
|
|
34
|
+
Browser = 0,
|
|
35
|
+
Media = 1,
|
|
36
|
+
ServerApplication = 2,
|
|
37
|
+
ServerRoot = 3,
|
|
37
38
|
Root = 4
|
|
38
39
|
}
|
|
39
40
|
export interface BuildOutputFile extends OutputFile {
|
|
@@ -18,9 +18,10 @@ const load_result_cache_1 = require("./load-result-cache");
|
|
|
18
18
|
const utils_1 = require("./utils");
|
|
19
19
|
var BuildOutputFileType;
|
|
20
20
|
(function (BuildOutputFileType) {
|
|
21
|
-
BuildOutputFileType[BuildOutputFileType["Browser"] =
|
|
22
|
-
BuildOutputFileType[BuildOutputFileType["Media"] =
|
|
23
|
-
BuildOutputFileType[BuildOutputFileType["
|
|
21
|
+
BuildOutputFileType[BuildOutputFileType["Browser"] = 0] = "Browser";
|
|
22
|
+
BuildOutputFileType[BuildOutputFileType["Media"] = 1] = "Media";
|
|
23
|
+
BuildOutputFileType[BuildOutputFileType["ServerApplication"] = 2] = "ServerApplication";
|
|
24
|
+
BuildOutputFileType[BuildOutputFileType["ServerRoot"] = 3] = "ServerRoot";
|
|
24
25
|
BuildOutputFileType[BuildOutputFileType["Root"] = 4] = "Root";
|
|
25
26
|
})(BuildOutputFileType || (exports.BuildOutputFileType = BuildOutputFileType = {}));
|
|
26
27
|
/**
|
|
@@ -292,7 +293,10 @@ class BundlerContext {
|
|
|
292
293
|
fileType = BuildOutputFileType.Media;
|
|
293
294
|
}
|
|
294
295
|
else if (this.#platformIsServer) {
|
|
295
|
-
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
|
+
fileType = result.metafile['ng-ssr-entry-bundle']
|
|
298
|
+
? BuildOutputFileType.ServerRoot
|
|
299
|
+
: BuildOutputFileType.ServerApplication;
|
|
296
300
|
}
|
|
297
301
|
else {
|
|
298
302
|
fileType = BuildOutputFileType.Browser;
|
|
@@ -24,6 +24,9 @@ export interface ExternalResultMetadata {
|
|
|
24
24
|
implicitServer: string[];
|
|
25
25
|
explicit: string[];
|
|
26
26
|
}
|
|
27
|
+
export type PrerenderedRoutesRecord = Record<string, {
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
}>;
|
|
27
30
|
/**
|
|
28
31
|
* Represents the result of a single builder execute call.
|
|
29
32
|
*/
|
|
@@ -33,7 +36,7 @@ export declare class ExecutionResult {
|
|
|
33
36
|
outputFiles: BuildOutputFile[];
|
|
34
37
|
assetFiles: BuildOutputAsset[];
|
|
35
38
|
errors: (Message | PartialMessage)[];
|
|
36
|
-
prerenderedRoutes:
|
|
39
|
+
prerenderedRoutes: PrerenderedRoutesRecord;
|
|
37
40
|
warnings: (Message | PartialMessage)[];
|
|
38
41
|
logs: string[];
|
|
39
42
|
externalMetadata?: ExternalResultMetadata;
|
|
@@ -46,7 +49,7 @@ export declare class ExecutionResult {
|
|
|
46
49
|
addLog(value: string): void;
|
|
47
50
|
addError(error: PartialMessage | string): void;
|
|
48
51
|
addErrors(errors: (PartialMessage | string)[]): void;
|
|
49
|
-
addPrerenderedRoutes(routes:
|
|
52
|
+
addPrerenderedRoutes(routes: PrerenderedRoutesRecord): void;
|
|
50
53
|
addWarning(error: PartialMessage | string): void;
|
|
51
54
|
addWarnings(errors: (PartialMessage | string)[]): void;
|
|
52
55
|
/**
|
|
@@ -19,7 +19,7 @@ class ExecutionResult {
|
|
|
19
19
|
outputFiles = [];
|
|
20
20
|
assetFiles = [];
|
|
21
21
|
errors = [];
|
|
22
|
-
prerenderedRoutes =
|
|
22
|
+
prerenderedRoutes = {};
|
|
23
23
|
warnings = [];
|
|
24
24
|
logs = [];
|
|
25
25
|
externalMetadata;
|
|
@@ -53,9 +53,13 @@ class ExecutionResult {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
addPrerenderedRoutes(routes) {
|
|
56
|
-
this.prerenderedRoutes
|
|
56
|
+
Object.assign(this.prerenderedRoutes, routes);
|
|
57
57
|
// Sort the prerendered routes.
|
|
58
|
-
|
|
58
|
+
const sortedObj = {};
|
|
59
|
+
for (const key of Object.keys(this.prerenderedRoutes).sort()) {
|
|
60
|
+
sortedObj[key] = this.prerenderedRoutes[key];
|
|
61
|
+
}
|
|
62
|
+
this.prerenderedRoutes = sortedObj;
|
|
59
63
|
}
|
|
60
64
|
addWarning(error) {
|
|
61
65
|
if (typeof error === 'string') {
|
|
@@ -85,4 +85,9 @@ export declare class MemoryCache<V> extends Cache<V, Map<string, V>> {
|
|
|
85
85
|
* @returns An iterable of all values in the cache.
|
|
86
86
|
*/
|
|
87
87
|
values(): MapIterator<V>;
|
|
88
|
+
/**
|
|
89
|
+
* Provides all the keys/values currently present in the cache instance.
|
|
90
|
+
* @returns An iterable of all key/value pairs in the cache.
|
|
91
|
+
*/
|
|
92
|
+
entries(): MapIterator<[string, V]>;
|
|
88
93
|
}
|
|
@@ -88,5 +88,12 @@ class MemoryCache extends Cache {
|
|
|
88
88
|
values() {
|
|
89
89
|
return this.store.values();
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Provides all the keys/values currently present in the cache instance.
|
|
93
|
+
* @returns An iterable of all key/value pairs in the cache.
|
|
94
|
+
*/
|
|
95
|
+
entries() {
|
|
96
|
+
return this.store.entries();
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
exports.MemoryCache = MemoryCache;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.createCompilerPluginOptions = createCompilerPluginOptions;
|
|
11
11
|
function createCompilerPluginOptions(options, target, sourceFileCache) {
|
|
12
|
-
const { workspaceRoot, optimizationOptions, sourcemapOptions, tsconfig, outputNames, fileReplacements, externalDependencies, preserveSymlinks, stylePreprocessorOptions, advancedOptimizations, inlineStyleLanguage, jit, cacheOptions, tailwindConfiguration, postcssConfiguration, publicPath, } = options;
|
|
12
|
+
const { workspaceRoot, optimizationOptions, sourcemapOptions, tsconfig, outputNames, fileReplacements, externalDependencies, preserveSymlinks, stylePreprocessorOptions, advancedOptimizations, inlineStyleLanguage, jit, cacheOptions, tailwindConfiguration, postcssConfiguration, publicPath, externalRuntimeStyles, instrumentForCoverage, } = options;
|
|
13
13
|
return {
|
|
14
14
|
// JS/TS options
|
|
15
15
|
pluginOptions: {
|
|
@@ -22,6 +22,8 @@ function createCompilerPluginOptions(options, target, sourceFileCache) {
|
|
|
22
22
|
sourceFileCache,
|
|
23
23
|
loadResultCache: sourceFileCache?.loadResultCache,
|
|
24
24
|
incremental: !!options.watch,
|
|
25
|
+
externalRuntimeStyles,
|
|
26
|
+
instrumentForCoverage,
|
|
25
27
|
},
|
|
26
28
|
// Component stylesheet options
|
|
27
29
|
styleOptions: {
|
|
@@ -36,7 +36,8 @@ class I18nInliner {
|
|
|
36
36
|
const files = new Map();
|
|
37
37
|
const pendingMaps = [];
|
|
38
38
|
for (const file of options.outputFiles) {
|
|
39
|
-
if (file.type === bundler_context_1.BuildOutputFileType.Root) {
|
|
39
|
+
if (file.type === bundler_context_1.BuildOutputFileType.Root || file.type === bundler_context_1.BuildOutputFileType.ServerRoot) {
|
|
40
|
+
// Skip also the server entry-point.
|
|
40
41
|
// Skip stats and similar files.
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
@@ -14,6 +14,7 @@ interface JavaScriptTransformRequest {
|
|
|
14
14
|
skipLinker?: boolean;
|
|
15
15
|
sideEffects?: boolean;
|
|
16
16
|
jit: boolean;
|
|
17
|
+
instrumentForCoverage?: boolean;
|
|
17
18
|
}
|
|
18
19
|
export default function transformJavaScript(request: JavaScriptTransformRequest): Promise<unknown>;
|
|
19
20
|
export {};
|
|
@@ -63,8 +63,12 @@ async function transformWithBabel(filename, data, options) {
|
|
|
63
63
|
// @ts-expect-error Import attribute syntax plugin does not currently have type definitions
|
|
64
64
|
const { default: importAttributePlugin } = await Promise.resolve().then(() => __importStar(require('@babel/plugin-syntax-import-attributes')));
|
|
65
65
|
const plugins = [importAttributePlugin];
|
|
66
|
-
|
|
66
|
+
if (options.instrumentForCoverage) {
|
|
67
|
+
const { default: coveragePlugin } = await Promise.resolve().then(() => __importStar(require('../babel/plugins/add-code-coverage.js')));
|
|
68
|
+
plugins.push(coveragePlugin);
|
|
69
|
+
}
|
|
67
70
|
if (shouldLink) {
|
|
71
|
+
// Lazy load the linker plugin only when linking is required
|
|
68
72
|
const linkerPlugin = await createLinkerPlugin(options);
|
|
69
73
|
plugins.push(linkerPlugin);
|
|
70
74
|
}
|
|
@@ -35,7 +35,7 @@ export declare class JavaScriptTransformer {
|
|
|
35
35
|
* @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
|
|
36
36
|
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
|
|
37
37
|
*/
|
|
38
|
-
transformFile(filename: string, skipLinker?: boolean, sideEffects?: boolean): Promise<Uint8Array>;
|
|
38
|
+
transformFile(filename: string, skipLinker?: boolean, sideEffects?: boolean, instrumentForCoverage?: boolean): Promise<Uint8Array>;
|
|
39
39
|
/**
|
|
40
40
|
* Performs JavaScript transformations on the provided data of a file. The file does not need
|
|
41
41
|
* to exist on the filesystem.
|
|
@@ -45,7 +45,7 @@ export declare class JavaScriptTransformer {
|
|
|
45
45
|
* @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
|
|
46
46
|
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
|
|
47
47
|
*/
|
|
48
|
-
transformData(filename: string, data: string, skipLinker: boolean, sideEffects?: boolean): Promise<Uint8Array>;
|
|
48
|
+
transformData(filename: string, data: string, skipLinker: boolean, sideEffects?: boolean, instrumentForCoverage?: boolean): Promise<Uint8Array>;
|
|
49
49
|
/**
|
|
50
50
|
* Stops all active transformation tasks and shuts down all workers.
|
|
51
51
|
* @returns A void promise that resolves when closing is complete.
|
|
@@ -52,7 +52,7 @@ class JavaScriptTransformer {
|
|
|
52
52
|
* @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
|
|
53
53
|
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
|
|
54
54
|
*/
|
|
55
|
-
async transformFile(filename, skipLinker, sideEffects) {
|
|
55
|
+
async transformFile(filename, skipLinker, sideEffects, instrumentForCoverage) {
|
|
56
56
|
const data = await (0, promises_1.readFile)(filename);
|
|
57
57
|
let result;
|
|
58
58
|
let cacheKey;
|
|
@@ -79,6 +79,7 @@ class JavaScriptTransformer {
|
|
|
79
79
|
data,
|
|
80
80
|
skipLinker,
|
|
81
81
|
sideEffects,
|
|
82
|
+
instrumentForCoverage,
|
|
82
83
|
...this.#commonOptions,
|
|
83
84
|
}, {
|
|
84
85
|
// The below is disable as with Yarn PNP this causes build failures with the below message
|
|
@@ -106,10 +107,10 @@ class JavaScriptTransformer {
|
|
|
106
107
|
* @param sideEffects If false, and `advancedOptimizations` is enabled tslib decorators are wrapped.
|
|
107
108
|
* @returns A promise that resolves to a UTF-8 encoded Uint8Array containing the result.
|
|
108
109
|
*/
|
|
109
|
-
async transformData(filename, data, skipLinker, sideEffects) {
|
|
110
|
+
async transformData(filename, data, skipLinker, sideEffects, instrumentForCoverage) {
|
|
110
111
|
// Perform a quick test to determine if the data needs any transformations.
|
|
111
112
|
// This allows directly returning the data without the worker communication overhead.
|
|
112
|
-
if (skipLinker && !this.#commonOptions.advancedOptimizations) {
|
|
113
|
+
if (skipLinker && !this.#commonOptions.advancedOptimizations && !instrumentForCoverage) {
|
|
113
114
|
const keepSourcemap = this.#commonOptions.sourcemap &&
|
|
114
115
|
(!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
|
|
115
116
|
return Buffer.from(keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), 'utf-8');
|
|
@@ -119,6 +120,7 @@ class JavaScriptTransformer {
|
|
|
119
120
|
data,
|
|
120
121
|
skipLinker,
|
|
121
122
|
sideEffects,
|
|
123
|
+
instrumentForCoverage,
|
|
122
124
|
...this.#commonOptions,
|
|
123
125
|
});
|
|
124
126
|
}
|
|
@@ -30,6 +30,7 @@ const node_path_1 = require("node:path");
|
|
|
30
30
|
const node_url_1 = require("node:url");
|
|
31
31
|
const node_zlib_1 = require("node:zlib");
|
|
32
32
|
const semver_1 = require("semver");
|
|
33
|
+
const schema_1 = require("../../builders/application/schema");
|
|
33
34
|
const manifest_1 = require("../../utils/server-rendering/manifest");
|
|
34
35
|
const stats_table_1 = require("../../utils/stats-table");
|
|
35
36
|
const bundler_context_1 = require("./bundler-context");
|
|
@@ -47,7 +48,7 @@ function logBuildStats(metafile, outputFiles, initial, budgetFailures, colors, c
|
|
|
47
48
|
++unchangedCount;
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
50
|
-
const isPlatformServer = type === bundler_context_1.BuildOutputFileType.
|
|
51
|
+
const isPlatformServer = type === bundler_context_1.BuildOutputFileType.ServerApplication || type === bundler_context_1.BuildOutputFileType.ServerRoot;
|
|
51
52
|
if (isPlatformServer && !ssrOutputEnabled) {
|
|
52
53
|
// Only log server build stats when SSR is enabled.
|
|
53
54
|
continue;
|
|
@@ -342,7 +343,7 @@ function getSupportedNodeTargets() {
|
|
|
342
343
|
return SUPPORTED_NODE_VERSIONS.split('||').map((v) => 'node' + (0, semver_1.coerce)(v)?.version);
|
|
343
344
|
}
|
|
344
345
|
async function createJsonBuildManifest(result, normalizedOptions) {
|
|
345
|
-
const { colors: color, outputOptions: { base, server, browser }, ssrOptions, } = normalizedOptions;
|
|
346
|
+
const { colors: color, outputOptions: { base, server, browser }, ssrOptions, outputMode, } = normalizedOptions;
|
|
346
347
|
const { warnings, errors, prerenderedRoutes } = result;
|
|
347
348
|
const manifest = {
|
|
348
349
|
errors: errors.length ? await (0, esbuild_1.formatMessages)(errors, { kind: 'error', color }) : [],
|
|
@@ -350,7 +351,9 @@ async function createJsonBuildManifest(result, normalizedOptions) {
|
|
|
350
351
|
outputPaths: {
|
|
351
352
|
root: (0, node_url_1.pathToFileURL)(base),
|
|
352
353
|
browser: (0, node_url_1.pathToFileURL)((0, node_path_1.join)(base, browser)),
|
|
353
|
-
server:
|
|
354
|
+
server: outputMode !== schema_1.OutputMode.Static && ssrOptions
|
|
355
|
+
? (0, node_url_1.pathToFileURL)((0, node_path_1.join)(base, server))
|
|
356
|
+
: undefined,
|
|
354
357
|
},
|
|
355
358
|
prerenderedRoutes,
|
|
356
359
|
};
|
|
@@ -398,4 +401,5 @@ function getEntryPointName(entryPoint) {
|
|
|
398
401
|
exports.SERVER_GENERATED_EXTERNALS = new Set([
|
|
399
402
|
'./polyfills.server.mjs',
|
|
400
403
|
'./' + manifest_1.SERVER_APP_MANIFEST_FILENAME,
|
|
404
|
+
'./' + manifest_1.SERVER_APP_ENGINE_MANIFEST_FILENAME,
|
|
401
405
|
]);
|
|
@@ -12,7 +12,6 @@ const mrmime_1 = require("mrmime");
|
|
|
12
12
|
const node_path_1 = require("node:path");
|
|
13
13
|
const load_esm_1 = require("../../../utils/load-esm");
|
|
14
14
|
const utils_1 = require("../utils");
|
|
15
|
-
const COMPONENT_REGEX = /%COMP%/g;
|
|
16
15
|
function createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles) {
|
|
17
16
|
return function angularAssetsMiddleware(req, res, next) {
|
|
18
17
|
if (req.url === undefined || res.writableEnded) {
|
|
@@ -76,12 +75,10 @@ function createAngularAssetsMiddleware(server, assets, outputFiles, usedComponen
|
|
|
76
75
|
// Shim the stylesheet if a component ID is provided
|
|
77
76
|
if (componentId.length > 0) {
|
|
78
77
|
// Validate component ID
|
|
79
|
-
if (
|
|
78
|
+
if (/^[_.\-\p{Letter}\d]+-c\d{9}$/u.test(componentId)) {
|
|
80
79
|
(0, load_esm_1.loadEsmModule)('@angular/compiler')
|
|
81
80
|
.then((compilerModule) => {
|
|
82
|
-
const encapsulatedData = compilerModule
|
|
83
|
-
.encapsulateStyle(new TextDecoder().decode(data))
|
|
84
|
-
.replaceAll(COMPONENT_REGEX, componentId);
|
|
81
|
+
const encapsulatedData = compilerModule.encapsulateStyle(new TextDecoder().decode(data), componentId);
|
|
85
82
|
res.setHeader('Content-Type', 'text/css');
|
|
86
83
|
res.setHeader('Cache-Control', 'no-cache');
|
|
87
84
|
res.end(encapsulatedData);
|
|
@@ -9,15 +9,31 @@
|
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.angularHtmlFallbackMiddleware = angularHtmlFallbackMiddleware;
|
|
11
11
|
const utils_1 = require("../utils");
|
|
12
|
+
const ALLOWED_FALLBACK_METHODS = Object.freeze(['GET', 'HEAD']);
|
|
12
13
|
function angularHtmlFallbackMiddleware(req, _res, next) {
|
|
13
14
|
// Similar to how it is handled in vite
|
|
14
15
|
// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
if (!req.method || !ALLOWED_FALLBACK_METHODS.includes(req.method)) {
|
|
17
|
+
// No fallback for unsupported request methods
|
|
18
|
+
next();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (req.url) {
|
|
22
|
+
const mimeType = (0, utils_1.lookupMimeTypeFromRequest)(req.url);
|
|
23
|
+
if (mimeType === 'text/html' || mimeType === 'application/xhtml+xml') {
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.warn(`Request for HTML file "${req.url}" was received but no asset found. Asset may be missing from build.`);
|
|
26
|
+
}
|
|
27
|
+
else if (mimeType) {
|
|
28
|
+
// No fallback for request of asset-like files
|
|
29
|
+
next();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!req.headers.accept ||
|
|
34
|
+
req.headers.accept.includes('text/html') ||
|
|
35
|
+
req.headers.accept.includes('text/*') ||
|
|
36
|
+
req.headers.accept.includes('*/*')) {
|
|
21
37
|
req.url = '/index.html';
|
|
22
38
|
}
|
|
23
39
|
next();
|