@angular/build 19.1.1 → 19.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "19.1.1",
3
+ "version": "19.1.3",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,7 +23,7 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.1901.1",
26
+ "@angular-devkit/architect": "0.1901.3",
27
27
  "@babel/core": "7.26.0",
28
28
  "@babel/helper-annotate-as-pure": "7.25.9",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
@@ -57,7 +57,7 @@
57
57
  "@angular/localize": "^19.0.0",
58
58
  "@angular/platform-server": "^19.0.0",
59
59
  "@angular/service-worker": "^19.0.0",
60
- "@angular/ssr": "^19.1.1",
60
+ "@angular/ssr": "^19.1.3",
61
61
  "less": "^4.2.0",
62
62
  "ng-packagr": "^19.0.0",
63
63
  "postcss": "^8.4.0",
@@ -104,16 +104,5 @@
104
104
  "bugs": {
105
105
  "url": "https://github.com/angular/angular-cli/issues"
106
106
  },
107
- "homepage": "https://github.com/angular/angular-cli",
108
- "dependenciesMeta": {
109
- "esbuild": {
110
- "built": true
111
- },
112
- "puppeteer": {
113
- "built": true
114
- }
115
- },
116
- "pnpm": {
117
- "onlyBuiltDependencies": []
118
- }
107
+ "homepage": "https://github.com/angular/angular-cli"
119
108
  }
@@ -187,28 +187,11 @@ function* emitOutputResults({ outputFiles, assetFiles, errors, warnings, externa
187
187
  };
188
188
  return;
189
189
  }
