@zenithbuild/cli 0.6.17 → 0.7.1

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.
Files changed (64) hide show
  1. package/dist/build/compiler-runtime.d.ts +59 -0
  2. package/dist/build/compiler-runtime.js +277 -0
  3. package/dist/build/expression-rewrites.d.ts +88 -0
  4. package/dist/build/expression-rewrites.js +372 -0
  5. package/dist/build/hoisted-code-transforms.d.ts +44 -0
  6. package/dist/build/hoisted-code-transforms.js +316 -0
  7. package/dist/build/merge-component-ir.d.ts +16 -0
  8. package/dist/build/merge-component-ir.js +257 -0
  9. package/dist/build/page-component-loop.d.ts +92 -0
  10. package/dist/build/page-component-loop.js +257 -0
  11. package/dist/build/page-ir-normalization.d.ts +23 -0
  12. package/dist/build/page-ir-normalization.js +370 -0
  13. package/dist/build/page-loop-metrics.d.ts +100 -0
  14. package/dist/build/page-loop-metrics.js +131 -0
  15. package/dist/build/page-loop-state.d.ts +261 -0
  16. package/dist/build/page-loop-state.js +92 -0
  17. package/dist/build/page-loop.d.ts +33 -0
  18. package/dist/build/page-loop.js +217 -0
  19. package/dist/build/scoped-identifier-rewrite.d.ts +112 -0
  20. package/dist/build/scoped-identifier-rewrite.js +245 -0
  21. package/dist/build/server-script.d.ts +41 -0
  22. package/dist/build/server-script.js +210 -0
  23. package/dist/build/type-declarations.d.ts +16 -0
  24. package/dist/build/type-declarations.js +158 -0
  25. package/dist/build/typescript-expression-utils.d.ts +23 -0
  26. package/dist/build/typescript-expression-utils.js +272 -0
  27. package/dist/build.d.ts +10 -18
  28. package/dist/build.js +74 -2261
  29. package/dist/component-instance-ir.d.ts +2 -2
  30. package/dist/component-instance-ir.js +146 -39
  31. package/dist/component-occurrences.js +63 -15
  32. package/dist/config.d.ts +66 -0
  33. package/dist/config.js +86 -0
  34. package/dist/debug-script.d.ts +1 -0
  35. package/dist/debug-script.js +8 -0
  36. package/dist/dev-build-session.d.ts +23 -0
  37. package/dist/dev-build-session.js +421 -0
  38. package/dist/dev-server.js +256 -54
  39. package/dist/framework-components/Image.zen +316 -0
  40. package/dist/images/materialize.d.ts +17 -0
  41. package/dist/images/materialize.js +200 -0
  42. package/dist/images/payload.d.ts +18 -0
  43. package/dist/images/payload.js +65 -0
  44. package/dist/images/runtime.d.ts +4 -0
  45. package/dist/images/runtime.js +254 -0
  46. package/dist/images/service.d.ts +4 -0
  47. package/dist/images/service.js +302 -0
  48. package/dist/images/shared.d.ts +58 -0
  49. package/dist/images/shared.js +306 -0
  50. package/dist/index.js +2 -17
  51. package/dist/manifest.js +45 -0
  52. package/dist/preview.d.ts +4 -1
  53. package/dist/preview.js +59 -6
  54. package/dist/resolve-components.js +20 -3
  55. package/dist/server-contract.js +3 -2
  56. package/dist/server-script-composition.d.ts +39 -0
  57. package/dist/server-script-composition.js +133 -0
  58. package/dist/startup-profile.d.ts +10 -0
  59. package/dist/startup-profile.js +62 -0
  60. package/dist/toolchain-paths.d.ts +1 -0
  61. package/dist/toolchain-paths.js +31 -0
  62. package/dist/version-check.d.ts +2 -1
  63. package/dist/version-check.js +12 -5
  64. package/package.json +5 -4
@@ -13,11 +13,16 @@
13
13
  import { createServer } from 'node:http';
14
14
  import { existsSync, watch } from 'node:fs';
15
15
  import { readFile, stat } from 'node:fs/promises';
