@webjsdev/server 0.9.0 → 0.10.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 (102) hide show
  1. package/README.md +4 -1
  2. package/index.d.ts +793 -125
  3. package/index.js +8 -4
  4. package/package.json +3 -3
  5. package/src/action-flash.js +185 -0
  6. package/src/action-params.js +156 -0
  7. package/src/action-route.js +8 -9
  8. package/src/action-seed.js +44 -5
  9. package/src/actions.js +97 -15
  10. package/src/api.js +67 -10
  11. package/src/app-layout.js +133 -0
  12. package/src/asset-hash.js +82 -49
  13. package/src/auth.js +612 -108
  14. package/src/browser-entries.js +2 -17
  15. package/src/build-epoch.js +1 -1
  16. package/src/cache-tags.js +5 -1
  17. package/src/check/rules-actions.js +37 -1
  18. package/src/check/rules-components.js +114 -40
  19. package/src/check/rules-imports.js +175 -4
  20. package/src/check/rules-route-modules.js +200 -0
  21. package/src/check/rules-routes.js +55 -0
  22. package/src/check/rules-routing.js +143 -95
  23. package/src/check/rules-typescript.js +24 -2
  24. package/src/check/rules.js +104 -9
  25. package/src/check/runner-support.js +53 -171
  26. package/src/check/runner.js +22 -6
  27. package/src/client-signals.js +171 -0
  28. package/src/component-scanner.js +22 -16
  29. package/src/context.js +152 -16
  30. package/src/cookie-store.js +269 -0
  31. package/src/core-exports.js +190 -0
  32. package/src/csrf.js +21 -3
  33. package/src/dev/bun-app-source.js +95 -5
  34. package/src/dev/config.js +7 -51
  35. package/src/dev/dev-service-worker.js +48 -0
  36. package/src/dev/esm-hot-hooks.js +373 -0
  37. package/src/dev/handler.js +502 -155
  38. package/src/dev/helpers.js +127 -96
  39. package/src/dev/hot-host.js +33 -6
  40. package/src/dev/poke-state.js +110 -0
  41. package/src/dev/serve.js +177 -129
  42. package/src/dev/server.js +92 -33
  43. package/src/dev/specifier-versions.js +125 -0
  44. package/src/dev/watch-ignore.js +49 -4
  45. package/src/dev/watch-poll.js +78 -0
  46. package/src/dev/watch-tree.js +59 -22
  47. package/src/dev-classify.js +59 -26
  48. package/src/dev-error.js +104 -3
  49. package/src/dev-hot-classify.js +291 -0
  50. package/src/dev-hot-client.js +245 -0
  51. package/src/dev-import.js +22 -5
  52. package/src/dev-overlay.js +26 -0
  53. package/src/dev-ready-probe.js +69 -0
  54. package/src/dev-regenerate.js +2 -2
  55. package/src/dev-reload-hold.js +49 -20
  56. package/src/dev-reload-reason.js +61 -0
  57. package/src/dev-reload-worker.js +62 -13
  58. package/src/dev-secret.js +73 -0
  59. package/src/dev-source-locations-hook.js +4 -1
  60. package/src/dev-source-locations.js +33 -2
  61. package/src/dev.js +0 -1
  62. package/src/env-files.js +124 -0
  63. package/src/file-storage.js +2 -2
  64. package/src/foreign-imports.js +228 -0
  65. package/src/form-action-identity.js +52 -25
  66. package/src/form-dispatch.js +142 -66
  67. package/src/html-cache.js +35 -4
  68. package/src/importmap.js +2 -2
  69. package/src/instrumentation.js +59 -3
  70. package/src/js-scan.js +5 -4
  71. package/src/jwe.js +155 -0
  72. package/src/lazy-components.js +166 -0
  73. package/src/listener-core.js +4 -2
  74. package/src/lit-hints.js +172 -0
  75. package/src/middleware-load.js +248 -0
  76. package/src/module-graph.js +73 -47
  77. package/src/module-side-effect.js +284 -0
  78. package/src/public-files.js +73 -0
  79. package/src/redirect-location.js +19 -0
  80. package/src/redirects.js +9 -1
  81. package/src/route-frontier.js +151 -0
  82. package/src/route-report.js +130 -0
  83. package/src/router.js +120 -13
  84. package/src/session.js +32 -10
  85. package/src/ssr/client-router-flag.js +1 -1
  86. package/src/ssr/document.js +37 -13
  87. package/src/ssr/head.js +78 -23
  88. package/src/ssr/preloads.js +31 -45
  89. package/src/ssr/render.js +241 -55
  90. package/src/ssr/responses.js +51 -4
  91. package/src/ssr/shipped-modules.js +22 -16
  92. package/src/testing.d.ts +71 -2
  93. package/src/testing.js +103 -21
  94. package/src/thenable-params.js +26 -0
  95. package/src/vendor/jspm.js +12 -1
  96. package/src/vendor/pins.js +9 -8
  97. package/src/vendor/scanner.js +14 -9
  98. package/src/vendor.js +5 -3
  99. package/webjs-config.schema.json +1 -6
  100. package/src/component-elision.js +0 -1828
  101. package/src/elision-differential.js +0 -101
  102. package/src/elision-report.js +0 -193
