@astryxdesign/cli 0.7.12-canary.f543ec7 → 0.7.12

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 (113) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/api/build/blocks/blocks.d.mts +41 -0
  3. package/api/build/blocks/blocks.mjs +118 -68
  4. package/api/component/__fixtures__/resolutions/modules.txt +3 -3
  5. package/api/docs/_adapter.d.mts +22 -0
  6. package/api/docs/_adapter.mjs +81 -3
  7. package/api/docs/docs.doc.mjs +5 -5
  8. package/api/docs/docs.type.d.mts +21 -0
  9. package/api/docs/docs.type.mjs +11 -0
  10. package/api/docs/list/list.d.mts +5 -1
  11. package/api/docs/list/list.mjs +72 -6
  12. package/api/docs/node/node.mjs +2 -40
  13. package/api/doctor/doctor.d.mts +4 -4
  14. package/api/doctor/doctor.mjs +248 -78
  15. package/api/icon/_authoring.mjs +8 -1
  16. package/api/icon/add/add.mjs +45 -5
  17. package/api/icon/icon.type.d.mts +24 -0
  18. package/api/icon/icon.type.mjs +6 -0
  19. package/api/icon/iconAdd.doc.mjs +1 -1
  20. package/api/icon/map/map.mjs +11 -4
  21. package/api/index.d.mts +1 -0
  22. package/api/index.mjs +1 -0
  23. package/api/integration/add-helpers.d.mts +5 -1
  24. package/api/integration/add-helpers.mjs +1 -1
  25. package/api/render/_adapter.mjs +9 -0
  26. package/api/render/render.doc.mjs +3 -3
  27. package/api/render/render.type.d.mts +1 -1
  28. package/api/render/render.type.mjs +1 -1
  29. package/api/search/ORDERING.md +3 -0
  30. package/api/search/_adapter.d.mts +32 -3
  31. package/api/search/_adapter.mjs +50 -22
  32. package/api/search/_fit-order.d.mts +47 -0
  33. package/api/search/_fit-order.mjs +257 -0
  34. package/api/search/_main-searcher.mjs +15 -5
  35. package/api/search/exportSearchIndex.doc.d.mts +10 -0
  36. package/api/search/exportSearchIndex.doc.mjs +58 -0
  37. package/api/search/rankSearchIndex.doc.d.mts +10 -0
  38. package/api/search/rankSearchIndex.doc.mjs +79 -0
  39. package/api/search/search-index.d.mts +44 -0
  40. package/api/search/search-index.mjs +270 -0
  41. package/api/search/search.type.d.mts +28 -0
  42. package/api/search/search.type.mjs +17 -0
  43. package/api/swizzle/_moduleGraph.d.mts +7 -0
  44. package/api/swizzle/_moduleGraph.mjs +191 -83
  45. package/api/swizzle/copy/copy.mjs +28 -6
  46. package/api/template/skeleton/skeleton.mjs +1 -1
  47. package/api/template/template.mjs +1 -0
  48. package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +1 -1
  49. package/assets/codemods/transforms/v0.1.0/index.mjs +1 -1
  50. package/assets/discover/official-packages.json +16 -0
  51. package/assets/docs/styling-overview.doc.mjs +3 -3
  52. package/assets/docs/tree/icon-sets.doc.mjs +4 -4
  53. package/assets/docs/tree/render-pages.doc.mjs +3 -3
  54. package/assets/docs/tree/tailwind.doc.mjs +41 -1
  55. package/assets/docs/tree/type-scale.doc.mjs +19 -2
  56. package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
  57. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
  58. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
  59. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
  60. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
  61. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
  62. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
  63. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
  64. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
  65. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
  66. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +41 -0
  67. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
  68. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
  69. package/assets/templates/pages/table-filter/page.tsx +15 -23
  70. package/assets/templates/pages/table-grouped/page.tsx +30 -30
  71. package/assets/templates/pages/table-inbox/page.tsx +3 -6
  72. package/assets/templates/themes/butter/butterTheme.ts +1 -1
  73. package/assets/templates/themes/gothic/gothicTheme.ts +16 -0
  74. package/assets/templates/themes/matcha/matchaTheme.ts +1 -1
  75. package/assets/templates/themes/neutral/neutralTheme.ts +6 -0
  76. package/assets/templates/themes/y2k/y2kPaletteRefs.generated.ts +1 -1
  77. package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
  78. package/clients/cli/commands/docs.doc.mjs +5 -1
  79. package/clients/cli/commands/docs.mjs +30 -2
  80. package/clients/cli/commands/experimental/icon.mjs +25 -4
  81. package/clients/cli/commands/experimental-render.doc.mjs +1 -1
  82. package/clients/cli/commands/template.mjs +204 -181
  83. package/clients/cli/lib/resolve-theme.mjs +16 -7
  84. package/foundation/discovery/component-discovery.mjs +1 -1
  85. package/foundation/discovery/hook-discovery.mjs +31 -8
  86. package/foundation/discovery/icon-set-writer.d.mts +21 -5
  87. package/foundation/discovery/icon-set-writer.mjs +114 -13
  88. package/foundation/discovery/template-adapter.mjs +2 -2
  89. package/foundation/doc-compiler/import.d.mts +5 -1
  90. package/foundation/doc-compiler/import.mjs +40 -6
  91. package/foundation/fs/import-url.d.mts +20 -0
  92. package/foundation/fs/import-url.mjs +22 -0
  93. package/foundation/fs/module-loader.d.mts +24 -3
  94. package/foundation/fs/module-loader.mjs +69 -47
  95. package/foundation/render/charts.d.mts +59 -0
  96. package/foundation/render/charts.mjs +216 -0
  97. package/foundation/render/embedded-imports.d.mts +7 -7
  98. package/foundation/render/embedded-imports.mjs +63 -54
  99. package/foundation/render/page-host.d.mts +4 -0
  100. package/foundation/render/page-host.mjs +45 -0
  101. package/foundation/render/page-worker.mjs +350 -57
  102. package/foundation/render/render-page.d.mts +11 -6
  103. package/foundation/render/render-page.mjs +164 -54
  104. package/foundation/render/ts-paths.d.mts +63 -0
  105. package/foundation/render/ts-paths.mjs +138 -0
  106. package/foundation/response/error-codes.doc.mjs +3 -3
  107. package/foundation/response/error-codes.mjs +3 -2
  108. package/foundation/text/levenshtein.d.mts +6 -1
  109. package/foundation/text/levenshtein.mjs +25 -9
  110. package/foundation/text/suggest-order.d.mts +8 -1
  111. package/foundation/text/suggest-order.mjs +72 -6
  112. package/foundation/xle/registry-core.mjs +1 -1
  113. package/package.json +7 -6