16
+ import { performance } from 'node:perf_hooks';
16
17
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
17
- import { build } from './build.js';
18
+ import { createDevBuildSession } from './dev-build-session.js';
19
+ import { createStartupProfiler } from './startup-profile.js';
18
20
  import { createSilentLogger } from './ui/logger.js';
19
21
  import { readChangeFingerprint } from './dev-watch.js';
20
- import { executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
22
+ import { defaultRouteDenyMessage, executeServerRoute, injectSsrPayload, loadRouteManifest, resolveWithinDist, toStaticFilePath } from './preview.js';
23
+ import { materializeImageMarkup } from './images/materialize.js';
24
+ import { injectImageRuntimePayload } from './images/payload.js';
25
+ import { handleImageRequest } from './images/service.js';
21
26
  import { resolveRequestRoute } from './server/resolve-request-route.js';
22
27
  const MIME_TYPES = {
23
28
  '.html': 'text/html',
@@ -25,8 +30,12 @@ const MIME_TYPES = {
25
30
  '.css': 'text/css',
26
31
  '.json': 'application/json',
27
32
  '.png': 'image/png',
33
+ '.jpeg': 'image/jpeg',
28
34
  '.jpg': 'image/jpeg',
29
- '.svg': 'image/svg+xml'
35
+ '.svg': 'image/svg+xml',
36
+ '.webp': 'image/webp',
37
+ '.avif': 'image/avif',
38
+ '.gif': 'image/gif'
30
39
  };
31
40
  // Note: V0 HMR script injection has been moved to the runtime client.
32
41
  // This server purely hosts the V1 HMR contract endpoints.
@@ -37,8 +46,10 @@ const MIME_TYPES = {
37
46
  * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
38
47
  */
39
48
  export async function createDevServer(options) {
49
+ const startupProfile = createStartupProfiler('cli-dev-server');
40
50
  const { pagesDir, outDir, port = 3000, host = '127.0.0.1', config = {}, logger: providedLogger = null } = options;
41
51
  const logger = providedLogger || createSilentLogger();
52
+ const buildSession = createDevBuildSession({ pagesDir, outDir, config, logger });
42
53
  const resolvedPagesDir = resolve(pagesDir);
43
54
  const resolvedOutDir = resolve(outDir);
44
55
  const resolvedOutDirTmp = resolve(dirname(resolvedOutDir), `${basename(resolvedOutDir)}.tmp`);
@@ -63,10 +74,11 @@ export async function createDevServer(options) {
63
74
  }, 15000);
64
75
  let buildId = 0;
65
76
  let pendingBuildId = 0;
66
- let buildStatus = 'ok'; // 'ok' | 'error' | 'building'
77
+ let buildStatus = 'building'; // 'ok' | 'error' | 'building'
67
78
  let lastBuildMs = Date.now();
68
79
  let durationMs = 0;
69
80
  let buildError = null;
81
+ let initialBuildSettled = false;
70
82
  const traceEnabled = config.devTrace === true || process.env.ZENITH_DEV_TRACE === '1';
71
83
  const verboseLogging = traceEnabled || logger.mode?.logLevel === 'verbose';
72
84
  // Stable dev CSS endpoint points to this backing asset.
@@ -74,6 +86,9 @@ export async function createDevServer(options) {
74
86
  let currentCssHref = '';
75
87
  let currentCssContent = '';
76
88
  let actualPort = port;
89
+ let currentRoutes = [];
90
+ const rebuildDebounceMs = 5;
91
+ const queuedRebuildDebounceMs = 5;
77
92
  function _publicHost() {
78
93
  if (host === '0.0.0.0' || host === '::') {
79
94
  return '127.0.0.1';
@@ -164,6 +179,34 @@ export async function createDevServer(options) {
164
179
  }
165
180
  return secFetchDest === 'empty';
166
181
  }
182
+ function _isBuildSwapReadError(error) {
183
+ const code = typeof error?.code === 'string' ? error.code : '';
184
+ return code === 'ENOENT' || code === 'ENOTDIR';
185
+ }
186
+ function _delay(ms) {
187
+ return new Promise((resolveDelay) => {
188
+ setTimeout(resolveDelay, ms);
189
+ });
190
+ }
191
+ async function _readFileForRequest(filePath, encoding = undefined) {
192
+ const attempts = buildStatus === 'building' ? 200 : 1;
193
+ let lastError = null;
194
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
195
+ try {
196
+ return encoding === undefined
197
+ ? await readFile(filePath)
198
+ : await readFile(filePath, encoding);
199
+ }
200
+ catch (error) {
201
+ lastError = error;
202
+ if (!_isBuildSwapReadError(error) || attempt === attempts - 1) {
203
+ throw error;
204
+ }
205
+ await _delay(50);
206
+ }
207
+ }
208
+ throw lastError;
209
+ }
167
210
  function _buildNotFoundPayload(pathname, category, cause) {
168
211
  const payload = {
169
212
  kind: 'zenith_dev_not_found',
@@ -231,6 +274,10 @@ export async function createDevServer(options) {
231
274
  if (cssAssets.length === 0) {
232
275
  return '';
233
276
  }
277
+ const devStable = cssAssets.find((entry) => entry.endsWith('/styles.dev.css'));
278
+ if (devStable) {
279
+ return devStable;
280
+ }
234
281
  const preferred = cssAssets.find((entry) => /\/styles(\.|\/|$)/.test(entry));
235
282
  return preferred || cssAssets[0];
236
283
  }
@@ -307,6 +354,24 @@ export async function createDevServer(options) {
307
354
  currentCssContent = cssContent;
308
355
  return true;
309
356
  }
357
+ async function _loadRoutesForRequests() {
358
+ if (buildStatus === 'building' && Array.isArray(currentRoutes) && currentRoutes.length > 0) {
359
+ return currentRoutes;
360
+ }
361
+ try {
362
+ const routes = await loadRouteManifest(outDir);
363
+ if (Array.isArray(routes) && routes.length > 0) {
364
+ currentRoutes = routes;
365
+ return routes;
366
+ }
367
+ }
368
+ catch (error) {
369
+ if (!(Array.isArray(currentRoutes) && currentRoutes.length > 0)) {
370
+ throw error;
371
+ }
372
+ }
373
+ return currentRoutes;
374
+ }
310
375
  function _broadcastEvent(type, payload = {}) {
311
376
  const eventBuildId = Number.isInteger(payload.buildId) ? payload.buildId : buildId;
312
377
  const data = JSON.stringify({
@@ -329,22 +394,62 @@ export async function createDevServer(options) {
329
394
  }
330
395
  }
331
396
  }
332
- // Initial build
333
- try {
334
- logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
335
- const initialBuild = await build({ pagesDir, outDir, config, logger });
336
- await _syncCssStateFromBuild(initialBuild, buildId);
337
- if (currentCssHref.length > 0) {
338
- logger.css(`ready (${currentCssHref})`, { onceKey: `css-ready:${buildId}:${currentCssHref}` });
397
+ async function _runInitialBuild() {
398
+ buildStatus = 'building';
399
+ buildError = null;
400
+ const startTime = Date.now();
401
+ startupProfile.emit('initial_build_start', { buildId });
402
+ try {
403
+ logger.build('Initial build (id=0)', { onceKey: 'dev-initial-build' });
404
+ const initialBuild = await buildSession.build();
405
+ const cssReady = await _syncCssStateFromBuild(initialBuild, buildId);
406
+ currentRoutes = await loadRouteManifest(outDir);
407
+ buildStatus = 'ok';
408
+ buildError = null;
409
+ lastBuildMs = Date.now();
410
+ durationMs = lastBuildMs - startTime;
411
+ if (cssReady && currentCssHref.length > 0) {
412
+ logger.css(`ready (${currentCssHref})`, { onceKey: `css-ready:${buildId}:${currentCssHref}` });
413
+ }
414
+ _trace('state_snapshot', {
415
+ status: buildStatus,
416
+ buildId,
417
+ cssHref: currentCssHref,
418
+ durationMs
419
+ });
420
+ startupProfile.emit('initial_build_complete', {
421
+ buildId,
422
+ status: buildStatus,
423
+ durationMs,
424
+ cssReady,
425
+ routes: Array.isArray(currentRoutes) ? currentRoutes.length : 0
426
+ });
427
+ }
428
+ catch (err) {
429
+ buildStatus = 'error';
430
+ buildError = { message: err instanceof Error ? err.message : String(err) };
431
+ lastBuildMs = Date.now();
432
+ durationMs = lastBuildMs - startTime;
433
+ logger.error('initial build failed', {
434
+ hint: 'fix the error and restart dev',
435
+ error: err
436
+ });
437
+ _trace('state_snapshot', {
438
+ status: buildStatus,
439
+ buildId,
440
+ durationMs,
441
+ error: buildError
442
+ });
443
+ startupProfile.emit('initial_build_complete', {
444
+ buildId,
445
+ status: buildStatus,
446
+ durationMs,
447
+ error: buildError?.message || ''
448
+ });
449
+ }
450
+ finally {
451
+ initialBuildSettled = true;
339
452
  }
340
- }
341
- catch (err) {
342
- buildStatus = 'error';
343
- buildError = { message: err instanceof Error ? err.message : String(err) };
344
- logger.error('initial build failed', {
345
- hint: 'fix the error and restart dev',
346
- error: err
347
- });
348
453
  }
349
454
  const server = createServer(async (req, res) => {
350
455
  const requestBase = typeof req.headers.host === 'string' && req.headers.host.length > 0
@@ -460,8 +565,24 @@ export async function createDevServer(options) {
460
565
  res.end(currentCssContent);
461
566
  return;
462
567
  }
568
+ if (pathname === '/_zenith/image') {
569
+ await handleImageRequest(req, res, {
570
+ requestUrl: url,
571
+ projectRoot,
572
+ config: config.images
573
+ });
574
+ return;
575
+ }
463
576
  if (pathname === '/__zenith/route-check') {
464
577
  try {
578
+ if (!initialBuildSettled && buildStatus === 'building') {
579
+ res.writeHead(503, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
580
+ res.end(JSON.stringify({
581
+ error: 'initial_build_pending',
582
+ message: 'initial build still in progress'
583
+ }));
584
+ return;
585
+ }
465
586
  // Security: Require explicitly designated header to prevent public oracle probing
466
587
  if (req.headers['x-zenith-route-check'] !== '1') {
467
588
  res.writeHead(403, { 'Content-Type': 'application/json' });
@@ -481,7 +602,7 @@ export async function createDevServer(options) {
481
602
  res.end(JSON.stringify({ error: 'external_route_evaluation_forbidden' }));
482
603
  return;
483
604
  }
484
- const routes = await loadRouteManifest(outDir);
605
+ const routes = await _loadRoutesForRequests();
485
606
  const resolvedCheck = resolveRequestRoute(targetUrl, routes);
486
607
  if (!resolvedCheck.matched || !resolvedCheck.route) {
487
608
  res.writeHead(404, { 'Content-Type': 'application/json' });
@@ -538,18 +659,48 @@ export async function createDevServer(options) {
538
659
  let resolvedPathFor404 = null;
539
660
  let staticRootFor404 = null;
540
661
  try {
662
+ if (!initialBuildSettled && buildStatus === 'building') {
663
+ const pendingPayload = {
664
+ kind: 'zenith_dev_build_pending',
665
+ requestedPath: pathname,
666
+ buildId,
667
+ buildStatus,
668
+ hint: 'Initial build is still running. Retry shortly or inspect /__zenith_dev/state.'
669
+ };
670
+ if (_looksLikeJsonRequest(req, pathname)) {
671
+ res.writeHead(503, {
672
+ 'Content-Type': 'application/json',
673
+ 'Cache-Control': 'no-store'
674
+ });
675
+ res.end(JSON.stringify(pendingPayload));
676
+ return;
677
+ }
678
+ res.writeHead(503, {
679
+ 'Content-Type': 'text/html; charset=utf-8',
680
+ 'Cache-Control': 'no-store'
681
+ });
682
+ res.end([
683
+ '<!DOCTYPE html>',
684
+ '<html><head><meta charset="utf-8"><title>Zenith Dev Building</title></head>',
685
+ '<body style="font-family: ui-monospace, SFMono-Regular, Menlo, monospace; padding: 20px; background: #101216; color: #e6edf3;">',
686
+ '<h1 style="margin-top:0;">Zenith Dev Building</h1>',
687
+ `<pre style="white-space: pre-wrap; line-height: 1.5;">Requested: ${pathname}\nStatus: initial build running\nHint: ${pendingPayload.hint}</pre>`,
688
+ '</body></html>'
689
+ ].join(''));
690
+ return;
691
+ }
541
692
  const requestExt = extname(pathname);
542
693
  if (requestExt && requestExt !== '.html') {
543
694
  const assetPath = join(outDir, pathname);
544
695
  resolvedPathFor404 = assetPath;
545
696
  staticRootFor404 = outDir;
546
- const asset = await readFile(assetPath);
697
+ const asset = await _readFileForRequest(assetPath);
547
698
  const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
548
699
  res.writeHead(200, { 'Content-Type': mime });
549
700
  res.end(asset);
550
701
  return;
551
702
  }
552
- const routes = await loadRouteManifest(outDir);
703
+ const routes = await _loadRoutesForRequests();
553
704
  const resolved = resolveRequestRoute(url, routes);
554
705
  let filePath = null;
555
706
  if (resolved.matched && resolved.route) {
@@ -611,17 +762,28 @@ export async function createDevServer(options) {
611
762
  if (result && result.kind === 'deny') {
612
763
  const status = Number.isInteger(result.status) ? result.status : 403;
613
764
  res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
614
- res.end(result.message || (status === 401 ? 'Unauthorized' : 'Forbidden'));
765
+ res.end(result.message || defaultRouteDenyMessage(status));
615
766
  return;
616
767
  }
617
768
  if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
618
769
  ssrPayload = result.data;
619
770
  }
620
771
  }
621
- let content = await readFile(filePath, 'utf8');
772
+ let content = await _readFileForRequest(filePath, 'utf8');
773
+ if (resolved.matched && resolved.route?.page_asset) {
774
+ const pageAssetPath = resolveWithinDist(outDir, resolved.route.page_asset);
775
+ content = await materializeImageMarkup({
776
+ html: content,
777
+ pageAssetPath,
778
+ payload: buildSession.getImageRuntimePayload(),
779
+ ssrData: ssrPayload,
780
+ routePathname: resolved.route.path || pathname
781
+ });
782
+ }
622
783
  if (ssrPayload) {
623
784
  content = injectSsrPayload(content, ssrPayload);
624
785
  }
786
+ content = injectImageRuntimePayload(content, buildSession.getImageRuntimePayload());
625
787
  res.writeHead(200, { 'Content-Type': 'text/html' });
626
788
  res.end(content);
627
789
  }
@@ -711,7 +873,8 @@ export async function createDevServer(options) {
711
873
  * Start watching source roots for changes.
712
874
  */
713
875
  function _startWatcher() {
714
- const triggerBuildDrain = (delayMs = 50) => {
876
+ const watcherStartedAt = performance.now();
877
+ const triggerBuildDrain = (delayMs = rebuildDebounceMs) => {
715
878
  if (_buildDebounce !== null) {
716
879
  clearTimeout(_buildDebounce);
717
880
  }
@@ -724,7 +887,8 @@ export async function createDevServer(options) {
724
887
  if (_buildInFlight) {
725
888
  return;
726
889
  }
727
- const changed = Array.from(_queuedFiles).map(_toDisplayPath).sort();
890
+ const changedFiles = Array.from(_queuedFiles);
891
+ const changed = changedFiles.map(_toDisplayPath).sort();
728
892
  if (changed.length === 0) {
729
893
  return;
730
894
  }
@@ -738,9 +902,13 @@ export async function createDevServer(options) {
738
902
  const startTime = Date.now();
739
903
  const previousCssAssetPath = currentCssAssetPath;
740
904
  const previousCssContent = currentCssContent;
905
+ const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
741
906
  try {
742
- const buildResult = await build({ pagesDir, outDir, config, logger });
907
+ const buildResult = await buildSession.build({ changedFiles, logger });
743
908
  const cssReady = await _syncCssStateFromBuild(buildResult, cycleBuildId);
909
+ if (!onlyCss) {
910
+ currentRoutes = await loadRouteManifest(outDir);
911
+ }
744
912
  const cssChanged = cssReady && (currentCssAssetPath !== previousCssAssetPath ||
745
913
  currentCssContent !== previousCssContent);
746
914
  buildId = cycleBuildId;
@@ -768,7 +936,6 @@ export async function createDevServer(options) {
768
936
  logger.hmr(`css_update (buildId=${cycleBuildId})`);
769
937
  _broadcastEvent('css_update', { href: currentCssHref, changedFiles: changed });
770
938
  }
771
- const onlyCss = changed.length > 0 && changed.every((f) => f.endsWith('.css'));
772
939
  if (!onlyCss) {
773
940
  logger.hmr(`reload (buildId=${cycleBuildId})`);
774
941
  _broadcastEvent('reload', { changedFiles: changed });
@@ -804,7 +971,7 @@ export async function createDevServer(options) {
804
971
  finally {
805
972
  _buildInFlight = false;
806
973
  if (_queuedFiles.size > 0) {
807
- triggerBuildDrain(20);
974
+ triggerBuildDrain(queuedRebuildDebounceMs);
808
975
  }
809
976
  }
810
977
  };
@@ -837,35 +1004,70 @@ export async function createDevServer(options) {
837
1004
  // fs.watch recursive may not be supported on this platform/root
838
1005
  }
839
1006
  }
1007
+ startupProfile.emit('watcher_ready', {
1008
+ roots: roots.length,
1009
+ activeWatchers: _watchers.length,
1010
+ durationMs: startupProfile.roundMs(performance.now() - watcherStartedAt)
1011
+ });
840
1012
  }
841
- return new Promise((resolve) => {
842
- server.listen(port, host, () => {
1013
+ const closeServer = () => {
1014
+ clearInterval(sseHeartbeat);
1015
+ for (const watcher of _watchers) {
1016
+ try {
1017
+ watcher.close();
1018
+ }
1019
+ catch {
1020
+ // ignore close errors
1021
+ }
1022
+ }
1023
+ _watchers = [];
1024
+ for (const client of hmrClients) {
1025
+ try {
1026
+ client.end();
1027
+ }
1028
+ catch { }
1029
+ }
1030
+ hmrClients.length = 0;
1031
+ server.close();
1032
+ };
1033
+ return new Promise((resolve, reject) => {
1034
+ let settled = false;
1035
+ server.once('error', (error) => {
1036
+ if (!settled) {
1037
+ settled = true;
1038
+ reject(error);
1039
+ }
1040
+ });
1041
+ server.listen(port, host, async () => {
843
1042
  actualPort = server.address().port;
844
- _startWatcher();
845
- resolve({
846
- server,
1043
+ startupProfile.emit('server_bound', {
1044
+ host: _publicHost(),
847
1045
  port: actualPort,
848
- close: () => {
849
- clearInterval(sseHeartbeat);
850
- for (const watcher of _watchers) {
851
- try {
852
- watcher.close();
853
- }
854
- catch {
855
- // ignore close errors
856
- }
857
- }
858
- _watchers = [];
859
- for (const client of hmrClients) {
860
- try {
861
- client.end();
862
- }
863
- catch { }
864
- }
865
- hmrClients.length = 0;
866
- server.close();
867
- }
1046
+ buildStatus
868
1047
  });
1048
+ _trace('server_bound', {
1049
+ host: _publicHost(),
1050
+ port: actualPort,
1051
+ buildStatus
1052
+ });
1053
+ try {
1054
+ await _runInitialBuild();
1055
+ _startWatcher();
1056
+ if (!settled) {
1057
+ settled = true;
1058
+ resolve({
1059
+ server,
1060
+ port: actualPort,
1061
+ close: closeServer
1062
+ });
1063
+ }
1064
+ }
1065
+ catch (error) {
1066
+ if (!settled) {
1067
+ settled = true;
1068
+ reject(error);
1069
+ }
1070
+ }
869
1071
  });
870
1072
  });
871
1073
  }