package/src/ssr/head.js CHANGED
@@ -639,28 +639,22 @@ export function wrapHead(opts) {
639
639
  }
640
640
  metaTags.push(`<meta name="viewport" content="${escapeAttr(viewportStr || 'width=device-width,initial-scale=1')}">`);
641
641
 
642
- if (m.themeColor) metaTags.push(`<meta name="theme-color" content="${escapeAttr(m.themeColor)}">`);
642
+ // themeColor: a string, a `{ color, media? }` descriptor, or an array of
643
+ // them (Next's viewport shape): one <meta> per descriptor.
644
+ for (const tc of themeColorList(m.themeColor)) {
645
+ const media = tc.media ? ` media="${escapeAttr(tc.media)}"` : '';
646
+ metaTags.push(`<meta name="theme-color" content="${escapeAttr(tc.color)}"${media}>`);
647
+ }
643
648
  if (m.colorScheme) metaTags.push(`<meta name="color-scheme" content="${escapeAttr(m.colorScheme)}">`);
644
649
 
645
- // robots: { index, follow, googleBot, etc. }
650
+ // robots: a string or Next's object (index, follow, the directive keys, and
651
+ // a nested `googleBot` resolved the same way into <meta name="googlebot">).
646
652
  if (m.robots) {
647
- if (typeof m.robots === 'string') {
648
- metaTags.push(`<meta name="robots" content="${escapeAttr(m.robots)}">`);
649
- } else if (typeof m.robots === 'object') {
650
- const parts = [];
651
- if (m.robots.index === false) parts.push('noindex');
652
- else if (m.robots.index === true) parts.push('index');
653
- if (m.robots.follow === false) parts.push('nofollow');
654
- else if (m.robots.follow === true) parts.push('follow');
655
- if (m.robots.noarchive) parts.push('noarchive');
656
- if (m.robots.nosnippet) parts.push('nosnippet');
657
- if (m.robots.noimageindex) parts.push('noimageindex');
658
- if (parts.length) {
659
- metaTags.push(`<meta name="robots" content="${escapeAttr(parts.join(', '))}">`);
660
- }
661
- if (typeof m.robots.googleBot === 'string') {
662
- metaTags.push(`<meta name="googlebot" content="${escapeAttr(m.robots.googleBot)}">`);
663
- }
653
+ const basic = resolveRobotsValue(m.robots);
654
+ if (basic) metaTags.push(`<meta name="robots" content="${escapeAttr(basic)}">`);
655
+ if (typeof m.robots === 'object') {
656
+ const gb = resolveRobotsValue(m.robots.googleBot);
657
+ if (gb) metaTags.push(`<meta name="googlebot" content="${escapeAttr(gb)}">`);
664
658
  }
665
659
  }
666
660
 
@@ -870,8 +864,8 @@ export function wrapHead(opts) {
870
864
  // above. `vendorPreloadTargets` still excludes core deliberately; this hint
871
865
  // is emitted from the head builder, not the vendor path.
872
866
  //
873
- // Gated on the boot actually shipping something. A fully elided page ships no
874
- // boot module and must not be handed a preload for a runtime it never loads
867
+ // Gated on the boot actually shipping something. A page with no component
868
+ // ships no boot module and must not be handed a preload for a runtime it never loads
875
869
  // (#780), which also keeps it off `global-error.{js,ts}`, whose document is
876
870
  // returned verbatim with no importmap and no boot script.
877
871
  if (opts.moduleUrls.length || lazyEntries) {
@@ -1109,6 +1103,13 @@ export function wrapHead(opts) {
1109
1103
  // conditional line makes the CSP-off document one newline shorter than the
1110
1104
  // CSP-on one, and the framework guarantees the two render identically
1111
1105
  // apart from the nonce itself.
1106
+ //
1107
+ // The dev reload client goes BEFORE the boot script (#1689): module scripts
1108
+ // run in document order, and it installs the hot client (the template
1109
+ // interner core's `html()` reads) that has to exist before the first
1110
+ // component module evaluates. It sits on the boot's own line and the blank
1111
+ // line after the boot is where it used to be, so a production document,
1112
+ // which has no reload client, is unchanged.
1112
1113
  const title = m.title || 'webjs app';
1113
1114
 
1114
1115
  return `<!doctype html>
@@ -1122,10 +1123,64 @@ ${metaTags.join('\n')}
1122
1123
  ${publicEnvShim({ dev: opts.dev, nonce: opts.nonce })}${embedScriptTag({ dev: opts.dev, nonce: opts.nonce })}${clientRouterEnabled() ? '' : `\n<script${n}>window.__WEBJS_CLIENT_ROUTER__=false;</script>`}
1123
1124
  ${importMapTag({ nonce: opts.nonce })}
1124
1125
  ${linkTags.join('\n')}
1125
- ${scriptTags.length ? scriptTags.join('\n') + '\n' : ''}${boot}
1126
- ${reload}
1126
+ ${reload}${scriptTags.length ? scriptTags.join('\n') + '\n' : ''}${boot}
1127
+
1127
1128
  ${suspenseBoot}
1128
1129
  </head>
1129
1130
  <body>
1130
1131
  `;
1131
1132
  }
1133
+
1134
+ /** Next's robot directive keys, in its emit order. */
1135
+ const ROBOTS_KEYS = [
1136
+ 'noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded',
1137
+ 'nositelinkssearchbox', 'unavailable_after', 'max-video-preview', 'max-image-preview', 'max-snippet',
1138
+ ];
1139
+
1140
+ /**
1141
+ * A port of Next.js's `resolveRobotsValue` (resolve-basics.ts): a string
1142
+ * passes through; an object becomes `index` / `noindex`, `follow` /
1143
+ * `nofollow`, then each set directive key (`key` for `true`,
1144
+ * `key:value` otherwise).
1145
+ * @param {unknown} robots
1146
+ * @returns {string | null}
1147
+ */
1148
+ export function resolveRobotsValue(robots) {
1149
+ if (!robots) return null;
1150
+ if (typeof robots === 'string') return robots;
1151
+ if (typeof robots !== 'object') return null;
1152
+ const r = /** @type {Record<string, unknown>} */ (robots);
1153
+ const values = [];
1154
+ if (r.index) values.push('index');
1155
+ else if (typeof r.index === 'boolean') values.push('noindex');
1156
+ if (r.follow) values.push('follow');
1157
+ else if (typeof r.follow === 'boolean') values.push('nofollow');
1158
+ for (const key of ROBOTS_KEYS) {
1159
+ const value = r[key];
1160
+ if (typeof value !== 'undefined' && value !== false) {
1161
+ values.push(typeof value === 'boolean' ? key : `${key}:${value}`);
1162
+ }
1163
+ }
1164
+ return values.length ? values.join(', ') : null;
1165
+ }
1166
+
1167
+ /**
1168
+ * Normalize Next's `themeColor` (a string, a descriptor, or an array of
1169
+ * descriptors) to a list of `{ color, media? }`.
1170
+ * @param {unknown} v
1171
+ * @returns {{ color: string, media?: string }[]}
1172
+ */
1173
+ export function themeColorList(v) {
1174
+ if (!v) return [];
1175
+ if (typeof v === 'string') return [{ color: v }];
1176
+ const list = Array.isArray(v) ? v : [v];
1177
+ /** @type {{ color: string, media?: string }[]} */
1178
+ const out = [];
1179
+ for (const d of list) {
1180
+ if (typeof d === 'string') out.push({ color: d });
1181
+ else if (d && typeof d === 'object' && typeof d.color === 'string') {
1182
+ out.push(typeof d.media === 'string' ? { color: d.color, media: d.media } : { color: d.color });
1183
+ }
1184
+ }
1185
+ return out;
1186
+ }
@@ -31,21 +31,16 @@ export function toUrlPath(file, appDir) {
31
31
  * are NOT preloaded: they're loaded by the IntersectionObserver-based
32
32
  * lazy-loader when the element enters the viewport.
33
33
  *
34
- * Elidable (display-only) components are skipped entirely: their imports
35
- * are stripped from the served source, so preloading their module would
36
- * fetch JS the browser never executes.
37
- *
38
34
  * A tag counts as lazy when its class says so at runtime (`isLazy`) OR its
39
35
  * file declares `static lazy = true` in source (`lazyFiles`, #1524), the same
40
36
  * per-file verdict the served-source rewrite uses, so the two never disagree.
41
37
  *
42
38
  * @param {Set<string>} usedTags
43
39
  * @param {string} appDir
44
- * @param {Set<string>} [elidable] absolute paths of elidable component files
45
40
  * @param {Map<string, string[]>} [lazyFiles] lazy component file -> its tags
46
41
  * @returns {{ eager: string[], lazy: Record<string, string> }}
47
42
  */
48
- export function componentPreloads(usedTags, appDir, elidable, lazyFiles) {
43
+ export function componentPreloads(usedTags, appDir, lazyFiles) {
49
44
  const eager = [];
50
45
  /** @type {Record<string, string>} */
51
46
  const lazy = {};
@@ -55,7 +50,6 @@ export function componentPreloads(usedTags, appDir, elidable, lazyFiles) {
55
50
  try {
56
51
  const abs = fileURLToPath(fileUrl);
57
52
  if (!abs.startsWith(appDir)) continue;
58
- if (elidable && elidable.has(abs)) continue;
59
53
  const url = toUrlPath(abs, appDir);
60
54
  if (isLazy(tag) || (lazyFiles && lazyFiles.has(abs))) {
61
55
  lazy[tag] = url;
@@ -90,13 +84,13 @@ export function componentPreloads(usedTags, appDir, elidable, lazyFiles) {
90
84
  * @param {import('../module-graph.js').ModuleGraph | undefined} graph
91
85
  * @param {string[]} entryFiles absolute paths of the SHIPPED page/layout modules (from `moduleUrls`)
92
86
  * @param {string} appDir
93
- * @param {Set<string>} [elidableComponents] absolute paths to skip in the walk
94
87
  * @param {Set<string>} [serverFiles] the action / server-file index, never preloaded
88
+ * @param {Set<string>} [skip] absolute paths to skip in the walk (the lazy files)
95
89
  * @param {string[]} [precomputedDeps] the transitive closure `pagePreloadHints` already walked for
96
90
  * THESE roots and THIS skip set. Omitted, the walk runs here (a direct caller)
97
91
  * @returns {string[]}
98
92
  */
99
- export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFiles, appDir, serverFiles, elidableComponents, precomputedDeps) {
93
+ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFiles, appDir, serverFiles, skip, precomputedDeps) {
100
94
  const seen = new Set(moduleUrls);
101
95
  const result = [];
102
96
 
@@ -133,10 +127,9 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
133
127
  const abs = resolve(appDir, url.startsWith('/') ? url.slice(1) : url);
134
128
  allEntries.push(abs);
135
129
  }
136
- // Skip elidable components and any subtree reachable only through
137
- // them: their imports are stripped from served source, so the
138
- // browser never fetches these modules.
139
- deps = transitiveDeps(graph, allEntries, appDir, elidableComponents);
130
+ // Skip the lazy components and any subtree reachable only through
131
+ // them: the lazy loader fetches those on first sight, not up front.
132
+ deps = transitiveDeps(graph, allEntries, appDir, skip);
140
133
  }
141
134
  for (const dep of deps) {
142
135
  if (byIndex(dep)) continue;
@@ -153,11 +146,11 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
153
146
  /**
154
147
  * Collect the bare npm vendor specifiers the page's SHIPPED modules import
155
148
  * (#754). The walk ROOTS are the boot's actually-shipped module set: the caller
156
- * passes `entryFiles` = the absolute paths of `moduleUrls` (which already drops
157
- * inert page/layout modules and substitutes an import-only page with its
158
- * components), plus `componentUrls` = the rendered components. From those roots
159
- * it walks the transitive app-graph closure (elidable components and the subtree
160
- * reachable only through them excluded), and collects each reached file's bare
149
+ * passes `entryFiles` = the absolute paths of `moduleUrls` (the components each
150
+ * route module renders, never a route module itself, #1686), plus
151
+ * `componentUrls` = the rendered components. From those roots it walks the
152
+ * transitive app-graph closure (the lazy components and the subtree reachable
153
+ * only through them excluded), and collects each reached file's bare
161
154
  * imports, excluding server files. The specifiers are resolved to `modulepreload`
162
155
  * targets via the vendor importmap; a specifier not in the map (unpinned /
163
156
  * unreached) drops out there.
@@ -166,35 +159,34 @@ export function deduplicatedPreloads(componentUrls, moduleUrls, graph, entryFile
166
159
  * dropped from the boot, whether a dropped page's SSR-only DIRECT vendor import
167
160
  * or its SSR-only RELATIVE HELPER's vendor, is never collected: the dropped
168
161
  * module is not a root, and nothing that ships imports it (pages/layouts are not
169
- * importable). So the canonical SSR-only-dependency pattern (which elision keeps
170
- * off the client) is never preloaded (no over-fetch).
162
+ * importable). So the canonical SSR-only-dependency pattern is never preloaded
163
+ * (no over-fetch).
171
164
  *
172
165
  * @param {import('../module-graph.js').ModuleGraph | undefined} graph
173
166
  * @param {string[]} entryFiles absolute paths of the SHIPPED page/layout modules (from `moduleUrls`)
174
167
  * @param {string[]} componentUrls rendered eager component URL paths
175
168
  * @param {string} appDir
176
- * @param {Set<string>} [elidableComponents]
169
+ * @param {Set<string>} [skip] absolute paths to skip in the walk (the lazy files)
177
170
  * @param {Set<string>} [serverFiles] the action / server-file index (`'use server'`, incl. no-`.server.` files)
178
171
  * @param {string[]} [precomputedDeps] the transitive closure `pagePreloadHints` already walked for
179
172
  * THESE roots and THIS skip set. Omitted, the walk runs here (a direct caller)
180
173
  * @returns {Set<string>}
181
174
  */
182
- export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir, elidableComponents, serverFiles, precomputedDeps) {
175
+ export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir, skip, serverFiles, precomputedDeps) {
183
176
  /** @type {Set<string>} */
184
177
  const specs = new Set();
185
178
  if (!graph) return specs;
186
179
  const bare = bareImports(graph);
187
180
  if (!bare.size) return specs;
188
- // Roots = the SHIPPED page/layout modules (already inert-dropped + import-only
189
- // expanded by the caller) + the rendered components, keyed by the graph's own
190
- // absolute paths. Walk their non-elided transitive closure, unless
181
+ // Roots = the boot's shipped modules + the rendered components, keyed by the
182
+ // graph's own absolute paths. Walk their transitive closure, unless
191
183
  // `pagePreloadHints` already walked it for this render (#1644).
192
184
  const allEntries = [...entryFiles];
193
185
  for (const url of componentUrls) {
194
186
  allEntries.push(resolve(appDir, url.startsWith('/') ? url.slice(1) : url));
195
187
  }
196
188
  const files = new Set(allEntries);
197
- for (const dep of precomputedDeps || transitiveDeps(graph, allEntries, appDir, elidableComponents)) files.add(dep);
189
+ for (const dep of precomputedDeps || transitiveDeps(graph, allEntries, appDir, skip)) files.add(dep);
198
190
  for (const file of files) {
199
191
  // A server file is never served to the browser (its source is an RPC /
200
192
  // throw-at-load stub), so a vendor it imports never ships and must NOT be
@@ -211,28 +203,23 @@ export function reachedVendorSpecifiers(graph, entryFiles, componentUrls, appDir
211
203
  }
212
204
 
213
205
  /**
214
- * The skip set for the preload walks: the elidable components plus, when the
215
- * app has `static lazy` components (#1524), every lazy file. Built once per
216
- * lazy map (a per-build object), since it was rebuilt per render (#1644).
217
- * @type {WeakMap<Map<string, string[]>, WeakMap<Set<string>, Set<string>>>}
206
+ * The skip set for the preload walks: every `static lazy` component file
207
+ * (#1524). Built once per lazy map (a per-build object), since it was rebuilt
208
+ * per render (#1644).
209
+ * @type {WeakMap<Map<string, string[]>, Set<string>>}
218
210
  */
219
211
  const SKIP_MEMO = new WeakMap();
220
- const NO_ELIDABLE = new Set();
221
212
 
222
213
  /**
223
- * @param {Set<string> | undefined} elidable
224
214
  * @param {Map<string, string[]> | undefined} lazyFiles
225
215
  * @returns {Set<string> | undefined}
226
216
  */
227
- function preloadSkipSet(elidable, lazyFiles) {
228
- if (!lazyFiles || !lazyFiles.size) return elidable;
229
- const key = elidable || NO_ELIDABLE;
230
- let byElidable = SKIP_MEMO.get(lazyFiles);
231
- if (!byElidable) { byElidable = new WeakMap(); SKIP_MEMO.set(lazyFiles, byElidable); }
232
- let skip = byElidable.get(key);
217
+ function preloadSkipSet(lazyFiles) {
218
+ if (!lazyFiles || !lazyFiles.size) return undefined;
219
+ let skip = SKIP_MEMO.get(lazyFiles);
233
220
  if (!skip) {
234
- skip = new Set([...(elidable || []), ...lazyFiles.keys()]);
235
- byElidable.set(key, skip);
221
+ skip = new Set(lazyFiles.keys());
222
+ SKIP_MEMO.set(lazyFiles, skip);
236
223
  }
237
224
  return skip;
238
225
  }
@@ -245,8 +232,8 @@ function preloadSkipSet(elidable, lazyFiles) {
245
232
  * walks it once and hands the closure to both.
246
233
  *
247
234
  * The walk roots are the BOOT's actually-shipped module set (`moduleUrls`,
248
- * inert modules already dropped and an import-only page already substituted)
249
- * plus the rendered eager components, NOT the raw route entries: rooting at
235
+ * the component frontier of each route module, #1686) plus the rendered eager
236
+ * components, NOT the raw route entries: rooting at
250
237
  * the raw entries would walk a dropped page's SSR-only subtree and hint a
251
238
  * `modulepreload` for a module nothing that ships imports (#780, the
252
239
  * app-module analog of the #754 vendor over-fetch).
@@ -256,15 +243,14 @@ function preloadSkipSet(elidable, lazyFiles) {
256
243
  * appDir: string,
257
244
  * moduleUrls: string[],
258
245
  * eagerComponents: string[],
259
- * elidableComponents: Set<string> | undefined,
260
246
  * lazyFiles: Map<string, string[]> | undefined,
261
247
  * serverFiles: Set<string> | undefined,
262
248
  * }} input
263
249
  * @returns {{ preloads: string[], vendorSpecifiers: Set<string>, skip: Set<string> | undefined }}
264
250
  */
265
251
  export function pagePreloadHints(input) {
266
- const { graph, appDir, moduleUrls, eagerComponents, elidableComponents, lazyFiles, serverFiles } = input;
267
- const skip = preloadSkipSet(elidableComponents, lazyFiles);
252
+ const { graph, appDir, moduleUrls, eagerComponents, lazyFiles, serverFiles } = input;
253
+ const skip = preloadSkipSet(lazyFiles);
268
254
  const shippedRoots = moduleUrls.map((u) => resolve(appDir, u.startsWith('/') ? u.slice(1) : u));
269
255
  /** @type {string[] | undefined} */
270
256
  let deps;