@@ -5,17 +5,23 @@
5
5
  * the project (TypeScript, JSX and StyleX compiled in memory) and renders its
6
6
  * default export to static markup with the project's own React.
7
7
  *
8
- * It runs in a worker thread so a page that never finishes rendering can be
9
- * stopped, and so nothing the page prints reaches the CLI's own output.
8
+ * It runs in a worker thread of a small host process (page-host.mjs), so a
9
+ * page that never finishes rendering can be stopped, nothing the page prints
10
+ * reaches the CLI's own output, and a crash of Node itself ends only the host,
11
+ * never the CLI.
10
12
  *
11
13
  * Outside a project, the Astryx packages the page's folder lacks resolve
12
14
  * from the package set the CLI runs from (served-packages.mjs), and React and
13
15
  * StyleX always come from that set, so the page loads one copy of each.
14
16
  *
15
- * @input workerData {entry, root, served}
17
+ * Charts the page imports from recharts draw only after their effects run,
18
+ * so the server render sets them aside and draws them after it with React
19
+ * DOM in a small document (charts.mjs).
20
+ *
21
+ * @input workerData {entry, root, served, withoutSegmenter}
16
22
  * @output one message: {ok: true, markup, css, files, texts, logs} or
17
23
  * {ok: false, error: {code, message, detail?}}
18
- * @position foundation/render — started by render-page.mjs
24
+ * @position foundation/render — started by page-host.mjs
19
25
  */
20
26
 
21
27
  import * as fs from 'node:fs';
@@ -25,16 +31,22 @@ import {pathToFileURL} from 'node:url';
25
31
  import {format} from 'node:util';
26
32
  import {parentPort, workerData} from 'node:worker_threads';
27
33
  import {transformSync} from '@babel/core';