190
- // Template updates only exist if no other JS changes have occurred
191
- const hasTemplateUpdates = !!templateUpdates?.size;
192
- if (hasTemplateUpdates) {
193
- const updateResult = {
194
- kind: results_1.ResultKind.ComponentUpdate,
195
- updates: Array.from(templateUpdates, ([id, content]) => ({
196
- type: 'template',
197
- id,
198
- content,
199
- })),
200
- };
201
- yield updateResult;
202
- }
203
- // Use an incremental result if previous output information is available
204
- if (rebuildState && changes) {
205
- const { previousAssetsInfo, previousOutputInfo } = rebuildState;
206
- const incrementalResult = {
207
- kind: results_1.ResultKind.Incremental,
190
+ // Use a full result if there is no rebuild state (no prior build result)
191
+ if (!rebuildState || !changes) {
192
+ const result = {
193
+ kind: results_1.ResultKind.Full,
208
194
  warnings: warnings,
209
- added: [],
210
- removed: [],
211
- modified: [],
212
195
  files: {},
213
196
  detail: {
214
197
  externalMetadata,
@@ -217,69 +200,37 @@ function* emitOutputResults({ outputFiles, assetFiles, errors, warnings, externa
217
200
  outputOptions,
218
201
  },
219
202
  };
220
- // Initially assume all previous output files have been removed
221
- const removedOutputFiles = new Map(previousOutputInfo);
222
- for (const file of outputFiles) {
223
- removedOutputFiles.delete(file.path);
224
- // Temporarily ignore JS files until Angular compiler plugin refactor to allow
225
- // bypassing application code bundling for template affecting only changes.
226
- // TODO: Remove once refactor is complete.
227
- if (hasTemplateUpdates && /\.js(?:\.map)?$/.test(file.path)) {
228
- continue;
229
- }
230
- const previousHash = previousOutputInfo.get(file.path)?.hash;
231
- let needFile = false;
232
- if (previousHash === undefined) {
233
- needFile = true;
234
- incrementalResult.added.push(file.path);
235
- }
236
- else if (previousHash !== file.hash) {
237
- needFile = true;
238
- incrementalResult.modified.push(file.path);
239
- }
240
- if (needFile) {
241
- incrementalResult.files[file.path] = {
242
- type: file.type,
243
- contents: file.contents,
244
- origin: 'memory',
245
- hash: file.hash,
246
- };
247
- }
248
- }
249
- // Initially assume all previous assets files have been removed
250
- const removedAssetFiles = new Map(previousAssetsInfo);
251
- for (const { source, destination } of assetFiles) {
252
- removedAssetFiles.delete(source);
253
- if (!previousAssetsInfo.has(source)) {
254
- incrementalResult.added.push(destination);
255
- }
256
- else if (changes.modified.has(source)) {
257
- incrementalResult.modified.push(destination);
258
- }
259
- else {
260
- continue;
261
- }
262
- incrementalResult.files[destination] = {
203
+ for (const file of assetFiles) {
204
+ result.files[file.destination] = {
263
205
  type: bundler_context_1.BuildOutputFileType.Browser,
264
- inputPath: source,
206
+ inputPath: file.source,
265
207
  origin: 'disk',
266
208
  };
267
209
  }
268
- // Include the removed output and asset files
269
- incrementalResult.removed.push(...Array.from(removedOutputFiles, ([file, { type }]) => ({
270
- path: file,
271
- type,
272
- })), ...Array.from(removedAssetFiles.values(), (file) => ({
273
- path: file,
274
- type: bundler_context_1.BuildOutputFileType.Browser,
275
- })));
276
- yield incrementalResult;
210
+ for (const file of outputFiles) {
211
+ result.files[file.path] = {
212
+ type: file.type,
213
+ contents: file.contents,
214
+ origin: 'memory',
215
+ hash: file.hash,
216
+ };
217
+ }
218
+ yield result;
277
219
  return;
278
220
  }
279
- // Otherwise, use a full result
280
- const result = {
281
- kind: results_1.ResultKind.Full,
221
+ // Template updates only exist if no other JS changes have occurred.
222
+ // A full page reload may be required based on the following incremental output change analysis.
223
+ const hasTemplateUpdates = !!templateUpdates?.size;
224
+ // Use an incremental result if previous output information is available
225
+ const { previousAssetsInfo, previousOutputInfo } = rebuildState;
226
+ const incrementalResult = {
227
+ kind: results_1.ResultKind.Incremental,
282
228
  warnings: warnings,
229
+ // Initially attempt to use a background update of files to support component updates.
230
+ background: hasTemplateUpdates,
231
+ added: [],
232
+ removed: [],
233
+ modified: [],
283
234
  files: {},
284
235
  detail: {
285
236
  externalMetadata,
@@ -288,20 +239,74 @@ function* emitOutputResults({ outputFiles, assetFiles, errors, warnings, externa
288
239
  outputOptions,
289
240
  },
290
241
  };
291
- for (const file of assetFiles) {
292
- result.files[file.destination] = {
242
+ // Initially assume all previous output files have been removed
243
+ const removedOutputFiles = new Map(previousOutputInfo);
244
+ for (const file of outputFiles) {
245
+ removedOutputFiles.delete(file.path);
246
+ const previousHash = previousOutputInfo.get(file.path)?.hash;
247
+ let needFile = false;
248
+ if (previousHash === undefined) {
249
+ needFile = true;
250
+ incrementalResult.added.push(file.path);
251
+ }
252
+ else if (previousHash !== file.hash) {
253
+ needFile = true;
254
+ incrementalResult.modified.push(file.path);
255
+ }
256
+ if (needFile) {
257
+ // Updates to non-JS files must signal an update with the dev server
258
+ if (!/(?:\.m?js|\.map)$/.test(file.path)) {
259
+ incrementalResult.background = false;
260
+ }
261
+ incrementalResult.files[file.path] = {
262
+ type: file.type,
263
+ contents: file.contents,
264
+ origin: 'memory',
265
+ hash: file.hash,
266
+ };
267
+ }
268
+ }
269
+ // Initially assume all previous assets files have been removed
270
+ const removedAssetFiles = new Map(previousAssetsInfo);
271
+ for (const { source, destination } of assetFiles) {
272
+ removedAssetFiles.delete(source);
273
+ if (!previousAssetsInfo.has(source)) {
274
+ incrementalResult.added.push(destination);
275
+ incrementalResult.background = false;
276
+ }
277
+ else if (changes.modified.has(source)) {
278
+ incrementalResult.modified.push(destination);
279
+ incrementalResult.background = false;
280
+ }
281
+ else {
282
+ continue;
283
+ }
284
+ incrementalResult.files[destination] = {
293
285
  type: bundler_context_1.BuildOutputFileType.Browser,
294
- inputPath: file.source,
286
+ inputPath: source,
295
287
  origin: 'disk',
296
288
  };
297
289
  }
298
- for (const file of outputFiles) {
299
- result.files[file.path] = {
300
- type: file.type,
301
- contents: file.contents,
302
- origin: 'memory',
303
- hash: file.hash,
290
+ // Include the removed output and asset files
291
+ incrementalResult.removed.push(...Array.from(removedOutputFiles, ([file, { type }]) => ({
292
+ path: file,
293
+ type,
294
+ })), ...Array.from(removedAssetFiles.values(), (file) => ({
295
+ path: file,
296
+ type: bundler_context_1.BuildOutputFileType.Browser,
297
+ })));
298
+ yield incrementalResult;
299
+ // If there are template updates and the incremental update was background only, a component
300
+ // update is possible.
301
+ if (hasTemplateUpdates && incrementalResult.background) {
302
+ const updateResult = {
303
+ kind: results_1.ResultKind.ComponentUpdate,
304
+ updates: Array.from(templateUpdates, ([id, content]) => ({
305
+ type: 'template',
306
+ id,
307
+ content,
308
+ })),
304
309
  };
310
+ yield updateResult;
305
311
  }
306
- yield result;
307
312
  }
@@ -29,6 +29,7 @@ export interface FullResult extends BaseResult {
29
29
  }
30
30
  export interface IncrementalResult extends BaseResult {
31
31
  kind: ResultKind.Incremental;
32
+ background?: boolean;
32
33
  added: string[];
33
34
  removed: {
34
35
  path: string;
@@ -110,6 +110,12 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
110
110
  // This will also replace file-based/inline styles as code if external runtime styles are not enabled.
111
111
  browserOptions.templateUpdates =
112
112
  serverOptions.liveReload && serverOptions.hmr && environment_options_1.useComponentTemplateHmr;
113
+ if (browserOptions.templateUpdates) {
114
+ context.logger.warn('Component HMR has been enabled.\n' +
115
+ 'If you encounter application reload issues, you can manually reload the page to bypass HMR and/or disable this feature with the' +
116
+ ' `--no-hmr` command line option.\n' +
117
+ 'Please consider reporting any issues you encounter here: https://github.com/angular/angular-cli/issues\n');
118
+ }
113
119
  browserOptions.incrementalResults = true;
114
120
  // Setup the prebundling transformer that will be shared across Vite prebundling requests
115
121
  const prebundleTransformer = new internal_1.JavaScriptTransformer(
@@ -166,6 +172,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
166
172
  updates: [],
167
173
  });
168
174
  }
175
+ let needClientUpdate = true;
169
176
  switch (result.kind) {
170
177
  case results_1.ResultKind.Full:
171
178
  if (result.detail?.['htmlIndexPath']) {
@@ -187,15 +194,13 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
187
194
  // The initial build will not yet have a server setup
188
195
  !server);
189
196
  }
190
- // Invalidate SSR module graph to ensure that only new rebuild is used and not stale component updates
191
- if (server && browserOptions.ssr && templateUpdates.size > 0) {
192
- server.moduleGraph.invalidateAll();
193
- }
194
197
  // Clear stale template updates on code rebuilds
195
198
  templateUpdates.clear();
196
199
  break;
197
200
  case results_1.ResultKind.Incremental:
198
201
  (0, node_assert_1.default)(server, 'Builder must provide an initial full build before incremental results.');
202
+ // Background updates should only update server files/options
203
+ needClientUpdate = !result.background;
199
204
  for (const removed of result.removed) {
200
205
  const filePath = '/' + normalizePath(removed.path);
201
206
  generatedFiles.delete(filePath);
@@ -211,13 +216,6 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
211
216
  case results_1.ResultKind.ComponentUpdate:
212
217
  (0, node_assert_1.default)(serverOptions.hmr, 'Component updates are only supported with HMR enabled.');
213
218
  (0, node_assert_1.default)(server, 'Builder must provide an initial full build before component update results.');
214
- // Invalidate SSR module graph to ensure that new component updates are used
215
- // TODO: Use fine-grained invalidation of only the component update modules
216
- if (browserOptions.ssr) {
217
- server.moduleGraph.invalidateAll();
218
- const { ɵresetCompiledComponents } = (await server.ssrLoadModule('/main.server.mjs'));
219
- ɵresetCompiledComponents();
220
- }
221
219
  for (const componentUpdate of result.updates) {
222
220
  if (componentUpdate.type === 'template') {
223
221
  templateUpdates.set(componentUpdate.id, componentUpdate.content);
@@ -262,7 +260,10 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
262
260
  ...[...assetFiles.values()].map(({ source }) => source),
263
261
  ]),
264
262
  ];
265
- await handleUpdate(normalizePath, generatedFiles, assetFiles, server, serverOptions, context.logger, componentStyles);
263
+ const updatedFiles = await invalidateUpdatedFiles(normalizePath, generatedFiles, assetFiles, server);
264
+ if (needClientUpdate) {
265
+ handleUpdate(server, serverOptions, context.logger, componentStyles, updatedFiles);
266
+ }
266
267
  }
267
268
  else {
268
269
  const projectName = context.target?.project;
@@ -335,7 +336,13 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
335
336
  }
336
337
  await new Promise((resolve) => (deferred = resolve));
337
338
  }
338
- async function handleUpdate(normalizePath, generatedFiles, assetFiles, server, serverOptions, logger, componentStyles) {
339
+ /**
340
+ * Invalidates any updated asset or generated files and resets their `updated` state.
341
+ * This function also clears the server application cache when necessary.
342
+ *
343
+ * @returns A list of files that were updated and invalidated.
344
+ */
345
+ async function invalidateUpdatedFiles(normalizePath, generatedFiles, assetFiles, server) {
339
346
  const updatedFiles = [];
340
347
  // Invalidate any updated asset
341
348
  for (const [file, record] of assetFiles) {
@@ -363,13 +370,21 @@ async function handleUpdate(normalizePath, generatedFiles, assetFiles, server, s
363
370
  const updatedModules = server.moduleGraph.getModulesByFile(normalizePath((0, node_path_1.join)(server.config.root, file)));
364
371
  updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m));
365
372
  }
366
- if (!updatedFiles.length) {
367
- return;
368
- }
369
373
  if (destroyAngularServerAppCalled) {
370
374
  // Trigger module evaluation before reload to initiate dependency optimization.
371
375
  await server.ssrLoadModule('/main.server.mjs');
372
376
  }
377
+ return updatedFiles;
378
+ }
379
+ /**
380
+ * Handles updates for the client by sending HMR or full page reload commands
381
+ * based on the updated files. It also ensures proper tracking of component styles and determines if
382
+ * a full reload is needed.
383
+ */
384
+ function handleUpdate(server, serverOptions, logger, componentStyles, updatedFiles) {
385
+ if (!updatedFiles.length) {
386
+ return;
387
+ }
373
388
  if (serverOptions.hmr) {
374
389
  if (updatedFiles.every((f) => f.endsWith('.css'))) {
375
390
  let requiresReload = false;
@@ -593,6 +608,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
593
608
  componentStyles,
594
609
  templateUpdates,
595
610
  ssrMode,
611
+ resetComponentUpdates: () => templateUpdates.clear(),
596
612
  }),
597
613
  (0, plugins_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser),
598
614
  await (0, plugins_1.createAngularSsrTransformPlugin)(serverOptions.workspaceRoot),
@@ -120,7 +120,11 @@ class AotCompilation extends angular_compilation_1.AngularCompilation {
120
120
  relativePath = relativePath.replaceAll('\\', '/');
121
121
  const updateId = encodeURIComponent(`${host.getCanonicalFileName(relativePath)}@${node.name?.text}`);
122
122
  const updateText = angularCompiler.emitHmrUpdateModule(node);
123
- if (updateText === null) {
123
+ // If compiler cannot generate an update for the component, prevent template updates.
124
+ // Also prevent template updates if $localize is directly present which also currently
125
+ // prevents a template update at runtime.
126
+ // TODO: Support localized template update modules and remove this check.
127
+ if (updateText === null || updateText.includes('$localize')) {
124
128
  // Build is needed if a template cannot be updated
125
129
  templateUpdates = undefined;
126
130
  break;
@@ -5,5 +5,5 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
- import type { Connect } from 'vite';
9
- export declare function createAngularComponentMiddleware(templateUpdates: ReadonlyMap<string, string>): Connect.NextHandleFunction;
8
+ import type { Connect, ViteDevServer } from 'vite';
9
+ export declare function createAngularComponentMiddleware(server: ViteDevServer, templateUpdates: ReadonlyMap<string, string>): Connect.NextHandleFunction;
@@ -8,13 +8,15 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.createAngularComponentMiddleware = createAngularComponentMiddleware;
11
+ const utils_1 = require("../utils");
11
12
  const ANGULAR_COMPONENT_PREFIX = '/@ng/component';
12
- function createAngularComponentMiddleware(templateUpdates) {
13
+ function createAngularComponentMiddleware(server, templateUpdates) {
13
14
  return function angularComponentMiddleware(req, res, next) {
14
15
  if (req.url === undefined || res.writableEnded) {
15
16
  return;
16
17
  }
17
- if (!req.url.startsWith(ANGULAR_COMPONENT_PREFIX)) {
18
+ const pathname = (0, utils_1.pathnameWithoutBasePath)(req.url, server.config.base);
19
+ if (!pathname.includes(ANGULAR_COMPONENT_PREFIX)) {
18
20
  next();
19
21
  return;
20
22
  }
@@ -7,4 +7,4 @@
7
7
  */
8
8
  import type { Connect, ViteDevServer } from 'vite';
9
9
  import { AngularMemoryOutputFiles } from '../utils';
10
- export declare function createAngularIndexHtmlMiddleware(server: ViteDevServer, outputFiles: AngularMemoryOutputFiles, indexHtmlTransformer: ((content: string) => Promise<string>) | undefined): Connect.NextHandleFunction;
10
+ export declare function createAngularIndexHtmlMiddleware(server: ViteDevServer, outputFiles: AngularMemoryOutputFiles, resetComponentUpdates: () => void, indexHtmlTransformer: ((content: string) => Promise<string>) | undefined): Connect.NextHandleFunction;
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.createAngularIndexHtmlMiddleware = createAngularIndexHtmlMiddleware;
11
11
  const node_path_1 = require("node:path");
12
12
  const utils_1 = require("../utils");
13
- function createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransformer) {
13
+ function createAngularIndexHtmlMiddleware(server, outputFiles, resetComponentUpdates, indexHtmlTransformer) {
14
14
  return function angularIndexHtmlMiddleware(req, res, next) {
15
15
  if (!req.url) {
16
16
  next();
@@ -29,6 +29,8 @@ function createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransfor
29
29
  next();
30
30
  return;
31
31
  }
32
+ // A request for the index indicates a full page reload request.
33
+ resetComponentUpdates();
32
34
  server
33
35
  .transformIndexHtml(req.url, Buffer.from(rawHtml).toString('utf-8'))
34
36
  .then(async (processedHtml) => {
@@ -35,8 +35,11 @@ async function createAngularMemoryPlugin(options) {
35
35
  // Vite will resolve these these files example:
36
36
  // `file:///@ng/component?c=src%2Fapp%2Fapp.component.ts%40AppComponent&t=1737017253850`
37
37
  const sourcePath = (0, node_url_1.fileURLToPath)(source);
38
- const { root } = (0, node_path_1.parse)(sourcePath);
39
- const sourceWithoutRoot = normalizePath('/' + sourcePath.slice(root.length));
38
+ const sourceWithoutRoot = sourcePath.startsWith(virtualProjectRoot)
39
+ ? normalizePath('/' + (0, node_path_1.relative)(virtualProjectRoot, sourcePath))
40
+ : // TODO: remove once https://github.com/angular/angular/blob/4e6017a9f5cda389c5fbf4f2c1519ce1bba23e11/packages/compiler/src/render3/r3_hmr_compiler.ts#L57
41
+ // is changed from `/@ng` to `./@ng/`
42
+ normalizePath('/' + sourcePath.slice((0, node_path_1.parse)(sourcePath).root.length));
40
43
  if (sourceWithoutRoot.startsWith(ANGULAR_PREFIX)) {
41
44
  const [, query] = source.split('?', 2);
42
45
  return `\0${sourceWithoutRoot}?${query}`;
@@ -38,6 +38,7 @@ interface AngularSetupMiddlewaresPluginOptions {
38
38
  componentStyles: Map<string, ComponentStyleRecord>;
39
39
  templateUpdates: Map<string, string>;
40
40
  ssrMode: ServerSsrMode;
41
+ resetComponentUpdates: () => void;
41
42
  }
42
43
  export declare function createAngularSetupMiddlewaresPlugin(options: AngularSetupMiddlewaresPluginOptions): Plugin;
43
44
  export {};
@@ -46,10 +46,10 @@ function createAngularSetupMiddlewaresPlugin(options) {
46
46
  name: 'vite:angular-setup-middlewares',
47
47
  enforce: 'pre',
48
48
  async configureServer(server) {
49
- const { indexHtmlTransformer, outputFiles, extensionMiddleware, assets, componentStyles, templateUpdates, ssrMode, } = options;
49
+ const { indexHtmlTransformer, outputFiles, extensionMiddleware, assets, componentStyles, templateUpdates, ssrMode, resetComponentUpdates, } = options;
50
50
  // Headers, assets and resources get handled first
51
51
  server.middlewares.use((0, middlewares_1.createAngularHeadersMiddleware)(server));
52
- server.middlewares.use((0, middlewares_1.createAngularComponentMiddleware)(templateUpdates));
52
+ server.middlewares.use((0, middlewares_1.createAngularComponentMiddleware)(server, templateUpdates));
53
53
  server.middlewares.use((0, middlewares_1.createAngularAssetsMiddleware)(server, assets, outputFiles, componentStyles, await createEncapsulateStyle()));
54
54
  extensionMiddleware?.forEach((middleware) => server.middlewares.use(middleware));
55
55
  // Returning a function, installs middleware after the main transform middleware but
@@ -64,7 +64,7 @@ function createAngularSetupMiddlewaresPlugin(options) {
64
64
  server.middlewares.use((0, middlewares_1.createAngularSsrInternalMiddleware)(server, indexHtmlTransformer));
65
65
  }
66
66
  server.middlewares.use(middlewares_1.angularHtmlFallbackMiddleware);
67
- server.middlewares.use((0, middlewares_1.createAngularIndexHtmlMiddleware)(server, outputFiles, indexHtmlTransformer));
67
+ server.middlewares.use((0, middlewares_1.createAngularIndexHtmlMiddleware)(server, outputFiles, resetComponentUpdates, indexHtmlTransformer));
68
68
  };
69
69
  },
70
70
  };
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.normalizeCacheOptions = normalizeCacheOptions;
11
11
  const node_path_1 = require("node:path");
12
12
  /** Version placeholder is replaced during the build process with actual package version */
13
- const VERSION = '19.1.1';
13
+ const VERSION = '19.1.3';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&