@angular/ssr 19.0.0-next.6 → 19.0.0-next.7

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/fesm2022/ssr.mjs CHANGED
@@ -134,11 +134,47 @@ function getAngularAppEngineManifest() {
134
134
  * ```js
135
135
  * stripTrailingSlash('path/'); // 'path'
136
136
  * stripTrailingSlash('/path'); // '/path'
137
+ * stripTrailingSlash('/'); // '/'
138
+ * stripTrailingSlash(''); // ''
137
139
  * ```
138
140
  */
139
141
  function stripTrailingSlash(url) {
140
142
  // Check if the last character of the URL is a slash
141
- return url[url.length - 1] === '/' ? url.slice(0, -1) : url;
143
+ return url.length > 1 && url[url.length - 1] === '/' ? url.slice(0, -1) : url;
144
+ }
145
+ /**
146
+ * Removes the leading slash from a URL if it exists.
147
+ *
148
+ * @param url - The URL string from which to remove the leading slash.
149
+ * @returns The URL string without a leading slash.
150
+ *
151
+ * @example
152
+ * ```js
153
+ * stripLeadingSlash('/path'); // 'path'
154
+ * stripLeadingSlash('/path/'); // 'path/'
155
+ * stripLeadingSlash('/'); // '/'
156
+ * stripLeadingSlash(''); // ''
157
+ * ```
158
+ */
159
+ function stripLeadingSlash(url) {
160
+ // Check if the first character of the URL is a slash
161
+ return url.length > 1 && url[0] === '/' ? url.slice(1) : url;
162
+ }
163
+ /**
164
+ * Adds a leading slash to a URL if it does not already have one.
165
+ *
166
+ * @param url - The URL string to which the leading slash will be added.
167
+ * @returns The URL string with a leading slash.
168
+ *
169
+ * @example
170
+ * ```js
171
+ * addLeadingSlash('path'); // '/path'
172
+ * addLeadingSlash('/path'); // '/path'
173
+ * ```
174
+ */
175
+ function addLeadingSlash(url) {
176
+ // Check if the URL already starts with a slash
177
+ return url[0] === '/' ? url : `/${url}`;
142
178
  }
143
179
  /**
144
180
  * Joins URL parts into a single URL string.
@@ -153,11 +189,11 @@ function stripTrailingSlash(url) {
153
189
  * ```js
154
190
  * joinUrlParts('path/', '/to/resource'); // '/path/to/resource'
155
191
  * joinUrlParts('/path/', 'to/resource'); // '/path/to/resource'
192
+ * joinUrlParts('', ''); // '/'
156
193
  * ```
157
194
  */
158
195
  function joinUrlParts(...parts) {
159
- // Initialize an array with an empty string to always add a leading slash
160
- const normalizeParts = [''];
196
+ const normalizeParts = [];
161
197
  for (const part of parts) {
162
198
  if (part === '') {
163
199
  // Skip any empty parts
@@ -174,7 +210,7 @@ function joinUrlParts(...parts) {
174
210
  normalizeParts.push(normalizedPart);
175
211
  }
176
212
  }
177
- return normalizeParts.join('/');
213
+ return addLeadingSlash(normalizeParts.join('/'));
178
214
  }
179
215
  /**
180
216
  * Strips `/index.html` from the end of a URL's path, if present.
@@ -354,8 +390,7 @@ class RouteTree {
354
390
  */
355
391
  insert(route, metadata) {
356
392
  let node = this.root;
357
- const normalizedRoute = stripTrailingSlash(route);
358
- const segments = normalizedRoute.split('/');
393
+ const segments = this.getPathSegments(route);
359
394
  for (const segment of segments) {
360
395
  // Replace parameterized segments (e.g., :id) with a wildcard (*) for matching
361
396
  const normalizedSegment = segment[0] === ':' ? '*' : segment;
@@ -369,7 +404,7 @@ class RouteTree {
369
404
  // At the leaf node, store the full route and its associated metadata
370
405
  node.metadata = {
371
406
  ...metadata,
372
- route: normalizedRoute,
407
+ route: segments.join('/'),
373
408
  };
374
409
  node.insertionIndex = this.insertionIndexCounter++;
375
410
  }
@@ -382,7 +417,7 @@ class RouteTree {
382
417
  * @returns The metadata of the best matching route or `undefined` if no match is found.
383
418
  */
384
419
  match(route) {
385
- const segments = stripTrailingSlash(route).split('/');
420
+ const segments = this.getPathSegments(route);
386
421
  return this.traverseBySegments(segments)?.metadata;
387
422
  }
388
423
  /**
@@ -427,6 +462,15 @@ class RouteTree {
427
462
  yield* this.traverse(childNode);
428
463
  }
429
464
  }
465
+ /**
466
+ * Extracts the path segments from a given route string.
467
+ *
468
+ * @param route - The route string from which to extract segments.
469
+ * @returns An array of path segments.
470
+ */
471
+ getPathSegments(route) {
472
+ return stripTrailingSlash(route).split('/');
473
+ }
430
474
  /**
431
475
  * Recursively traverses the route tree from a given node, attempting to match the remaining route segments.
432
476
  * If the node is a leaf node (no more segments to match) and contains metadata, the node is yielded.
@@ -515,88 +559,87 @@ const VALID_REDIRECT_RESPONSE_CODES = new Set([301, 302, 303, 307, 308]);
515
559
  * and lazy-loaded routes. It yields route metadata for each route and its potential variants.
516
560
  *
517
561
  * @param options - The configuration options for traversing routes.
518
- * @returns An async iterable iterator of route tree node metadata.
562
+ * @returns An async iterable iterator yielding either route tree node metadata or an error object with an error message.
519
563
  */
520
- async function* traverseRoutesConfig({ routes, compiler, parentInjector, parentRoute, serverConfigRouteTree, invokeGetPrerenderParams, }) {
564
+ async function* traverseRoutesConfig(options) {
565
+ const { routes, compiler, parentInjector, parentRoute, serverConfigRouteTree, invokeGetPrerenderParams, includePrerenderFallbackRoutes, } = options;
521
566
  for (const route of routes) {
522
- const { path = '', redirectTo, loadChildren, children } = route;
523
- const currentRoutePath = joinUrlParts(parentRoute, path);
524
- // Get route metadata from the server config route tree, if available
525
- const metadata = {
526
- ...(serverConfigRouteTree
527
- ? getMatchedRouteMetadata(serverConfigRouteTree, currentRoutePath)
528
- : undefined),
529
- route: currentRoutePath,
530
- };
531
- // Handle redirects
532
- if (typeof redirectTo === 'string') {
533
- const redirectToResolved = resolveRedirectTo(currentRoutePath, redirectTo);
534
- if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
535
- throw new Error(`The '${metadata.status}' status code is not a valid redirect response code. ` +
536
- `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`);
567
+ try {
568
+ const { path = '', redirectTo, loadChildren, children } = route;
569
+ const currentRoutePath = joinUrlParts(parentRoute, path);
570
+ // Get route metadata from the server config route tree, if available
571
+ let matchedMetaData;
572
+ if (serverConfigRouteTree) {
573
+ matchedMetaData = serverConfigRouteTree.match(currentRoutePath);
574
+ if (!matchedMetaData) {
575
+ yield {
576
+ error: `The '${currentRoutePath}' route does not match any route defined in the server routing configuration. ` +
577
+ 'Please ensure this route is added to the server routing configuration.',
578
+ };
579
+ continue;
580
+ }
537
581
  }
538
- yield { ...metadata, redirectTo: redirectToResolved };
539
- }
540
- else if (metadata.renderMode === RenderMode.Prerender) {
541
- // Handle SSG routes
542
- yield* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams);
543
- }
544
- else {
545
- yield metadata;
546
- }
547
- // Recursively process child routes
548
- if (children?.length) {
549
- yield* traverseRoutesConfig({
550
- routes: children,
551
- compiler,
552
- parentInjector,
553
- parentRoute: currentRoutePath,
554
- serverConfigRouteTree,
555
- invokeGetPrerenderParams,
556
- });
557
- }
558
- // Load and process lazy-loaded child routes
559
- if (loadChildren) {
560
- const loadedChildRoutes = await _loadChildren(route, compiler, parentInjector).toPromise();
561
- if (loadedChildRoutes) {
562
- const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
582
+ const metadata = {
583
+ ...matchedMetaData,
584
+ route: currentRoutePath,
585
+ };
586
+ // Handle redirects
587
+ if (typeof redirectTo === 'string') {
588
+ const redirectToResolved = resolveRedirectTo(currentRoutePath, redirectTo);
589
+ if (metadata.status && !VALID_REDIRECT_RESPONSE_CODES.has(metadata.status)) {
590
+ yield {
591
+ error: `The '${metadata.status}' status code is not a valid redirect response code. ` +
592
+ `Please use one of the following redirect response codes: ${[...VALID_REDIRECT_RESPONSE_CODES.values()].join(', ')}.`,
593
+ };
594
+ continue;
595
+ }
596
+ yield { ...metadata, redirectTo: redirectToResolved };
597
+ }
598
+ else if (metadata.renderMode === RenderMode.Prerender) {
599
+ // Handle SSG routes
600
+ yield* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
601
+ }
602
+ else {
603
+ yield metadata;
604
+ }
605
+ // Recursively process child routes
606
+ if (children?.length) {
563
607
  yield* traverseRoutesConfig({
564
- routes: childRoutes,
565
- compiler,
566
- parentInjector: injector,
608
+ ...options,
609
+ routes: children,
567
610
  parentRoute: currentRoutePath,
568
- serverConfigRouteTree,
569
- invokeGetPrerenderParams,
570
611
  });
571
612
  }
613
+ // Load and process lazy-loaded child routes
614
+ if (loadChildren) {
615
+ const loadedChildRoutes = await _loadChildren(route, compiler, parentInjector).toPromise();
616
+ if (loadedChildRoutes) {
617
+ const { routes: childRoutes, injector = parentInjector } = loadedChildRoutes;
618
+ yield* traverseRoutesConfig({
619
+ ...options,
620
+ routes: childRoutes,
621
+ parentInjector: injector,
622
+ parentRoute: currentRoutePath,
623
+ });
624
+ }
625
+ }
626
+ }
627
+ catch (error) {
628
+ yield { error: `Error processing route '${route.path}': ${error.message}` };
572
629
  }
573
630
  }
574
631
  }
575
- /**
576
- * Retrieves the matched route metadata from the server configuration route tree.
577
- *
578
- * @param serverConfigRouteTree - The server configuration route tree.
579
- * @param currentRoutePath - The current route path being processed.
580
- * @returns The metadata associated with the matched route.
581
- */
582
- function getMatchedRouteMetadata(serverConfigRouteTree, currentRoutePath) {
583
- const metadata = serverConfigRouteTree.match(currentRoutePath);
584
- if (!metadata) {
585
- throw new Error(`The '${currentRoutePath}' route does not match any route defined in the server routing configuration. ` +
586
- 'Please ensure this route is added to the server routing configuration.');
587
- }
588
- return metadata;
589
- }
590
632
  /**
591
633
  * Handles SSG (Static Site Generation) routes by invoking `getPrerenderParams` and yielding
592
- * all parameterized paths.
634
+ * all parameterized paths, returning any errors encountered.
593
635
  *
594
636
  * @param metadata - The metadata associated with the route tree node.
595
637
  * @param parentInjector - The dependency injection container for the parent route.
596
638
  * @param invokeGetPrerenderParams - A flag indicating whether to invoke the `getPrerenderParams` function.
597
- * @returns An async iterable iterator that yields route tree node metadata for each SSG path.
639
+ * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result.
640
+ * @returns An async iterable iterator that yields route tree node metadata for each SSG path or errors.
598
641
  */
599
- async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams) {
642
+ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParams, includePrerenderFallbackRoutes) {
600
643
  if (metadata.renderMode !== RenderMode.Prerender) {
601
644
  throw new Error(`'handleSSGRoute' was called for a route which rendering mode is not prerender.`);
602
645
  }
@@ -605,30 +648,48 @@ async function* handleSSGRoute(metadata, parentInjector, invokeGetPrerenderParam
605
648
  if ('getPrerenderParams' in meta) {
606
649
  delete meta['getPrerenderParams'];
607
650
  }
608
- if (invokeGetPrerenderParams && URL_PARAMETER_REGEXP.test(currentRoutePath)) {
651
+ if (!URL_PARAMETER_REGEXP.test(currentRoutePath)) {
652
+ // Route has no parameters
653
+ yield {
654
+ ...meta,
655
+ route: currentRoutePath,
656
+ };
657
+ return;
658
+ }
659
+ if (invokeGetPrerenderParams) {
609
660
  if (!getPrerenderParams) {
610
- throw new Error(`The '${currentRoutePath}' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. ` +
611
- `Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
612
- `or specify a different 'renderMode'.`);
661
+ yield {
662
+ error: `The '${currentRoutePath}' route uses prerendering and includes parameters, but 'getPrerenderParams' is missing. ` +
663
+ `Please define 'getPrerenderParams' function for this route in your server routing configuration ` +
664
+ `or specify a different 'renderMode'.`,
665
+ };
666
+ return;
613
667
  }
614
668
  const parameters = await runInInjectionContext(parentInjector, () => getPrerenderParams());
615
- for (const params of parameters) {
616
- const routeWithResolvedParams = currentRoutePath.replace(URL_PARAMETER_REGEXP, (match) => {
617
- const parameterName = match.slice(1);
618
- const value = params[parameterName];
619
- if (typeof value !== 'string') {
620
- throw new Error(`The 'getPrerenderParams' function defined for the '${currentRoutePath}' route ` +
621
- `returned a non-string value for parameter '${parameterName}'. ` +
622
- `Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
623
- 'specified in this route.');
624
- }
625
- return value;
626
- });
627
- yield { ...meta, route: routeWithResolvedParams };
669
+ try {
670
+ for (const params of parameters) {
671
+ const routeWithResolvedParams = currentRoutePath.replace(URL_PARAMETER_REGEXP, (match) => {
672
+ const parameterName = match.slice(1);
673
+ const value = params[parameterName];
674
+ if (typeof value !== 'string') {
675
+ throw new Error(`The 'getPrerenderParams' function defined for the '${currentRoutePath}' route ` +
676
+ `returned a non-string value for parameter '${parameterName}'. ` +
677
+ `Please make sure the 'getPrerenderParams' function returns values for all parameters ` +
678
+ 'specified in this route.');
679
+ }
680
+ return value;
681
+ });
682
+ yield { ...meta, route: routeWithResolvedParams };
683
+ }
684
+ }
685
+ catch (error) {
686
+ yield { error: `${error.message}` };
687
+ return;
628
688
  }
629
689
  }
630
690
  // Handle fallback render modes
631
- if (fallback !== PrerenderFallback.None || !invokeGetPrerenderParams) {
691
+ if (includePrerenderFallbackRoutes &&
692
+ (fallback !== PrerenderFallback.None || !invokeGetPrerenderParams)) {
632
693
  yield {
633
694
  ...meta,
634
695
  route: currentRoutePath,
@@ -661,21 +722,31 @@ function resolveRedirectTo(routePath, redirectTo) {
661
722
  * Builds a server configuration route tree from the given server routes configuration.
662
723
  *
663
724
  * @param serverRoutesConfig - The array of server routes to be used for configuration.
664
- * @returns A `RouteTree` populated with the server routes and their metadata.
725
+
726
+ * @returns An object containing:
727
+ * - `serverConfigRouteTree`: A populated `RouteTree` instance, which organizes the server routes
728
+ * along with their additional metadata.
729
+ * - `errors`: An array of strings that list any errors encountered during the route tree construction
730
+ * process, such as invalid paths.
665
731
  */
666
732
  function buildServerConfigRouteTree(serverRoutesConfig) {
667
733
  const serverConfigRouteTree = new RouteTree();
734
+ const errors = [];
668
735
  for (const { path, ...metadata } of serverRoutesConfig) {
736
+ if (path[0] === '/') {
737
+ errors.push(`Invalid '${path}' route configuration: the path cannot start with a slash.`);
738
+ continue;
739
+ }
669
740
  serverConfigRouteTree.insert(path, metadata);
670
741
  }
671
- return serverConfigRouteTree;
742
+ return { serverConfigRouteTree, errors };
672
743
  }
673
744
  /**
674
745
  * Retrieves routes from the given Angular application.
675
746
  *
676
747
  * This function initializes an Angular platform, bootstraps the application or module,
677
748
  * and retrieves routes from the Angular router configuration. It handles both module-based
678
- * and function-based bootstrapping. It yields the resulting routes as `RouteTreeNodeMetadata` objects.
749
+ * and function-based bootstrapping. It yields the resulting routes as `RouteTreeNodeMetadata` objects or errors.
679
750
  *
680
751
  * @param bootstrap - A function that returns a promise resolving to an `ApplicationRef` or an Angular module to bootstrap.
681
752
  * @param document - The initial HTML document used for server-side rendering.
@@ -684,12 +755,11 @@ function buildServerConfigRouteTree(serverRoutesConfig) {
684
755
  * for ensuring that API requests for relative paths succeed, which is essential for accurate route extraction.
685
756
  * @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
686
757
  * to handle prerendering paths. Defaults to `false`.
687
- * See:
688
- * - https://github.com/angular/angular/blob/d608b857c689d17a7ffa33bbb510301014d24a17/packages/platform-server/src/location.ts#L51
689
- * - https://github.com/angular/angular/blob/6882cc7d9eed26d3caeedca027452367ba25f2b9/packages/platform-server/src/http.ts#L44
690
- * @returns A promise that resolves to an object of type `AngularRouterConfigResult`.
758
+ * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
759
+ *
760
+ * @returns A promise that resolves to an object of type `AngularRouterConfigResult` or errors.
691
761
  */
692
- async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams = false) {
762
+ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams = false, includePrerenderFallbackRoutes = true) {
693
763
  const { protocol, host } = url;
694
764
  // Create and initialize the Angular platform for server-side rendering.
695
765
  const platformRef = createPlatformFactory(platformCore, 'server', [
@@ -717,12 +787,25 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
717
787
  const injector = applicationRef.injector;
718
788
  const router = injector.get(Router);
719
789
  const routesResults = [];
790
+ const errors = [];
791
+ const baseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
792
+ injector.get(PlatformLocation).getBaseHrefFromDOM();
720
793
  if (router.config.length) {
721
794
  const compiler = injector.get(Compiler);
722
795
  const serverRoutesConfig = injector.get(SERVER_ROUTES_CONFIG, null, { optional: true });
723
- const serverConfigRouteTree = serverRoutesConfig
724
- ? buildServerConfigRouteTree(serverRoutesConfig)
725
- : undefined;
796
+ let serverConfigRouteTree;
797
+ if (serverRoutesConfig) {
798
+ const result = buildServerConfigRouteTree(serverRoutesConfig);
799
+ serverConfigRouteTree = result.serverConfigRouteTree;
800
+ errors.push(...result.errors);
801
+ }
802
+ if (errors.length) {
803
+ return {
804
+ baseHref,
805
+ routes: routesResults,
806
+ errors,
807
+ };
808
+ }
726
809
  // Retrieve all routes from the Angular router configuration.
727
810
  const traverseRoutes = traverseRoutesConfig({
728
811
  routes: router.config,
@@ -731,19 +814,24 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
731
814
  parentRoute: '',
732
815
  serverConfigRouteTree,
733
816
  invokeGetPrerenderParams,
817
+ includePrerenderFallbackRoutes,
734
818
  });
735
819
  for await (const result of traverseRoutes) {
736
- routesResults.push(result);
820
+ if ('error' in result) {
821
+ errors.push(result.error);
822
+ }
823
+ else {
824
+ routesResults.push(result);
825
+ }
737
826
  }
738
827
  }
739
828
  else {
740
829
  routesResults.push({ route: '', renderMode: RenderMode.Prerender });
741
830
  }
742
- const baseHref = injector.get(APP_BASE_HREF, null, { optional: true }) ??
743
- injector.get(PlatformLocation).getBaseHrefFromDOM();
744
831
  return {
745
832
  baseHref,
746
833
  routes: routesResults,
834
+ errors,
747
835
  };
748
836
  }
749
837
  finally {
@@ -763,13 +851,17 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke
763
851
  * If not provided, the default manifest is retrieved using `getAngularAppManifest()`.
764
852
  * @param invokeGetPrerenderParams - A boolean flag indicating whether to invoke `getPrerenderParams` for parameterized SSG routes
765
853
  * to handle prerendering paths. Defaults to `false`.
766
- * @returns A promise that resolves to a populated `RouteTree` containing all extracted routes from the Angular application.
854
+ * @param includePrerenderFallbackRoutes - A flag indicating whether to include fallback routes in the result. Defaults to `true`.
855
+ *
856
+ * @returns A promise that resolves to an object containing:
857
+ * - `routeTree`: A populated `RouteTree` containing all extracted routes from the Angular application.
858
+ * - `errors`: An array of strings representing any errors encountered during the route extraction process.
767
859
  */
768
- async function extractRoutesAndCreateRouteTree(url, manifest = getAngularAppManifest(), invokeGetPrerenderParams = false) {
860
+ async function extractRoutesAndCreateRouteTree(url, manifest = getAngularAppManifest(), invokeGetPrerenderParams = false, includePrerenderFallbackRoutes = true) {
769
861
  const routeTree = new RouteTree();
770
862
  const document = await new ServerAssets(manifest).getIndexServerHtml();
771
863
  const bootstrap = await manifest.bootstrap();
772
- const { baseHref, routes } = await getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams);
864
+ const { baseHref, routes, errors } = await getRoutesFromAngularRouterConfig(bootstrap, document, url, invokeGetPrerenderParams, includePrerenderFallbackRoutes);
773
865
  for (const { route, ...metadata } of routes) {
774
866
  if (metadata.redirectTo !== undefined) {
775
867
  metadata.redirectTo = joinUrlParts(baseHref, metadata.redirectTo);
@@ -777,7 +869,10 @@ async function extractRoutesAndCreateRouteTree(url, manifest = getAngularAppMani
777
869
  const fullRoute = joinUrlParts(baseHref, route);
778
870
  routeTree.insert(fullRoute, metadata);
779
871
  }
780
- return routeTree;
872
+ return {
873
+ routeTree,
874
+ errors,
875
+ };
781
876
  }
782
877
 
783
878
  /**
@@ -912,7 +1007,13 @@ class ServerRouter {
912
1007
  // Create and store a new promise for the build process.
913
1008
  // This prevents concurrent builds by re-using the same promise.
914
1009
  ServerRouter.#extractionPromise ??= extractRoutesAndCreateRouteTree(url, manifest)
915
- .then((routeTree) => new ServerRouter(routeTree))
1010
+ .then(({ routeTree, errors }) => {
1011
+ if (errors.length > 0) {
1012
+ throw new Error('Error(s) occurred while extracting routes:\n' +
1013
+ errors.map((error) => `- ${error}`).join('\n'));
1014
+ }
1015
+ return new ServerRouter(routeTree);
1016
+ })
916
1017
  .finally(() => {
917
1018
  ServerRouter.#extractionPromise = undefined;
918
1019
  });