34
+ import {matchPath, readPathsIn, resolvePath} from './ts-paths.mjs';
28
35
  import {createJiti} from 'jiti';
29
36
  import {assetDataUri, isAssetPath} from './assets.mjs';
30
37
  import {SINGLE_COPY, isServedPackage, packageOf} from './served-packages.mjs';
31
38
  import {findNodeModulesPackage} from '../fs/paths.mjs';
39
+ import {CHART_GLOBAL, CHART_ROOTS, chartDocument, chartSize, markLegends} from './charts.mjs';
32
40
 
33
41
  /** @typedef {{level: string, text: string}} PageLog */
34
42
 
35
- /** @type {{entry: string, root: string, served: {dir: string, integrations: string[]} | null}} */
43
+ /** @type {{entry: string, root: string, served: {dir: string, integrations: string[]} | null, withoutSegmenter?: boolean}} */
36
44
  const {entry, root} = workerData;
37
45
 
46
+ // Some Node builds crash in Intl.Segmenter. Without it, Core splits text by
47
+ // code points, so a joined emoji can count as several characters.
48
+ if (workerData.withoutSegmenter) delete (/** @type {any} */ (Intl)).Segmenter;
49
+
38
50
  /** The package set, for a page outside a project whose folder has no Core of its own. */
39
51
  const served = workerData.served && findNodeModulesPackage(path.dirname(entry), '@astryxdesign/core') === null ? workerData.served : null;
40
52
  const ROOT_PREFIX = root.endsWith(path.sep) ? root : root + path.sep;
@@ -136,6 +148,252 @@ const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
136
148
  const isLocal = file =>
137
149
  !file.includes(NODE_MODULES) && (file === entry || file.startsWith(ROOT_PREFIX));
138
150
 
151
+ /** Charts the server render set aside, drawn after it. @type {any[]} */
152
+ const charts = [];
153
+ /** The props each chart was last drawn with in the page-wide draw. @type {any[]} */
154
+ const pageCharts = [];
155
+ /**
156
+ * Whether a chart rendered again after the page-wide draw first committed. A
157
+ * chart renders again only when the component that made it renders again:
158
+ * its effects changed its state, which a closure in the chart may read, so
159
+ * the props alone cannot say the chart is the same. React's development
160
+ * double render under StrictMode runs before the commit, so it does not
161
+ * count.
162
+ */
163
+ let renderedAfterCommit = false;
164
+
165
+ /**
166
+ * A chart's props as a string that two renders of the same chart share:
167
+ * data by value, functions by name, elements by type and props.
168
+ * @param {unknown} value
169
+ * @param {number} [depth]
170
+ * @returns {string}
171
+ */
172
+ function sameKey(value, depth = 0) {
173
+ if (depth > 8) return '…';
174
+ if (typeof value === 'function') return `fn:${value.name}`;
175
+ if (typeof value === 'symbol') return String(value);
176
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'undefined';
177
+ if (Array.isArray(value)) return `[${value.map(v => sameKey(v, depth + 1)).join(',')}]`;
178
+ const record = /** @type {Record<string, unknown>} */ (value);
179
+ if ('$$typeof' in record && 'props' in record) {
180
+ const type = /** @type {any} */ (record.type);
181
+ return `<${typeof type === 'string' ? type : (type?.displayName ?? type?.name ?? '?')} ${sameKey(record.props, depth + 1)}>`;
182
+ }
183
+ return `{${Object.keys(record)
184
+ .filter(key => !key.startsWith('_'))
185
+ .sort()
186
+ .map(key => `${key}:${sameKey(record[key], depth + 1)}`)
187
+ .join(',')}}`;
188
+ }
189
+ /** React's createElement, once the page's React has loaded. @type {any} */
190
+ let createElement = null;
191
+ /** React's useContext and useRef. @type {any} */
192
+ let useContext = null;
193
+ /** @type {any} */
194
+ let useRef = null;
195
+ /** @type {any} */
196
+ let useLayoutEffect = null;
197
+ /** Set inside a drawn chart, so a chart inside it is not wrapped again. @type {any} */
198
+ let InChart = null;
199
+ /**
200
+ * `aside` while the server render sets charts aside; `page` while the whole
201
+ * page is drawn again, so each chart sees the context it was rendered in;
202
+ * `alone` while each chart is drawn by itself, when the page cannot be.
203
+ * @type {'aside' | 'page' | 'alone'}
204
+ */
205
+ let mode = 'aside';
206
+
207
+ /**
208
+ * Wrap a recharts chart, or a ResponsiveContainer, so the server render sets
209
+ * it aside and the drawing pass draws it: a container at the size it starts
210
+ * at, in a box that lets the drawing fit the page's width.
211
+ * @param {any} Chart
212
+ * @param {boolean} container
213
+ */
214
+ /** @type {any} */ (globalThis).__astryxRenderChart = (/** @type {any} */ Chart, /** @type {boolean} */ container) => {
215
+ function RenderedChart(/** @type {any} */ props) {
216
+ if (mode === 'aside') {
217
+ charts.push(createElement(RenderedChart, props));
218
+ return createElement('astryx-chart', {'data-chart': charts.length - 1});
219
+ }
220
+ const inside = useContext(InChart);
221
+ // Each chart drawn in the page notes the props it was last drawn with,
222
+ // by its place in the page, to compare with the first render's.
223
+ const place = useRef(/** @type {number | null} */ (null));
224
+ const committed = useRef(false);
225
+ useLayoutEffect(() => {
226
+ committed.current = true;
227
+ });
228
+ if (mode === 'page' && !inside) {
229
+ place.current ??= pageCharts.length;
230
+ pageCharts[place.current] = props;
231
+ if (committed.current) renderedAfterCommit = true;
232
+ }
233
+ const drawn = container
234
+ ? createElement('div', {className: 'recharts-responsive-container'}, createElement(Chart, {...props, ...chartSize(props)}))
235
+ : createElement(Chart, props);
236
+ if (mode === 'alone' || inside) return drawn;
237
+ return createElement('astryx-chart', null, createElement(InChart.Provider, {value: true}, drawn));
238
+ }
239
+ return RenderedChart;
240
+ };
241
+
242
+ /**
243
+ * Point the page's recharts charts, and its ResponsiveContainer, at the
244
+ * wrapper the worker installs.
245
+ * @param {any} p the import of 'recharts'
246
+ * @param {any} t Babel's types
247
+ */
248
+ function setChartsAside(p, t) {
249
+ for (const s of p.node.specifiers) {
250
+ if (s.type !== 'ImportSpecifier') continue;
251
+ const name = s.imported.name ?? s.imported.value;
252
+ const container = name === 'ResponsiveContainer';
253
+ if (!container && !CHART_ROOTS.has(name)) continue;
254
+ const original = p.scope.generateUidIdentifier(name);
255
+ const local = s.local.name;
256
+ s.local = original;
257
+ p.insertAfter(
258
+ t.variableDeclaration('const', [
259
+ t.variableDeclarator(
260
+ t.identifier(local),
261
+ t.callExpression(t.memberExpression(t.identifier('globalThis'), t.identifier(CHART_GLOBAL)), [
262
+ original,
263
+ t.booleanLiteral(container),
264
+ ]),
265
+ ),
266
+ ]),
267
+ );
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Draw the charts the server render set aside, and put each drawing where
273
+ * its placeholder is.
274
+ *
275
+ * Draw the whole page first. A chart's own parts then see the React context
276
+ * the page gives them. If that draw throws, or the page's effects change a
277
+ * chart (its props, or which charts there are), draw each chart alone, from
278
+ * the props of the first render. A chart that fails stays empty and is
279
+ * counted.
280
+ * @param {string} markup
281
+ * @param {any} Page
282
+ * @returns {Promise<{markup: string, failed: number, alone: false | 'error' | 'changed' | 'rerendered'}>}
283
+ */
284
+ async function drawCharts(markup, Page) {
285
+ const placeholder = /<astryx-chart data-chart="(\d+)"><\/astryx-chart>/g;
286
+ /** @type {any} */
287
+ let client;
288
+ /** @type {any} */
289
+ let flushSync;
290
+ try {
291
+ await chartDocument();
292
+ client = await jiti.import('react-dom/client');
293
+ ({flushSync} = /** @type {any} */ (await jiti.import('react-dom')));
294
+ // recharts decided it runs on a server when it loaded for the server
295
+ // render. Now there is a document, so it measures text and lays out
296
+ // axis labels as in a browser.
297
+ const recharts = /** @type {any} */ (await jiti.import('recharts'));
298
+ if (recharts?.Global) recharts.Global.isSsr = false;
299
+ } catch {
300
+ // Without a document to draw in, every chart stays empty and is counted.
301
+ return {markup: markup.replace(placeholder, ''), failed: charts.length, alone: /** @type {false} */ (false)};
302
+ }
303
+ /**
304
+ * Render one element in its own root and hand its host to `read`.
305
+ * @param {unknown} element
306
+ * @param {(host: any) => string[]} read
307
+ * @returns {string[] | null} null when the render failed
308
+ */
309
+ const drawIn = (element, read) => {
310
+ /** @type {unknown} */
311
+ let error = null;
312
+ const host = /** @type {any} */ (globalThis).document.createElement('div');
313
+ const root = client.createRoot(host, {
314
+ onUncaughtError: (/** @type {unknown} */ e) => {
315
+ error = e;
316
+ },
317
+ onRecoverableError: () => {},
318
+ });
319
+ try {
320
+ flushSync(() => root.render(element));
321
+ } catch (e) {
322
+ error = e;
323
+ }
324
+ if (!error) markLegends(host);
325
+ const out = error ? null : read(host);
326
+ root.unmount();
327
+ return out;
328
+ };
329
+ mode = 'page';
330
+ let drawn = drawIn(createElement(Page), host => [...host.querySelectorAll('astryx-chart')].map(el => el.innerHTML));
331
+ /** @type {false | 'error' | 'changed' | 'rerendered'} */
332
+ let alone = false;
333
+ if (!drawn) alone = 'error';
334
+ // The page's effects changed a chart: its props, which charts there are,
335
+ // or the state of the component that made it, which a closure or context
336
+ // in the chart may read.
337
+ else if (
338
+ drawn.length !== charts.length ||
339
+ pageCharts.length !== charts.length ||
340
+ charts.some((chart, i) => sameKey(chart.props) !== sameKey(pageCharts[i]))
341
+ ) {
342
+ alone = 'changed';
343
+ } else if (renderedAfterCommit) {
344
+ alone = 'rerendered';
345
+ }
346
+ if (alone) {
347
+ mode = 'alone';
348
+ drawn = charts.map(chart => drawIn(chart, host => [host.innerHTML])?.[0] ?? '');
349
+ }
350
+ let failed = 0;
351
+ // A chart that drew no picture counts as failed, once.
352
+ const pictures = /** @type {string[]} */ (drawn).map(html => {
353
+ if (html.includes('<svg')) return html;
354
+ failed += 1;
355
+ return '';
356
+ });
357
+ return {
358
+ markup: markup.replace(placeholder, (_, i) => pictures[Number(i)] ?? ''),
359
+ failed,
360
+ alone,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Babel plugin: a bare import that a path alias places loads from that file,
366
+ * as in TypeScript, before any installed copy. An import the aliases do not
367
+ * place is left alone, so an installed package loads. It runs on the page's
368
+ * own files, before {@link pagePlugin}, and on every module the page loads
369
+ * from outside them and outside `node_modules` (a sibling package's source,
370
+ * say).
371
+ */
372
+ function aliasPlugin() {
373
+ return () => {
374
+ /** @param {any} node a string literal to point at the file an alias places */
375
+ const point = node => {
376
+ if (node?.type !== 'StringLiteral') return;
377
+ const file = resolvePath(PATHS, node.value);
378
+ if (file) node.value = file;
379
+ };
380
+ return {
381
+ visitor: {
382
+ 'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration'(/** @type {any} */ p) {
383
+ point(p.node.source);
384
+ },
385
+ CallExpression(/** @type {any} */ p) {
386
+ const callee = p.node.callee;
387
+ if (callee?.type === 'Import' || (callee?.type === 'Identifier' && callee.name === 'require')) point(p.node.arguments?.[0]);
388
+ },
389
+ ImportExpression(/** @type {any} */ p) {
390
+ point(p.node.source);
391
+ },
392
+ },
393
+ };
394
+ };
395
+ }
396
+
139
397
  /**
140
398
  * Babel plugin: replace asset imports with data: URIs and stylesheet imports
141
399
  * with nothing (their CSS is inlined into the page), and record static text
@@ -148,6 +406,7 @@ function pagePlugin(filename) {
148
406
  visitor: {
149
407
  ImportDeclaration(/** @type {any} */ p) {
150
408
  const spec = /** @type {string} */ (p.node.source.value);
409
+ if (spec === 'recharts') setChartsAside(p, t);
151
410
  const raw = spec.split('?')[0];
152
411
  // A stylesheet, image or JSON file imported through a project alias
153
412
  // (`@/styles/site.css`) resolves the way modules do.
@@ -179,6 +438,23 @@ function pagePlugin(filename) {
179
438
  p.remove();
180
439
  return;
181
440
  }
441
+ if (/\.css$/i.test(bare) && bare === raw && namesAlias(raw)) {
442
+ // An aliased stylesheet with no target falls back to a package, as in
443
+ // TypeScript; when there is none either, the alias is wrong.
444
+ let found = true;
445
+ try {
446
+ createRequire(filename).resolve(raw);
447
+ } catch {
448
+ found = false;
449
+ }
450
+ if (!found) {
451
+ const err = new Error(
452
+ `${path.basename(filename)} imports ${spec} through a path alias that does not lead to a file.`,
453
+ );
454
+ /** @type {any} */ (err).renderCode = 'ERR_FILE_NOT_FOUND';
455
+ throw err;
456
+ }
457
+ }
182
458
  if (/\.css$/i.test(bare)) {
183
459
  if (bare.startsWith('.') || path.isAbsolute(bare)) {
184
460
  const file = path.resolve(dir, bare);
@@ -303,61 +579,34 @@ function pagePlugin(filename) {
303
579
  }
304
580
 
305
581
  /**
306
- * Path aliases from the project's tsconfig.json or jsconfig.json
307
- * (`compilerOptions.paths`), as jiti aliases: `"@/*": ["./src/*"]` becomes
308
- * `{"@/": "<root>/src/"}`.
309
- * @returns {Record<string, string>}
582
+ * The project's path aliases (`compilerOptions.paths` in tsconfig.json or
583
+ * jsconfig.json), matched by TypeScript's rules (see ts-paths.mjs).
310
584
  */
311
- function projectAliases() {
312
- /** @type {Record<string, string>} */
313
- const alias = {};
314
- for (const name of ['tsconfig.json', 'jsconfig.json']) {
315
- const file = path.join(root, name);
316
- if (!fs.existsSync(file)) continue;
317
- /** @type {any} */
318
- let config;
319
- try {
320
- // Allow the comments and trailing commas these files often carry.
321
- const text = fs
322
- .readFileSync(file, 'utf8')
323
- .replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? '')
324
- .replace(/,(\s*[}\]])/g, '$1');
325
- config = JSON.parse(text);
326
- } catch {
327
- continue;
328
- }
329
- const options = config?.compilerOptions ?? {};
330
- const baseDir = path.resolve(root, typeof options.baseUrl === 'string' ? options.baseUrl : '.');
331
- for (const [key, targets] of Object.entries(options.paths ?? {})) {
332
- const target = Array.isArray(targets) ? targets[0] : undefined;
333
- if (typeof target !== 'string') continue;
334
- if (key.endsWith('/*') && target.endsWith('/*')) {
335
- alias[key.slice(0, -1)] = `${path.resolve(baseDir, target.slice(0, -2))}/`;
336
- } else if (!key.includes('*') && !target.includes('*')) {
337
- alias[key] = path.resolve(baseDir, target);
338
- }
339
- }
340
- break;
341
- }
342
- return alias;
343
- }
344
-
345
- const ALIASES = projectAliases();
585
+ const PATHS = readPathsIn(root) ?? [];
346
586
 
347
587
  /** @param {string} spec whether a specifier goes through a project alias */
348
- const isAliased = spec =>
349
- Object.keys(ALIASES).some(key => (key.endsWith('/') ? spec.startsWith(key) : spec === key));
350
-
351
- /** @param {string} spec a specifier through a project alias, as an absolute path */
352
- const resolveAlias = spec => {
353
- for (const [key, target] of Object.entries(ALIASES)) {
354
- if (key.endsWith('/') ? spec.startsWith(key) : spec === key) {
355
- return key.endsWith('/') ? target + spec.slice(key.length) : target;
356
- }
357
- }
358
- return spec;
588
+ const isAliased = spec => resolvePath(PATHS, spec) !== null;
589
+
590
+ /**
591
+ * Whether a specifier that resolves to no file was meant for an alias: it
592
+ * matches a key with a literal prefix. A catch-all key (`"*"`) matches every
593
+ * package name too, so an import it cannot place is a package that is not
594
+ * installed.
595
+ * @param {string} spec
596
+ */
597
+ const namesAlias = spec => {
598
+ const match = matchPath(PATHS, spec);
599
+ return match !== null && match.pattern.prefix !== '';
359
600
  };
360
601
 
602
+ /**
603
+ * A specifier through a project alias, as the absolute path of the first
604
+ * target that exists; the specifier itself when none does, so it resolves as
605
+ * a package.
606
+ * @param {string} spec
607
+ */
608
+ const resolveAlias = spec => resolvePath(PATHS, spec) ?? spec;
609
+
361
610
  /** Packages the page took from the package set, by name: their folders. */
362
611
  /** @type {Map<string, string>} */
363
612
  const fromSet = new Map();
@@ -381,6 +630,7 @@ function packageDirOf(file, name) {
381
630
 
382
631
  const moduleApi = /** @type {any} */ (Module);
383
632
 
633
+
384
634
  /** A module at the served Core's folder: what resolves as the Core would. */
385
635
  const servedParent = served ? new moduleApi(path.join(served.dir, 'package.json')) : null;
386
636
  if (servedParent && served) {
@@ -456,7 +706,7 @@ const jiti = createJiti(entry, {
456
706
  extensions: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts', '.cts', '.json'],
457
707
  moduleCache: true,
458
708
  interopDefault: true,
459
- alias: {...SINGLE_COPY_ALIASES, ...ALIASES},
709
+ alias: SINGLE_COPY_ALIASES,
460
710
  jsx: {runtime: 'automatic'},
461
711
  transform(opts) {
462
712
  const filename = opts.filename ?? '';
@@ -476,6 +726,7 @@ const jiti = createJiti(entry, {
476
726
  generatorOpts: {retainLines: true},
477
727
  compact: false,
478
728
  plugins: [
729
+ aliasPlugin(),
479
730
  pagePlugin(filename),
480
731
  ...(source.includes('stylex')
481
732
  ? [
@@ -495,6 +746,31 @@ const jiti = createJiti(entry, {
495
746
  const rules = /** @type {any} */ (result?.metadata)?.stylex;
496
747
  if (Array.isArray(rules)) stylexRules.push(...rules);
497
748
  source = result?.code ?? source;
749
+ } else if (
750
+ PATHS.length > 0 &&
751
+ filename &&
752
+ !filename.includes(NODE_MODULES) &&
753
+ /\.(tsx?|jsx?|mjs|cjs|mts|cts)$/.test(filename)
754
+ ) {
755
+ const ts = /\.(tsx?|mts|cts)$/.test(filename);
756
+ const result = transformSync(source, {
757
+ filename,
758
+ babelrc: false,
759
+ configFile: false,
760
+ sourceType: 'unambiguous',
761
+ // The options jiti compiles these files with: JSX only in .jsx and
762
+ // .tsx files (so `<string>value` stays a type assertion), and legacy
763
+ // decorators.
764
+ parserOpts: {
765
+ plugins: /** @type {import('@babel/core').ParserOptions['plugins']} */ (
766
+ ts ? [...(/\.[cm]?tsx$/.test(filename) ? ['jsx'] : []), 'typescript', 'decorators-legacy'] : ['jsx']
767
+ ),
768
+ },
769
+ generatorOpts: {retainLines: true},
770
+ compact: false,
771
+ plugins: [aliasPlugin()],
772
+ });
773
+ source = result?.code ?? source;
498
774
  }
499
775
  return {code: plain.transform({...opts, source, babel: {...opts.babel, compact: false}})};
500
776
  },
@@ -524,7 +800,7 @@ function classify(e) {
524
800
  const missing =
525
801
  /Cannot find (?:module|package) ['"]([^'"]+)['"]/.exec(message) ??
526
802
  /Failed to resolve import ['"]([^'"]+)['"]/.exec(message);
527
- if (missing && (isAliased(missing[1]) || /^[@~#]\/|^#/.test(missing[1]))) {
803
+ if (missing && (namesAlias(missing[1]) || /^[@~#]\/|^#/.test(missing[1]))) {
528
804
  return fail(
529
805
  'ERR_FILE_NOT_FOUND',
530
806
  `The page imports ${missing[1]} through a path alias that does not lead to a file. Check \`compilerOptions.paths\` in the project's tsconfig.json, or import it by a relative path.`,
@@ -582,6 +858,11 @@ try {
582
858
  let server;
583
859
  try {
584
860
  React = await jiti.import('react');
861
+ createElement = React.createElement;
862
+ useContext = React.useContext;
863
+ useRef = React.useRef;
864
+ useLayoutEffect = React.useLayoutEffect;
865
+ InChart = React.createContext(false);
585
866
  server = await jiti.import('react-dom/server');
586
867
  } catch (e) {
587
868
  classify(e);
@@ -589,8 +870,18 @@ try {
589
870
  }
590
871
  /** @type {string} */
591
872
  let markup;
873
+ let chartsFailed = 0;
874
+ /** @type {false | 'error' | 'changed' | 'rerendered'} */
875
+ let chartsAlone = false;
592
876
  try {
593
877
  markup = server.renderToStaticMarkup(React.createElement(Page));
878
+ if (charts.length > 0) {
879
+ // Drawing the page again re-runs its code: what it prints then was
880
+ // already reported by the server render.
881
+ const printed = logs.length;
882
+ ({markup, failed: chartsFailed, alone: chartsAlone} = await drawCharts(markup, Page));
883
+ logs.length = printed;
884
+ }
594
885
  } catch (e) {
595
886
  classify(e);
596
887
  throw null;
@@ -613,6 +904,8 @@ try {
613
904
  packageCss,
614
905
  skippedCss,
615
906
  fromSet: [...fromSet].map(([name, dir]) => ({name, dir})),
907
+ chartsFailed,
908
+ chartsAlone,
616
909
  assets: [...assetsInlined],
617
910
  files: localFiles,
618
911
  texts,
@@ -1,6 +1,11 @@
1
1
  // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
+ /**
5
+ * The warning for a page rendered without Intl.Segmenter.
6
+ * @returns {RenderWarning}
7
+ */
8
+ export function segmenterWarning(): RenderWarning;
4
9
  /**
5
10
  * Render a page module to one self-contained HTML file.
6
11
  * @param {string} entry absolute path of the page module
@@ -20,7 +25,7 @@ export const RENDER_TIMEOUT_MS: 60000;
20
25
  export { PAGE_EXTENSIONS } from "./embedded-files.mjs";
21
26
  /**
22
27
  * @typedef {object} RenderWarning
23
- * @property {'dropped-children' | 'remote-asset' | 'missing-asset' | 'page-output' | 'page-script' | 'no-astryx-styles' | 'chart-size' | 'not-embedded'} code - What kind of problem.
28
+ * @property {'dropped-children' | 'remote-asset' | 'missing-asset' | 'page-output' | 'page-script' | 'no-astryx-styles' | 'chart-size' | 'not-embedded' | 'node-crash'} code - What kind of problem.
24
29
  * @property {string} message - What happened and what to do, in one sentence.
25
30
  */
26
31
  /**
@@ -46,15 +51,11 @@ export class RenderError extends Error {
46
51
  code: string;
47
52
  detail: Record<string, unknown> | undefined;
48
53
  }
49
- export type ServedSet = {
50
- dir: string;
51
- integrations: readonly string[];
52
- };
53
54
  export type RenderWarning = {
54
55
  /**
55
56
  * - What kind of problem.
56
57
  */
57
- code: "dropped-children" | "remote-asset" | "missing-asset" | "page-output" | "page-script" | "no-astryx-styles" | "chart-size" | "not-embedded";
58
+ code: "dropped-children" | "remote-asset" | "missing-asset" | "page-output" | "page-script" | "no-astryx-styles" | "chart-size" | "not-embedded" | "node-crash";
58
59
  /**
59
60
  * - What happened and what to do, in one sentence.
60
61
  */
@@ -98,3 +99,7 @@ export type RenderedPage = {
98
99
  dir: string;
99
100
  }[];
100
101
  };
102
+ export type ServedSet = {
103
+ dir: string;
104
+ integrations: readonly string[];
105
+ };