@astryxdesign/cli 0.7.12-canary.ccb4a6d → 0.7.12-canary.da3dbdc

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 (85) hide show
  1. package/api/component/__fixtures__/resolutions/modules.txt +5 -5
  2. package/api/docs/_adapter.d.mts +22 -0
  3. package/api/docs/_adapter.mjs +81 -3
  4. package/api/docs/docs.doc.mjs +5 -5
  5. package/api/docs/docs.type.d.mts +21 -0
  6. package/api/docs/docs.type.mjs +11 -0
  7. package/api/docs/list/list.d.mts +5 -1
  8. package/api/docs/list/list.mjs +72 -6
  9. package/api/docs/node/node.mjs +2 -40
  10. package/api/index.d.mts +1 -0
  11. package/api/index.mjs +1 -0
  12. package/api/render/_adapter.mjs +9 -0
  13. package/api/render/render.doc.mjs +2 -2
  14. package/api/search/_adapter.d.mts +22 -1
  15. package/api/search/_adapter.mjs +37 -20
  16. package/api/search/exportSearchIndex.doc.d.mts +10 -0
  17. package/api/search/exportSearchIndex.doc.mjs +58 -0
  18. package/api/search/rankSearchIndex.doc.d.mts +10 -0
  19. package/api/search/rankSearchIndex.doc.mjs +79 -0
  20. package/api/search/search-index.d.mts +44 -0
  21. package/api/search/search-index.mjs +270 -0
  22. package/api/search/search.type.d.mts +28 -0
  23. package/api/search/search.type.mjs +17 -0
  24. package/api/swizzle/_moduleGraph.d.mts +7 -0
  25. package/api/swizzle/_moduleGraph.mjs +191 -83
  26. package/api/swizzle/copy/copy.mjs +100 -9
  27. package/api/swizzle/swizzle.doc.mjs +7 -3
  28. package/api/swizzle/swizzle.type.d.mts +5 -0
  29. package/api/swizzle/swizzle.type.mjs +1 -0
  30. package/api/template/template.mjs +1 -0
  31. package/assets/discover/official-packages.json +16 -0
  32. package/assets/docs/browser-support.doc.mjs +2 -2
  33. package/assets/docs/styling-overview.doc.mjs +6 -2
  34. package/assets/docs/tree/render-pages.doc.mjs +2 -2
  35. package/assets/docs/tree/tailwind.doc.mjs +41 -1
  36. package/assets/docs/tree/type-scale.doc.mjs +19 -2
  37. package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.doc.mjs +14 -0
  38. package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.tsx +146 -0
  39. package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.tsx +173 -0
  41. package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
  42. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
  43. package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
  44. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
  45. package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
  46. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
  47. package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
  48. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
  49. package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
  50. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
  51. package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
  52. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
  53. package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
  54. package/assets/templates/pages/table-filter/page.tsx +15 -23
  55. package/assets/templates/pages/table-inbox/page.tsx +3 -6
  56. package/assets/templates/themes/butter/butterTheme.ts +2 -2
  57. package/assets/templates/themes/stone/stoneTheme.ts +1 -1
  58. package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
  59. package/clients/cli/commands/docs.doc.mjs +5 -1
  60. package/clients/cli/commands/docs.mjs +30 -2
  61. package/clients/cli/commands/swizzle.doc.mjs +4 -0
  62. package/clients/cli/commands/swizzle.mjs +12 -0
  63. package/clients/cli/lib/resolve-theme.mjs +16 -7
  64. package/foundation/discovery/hook-discovery.mjs +31 -8
  65. package/foundation/doc-compiler/import.d.mts +5 -1
  66. package/foundation/doc-compiler/import.mjs +40 -6
  67. package/foundation/fs/import-url.d.mts +20 -0
  68. package/foundation/fs/import-url.mjs +22 -0
  69. package/foundation/fs/module-loader.d.mts +24 -3
  70. package/foundation/fs/module-loader.mjs +69 -47
  71. package/foundation/render/charts.d.mts +59 -0
  72. package/foundation/render/charts.mjs +216 -0
  73. package/foundation/render/embedded-imports.d.mts +7 -7
  74. package/foundation/render/embedded-imports.mjs +63 -54
  75. package/foundation/render/page-worker.mjs +339 -52
  76. package/foundation/render/render-page.mjs +26 -10
  77. package/foundation/render/ts-paths.d.mts +63 -0
  78. package/foundation/render/ts-paths.mjs +138 -0
  79. package/foundation/response/error-codes.doc.mjs +2 -2
  80. package/foundation/response/error-codes.mjs +3 -2
  81. package/foundation/text/levenshtein.d.mts +6 -1
  82. package/foundation/text/levenshtein.mjs +25 -9
  83. package/foundation/text/suggest-order.d.mts +8 -1
  84. package/foundation/text/suggest-order.mjs +72 -6
  85. package/package.json +7 -6
@@ -12,6 +12,10 @@
12
12
  * from the package set the CLI runs from (served-packages.mjs), and React and
13
13
  * StyleX always come from that set, so the page loads one copy of each.
14
14
  *
15
+ * Charts the page imports from recharts draw only after their effects run,
16
+ * so the server render sets them aside and draws them after it with React
17
+ * DOM in a small document (charts.mjs).
18
+ *
15
19
  * @input workerData {entry, root, served}
16
20
  * @output one message: {ok: true, markup, css, files, texts, logs} or
17
21
  * {ok: false, error: {code, message, detail?}}
@@ -25,10 +29,12 @@ import {pathToFileURL} from 'node:url';
25
29
  import {format} from 'node:util';
26
30
  import {parentPort, workerData} from 'node:worker_threads';
27
31
  import {transformSync} from '@babel/core';
32
+ import {matchPath, readPathsIn, resolvePath} from './ts-paths.mjs';
28
33
  import {createJiti} from 'jiti';
29
34
  import {assetDataUri, isAssetPath} from './assets.mjs';
30
35
  import {SINGLE_COPY, isServedPackage, packageOf} from './served-packages.mjs';
31
36
  import {findNodeModulesPackage} from '../fs/paths.mjs';
37
+ import {CHART_GLOBAL, CHART_ROOTS, chartDocument, chartSize, markLegends} from './charts.mjs';
32
38
 
33
39
  /** @typedef {{level: string, text: string}} PageLog */
34
40
 
@@ -136,6 +142,252 @@ const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
136
142
  const isLocal = file =>
137
143
  !file.includes(NODE_MODULES) && (file === entry || file.startsWith(ROOT_PREFIX));
138
144
 
145
+ /** Charts the server render set aside, drawn after it. @type {any[]} */
146
+ const charts = [];
147
+ /** The props each chart was last drawn with in the page-wide draw. @type {any[]} */
148
+ const pageCharts = [];
149
+ /**
150
+ * Whether a chart rendered again after the page-wide draw first committed. A
151
+ * chart renders again only when the component that made it renders again:
152
+ * its effects changed its state, which a closure in the chart may read, so
153
+ * the props alone cannot say the chart is the same. React's development
154
+ * double render under StrictMode runs before the commit, so it does not
155
+ * count.
156
+ */
157
+ let renderedAfterCommit = false;
158
+
159
+ /**
160
+ * A chart's props as a string that two renders of the same chart share:
161
+ * data by value, functions by name, elements by type and props.
162
+ * @param {unknown} value
163
+ * @param {number} [depth]
164
+ * @returns {string}
165
+ */
166
+ function sameKey(value, depth = 0) {
167
+ if (depth > 8) return '…';
168
+ if (typeof value === 'function') return `fn:${value.name}`;
169
+ if (typeof value === 'symbol') return String(value);
170
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'undefined';
171
+ if (Array.isArray(value)) return `[${value.map(v => sameKey(v, depth + 1)).join(',')}]`;
172
+ const record = /** @type {Record<string, unknown>} */ (value);
173
+ if ('$$typeof' in record && 'props' in record) {
174
+ const type = /** @type {any} */ (record.type);
175
+ return `<${typeof type === 'string' ? type : (type?.displayName ?? type?.name ?? '?')} ${sameKey(record.props, depth + 1)}>`;
176
+ }
177
+ return `{${Object.keys(record)
178
+ .filter(key => !key.startsWith('_'))
179
+ .sort()
180
+ .map(key => `${key}:${sameKey(record[key], depth + 1)}`)
181
+ .join(',')}}`;
182
+ }
183
+ /** React's createElement, once the page's React has loaded. @type {any} */
184
+ let createElement = null;
185
+ /** React's useContext and useRef. @type {any} */
186
+ let useContext = null;
187
+ /** @type {any} */
188
+ let useRef = null;
189
+ /** @type {any} */
190
+ let useLayoutEffect = null;
191
+ /** Set inside a drawn chart, so a chart inside it is not wrapped again. @type {any} */
192
+ let InChart = null;
193
+ /**
194
+ * `aside` while the server render sets charts aside; `page` while the whole
195
+ * page is drawn again, so each chart sees the context it was rendered in;
196
+ * `alone` while each chart is drawn by itself, when the page cannot be.
197
+ * @type {'aside' | 'page' | 'alone'}
198
+ */
199
+ let mode = 'aside';
200
+
201
+ /**
202
+ * Wrap a recharts chart, or a ResponsiveContainer, so the server render sets
203
+ * it aside and the drawing pass draws it: a container at the size it starts
204
+ * at, in a box that lets the drawing fit the page's width.
205
+ * @param {any} Chart
206
+ * @param {boolean} container
207
+ */
208
+ /** @type {any} */ (globalThis).__astryxRenderChart = (/** @type {any} */ Chart, /** @type {boolean} */ container) => {
209
+ function RenderedChart(/** @type {any} */ props) {
210
+ if (mode === 'aside') {
211
+ charts.push(createElement(RenderedChart, props));
212
+ return createElement('astryx-chart', {'data-chart': charts.length - 1});
213
+ }
214
+ const inside = useContext(InChart);
215
+ // Each chart drawn in the page notes the props it was last drawn with,
216
+ // by its place in the page, to compare with the first render's.
217
+ const place = useRef(/** @type {number | null} */ (null));
218
+ const committed = useRef(false);
219
+ useLayoutEffect(() => {
220
+ committed.current = true;
221
+ });
222
+ if (mode === 'page' && !inside) {
223
+ place.current ??= pageCharts.length;
224
+ pageCharts[place.current] = props;
225
+ if (committed.current) renderedAfterCommit = true;
226
+ }
227
+ const drawn = container
228
+ ? createElement('div', {className: 'recharts-responsive-container'}, createElement(Chart, {...props, ...chartSize(props)}))
229
+ : createElement(Chart, props);
230
+ if (mode === 'alone' || inside) return drawn;
231
+ return createElement('astryx-chart', null, createElement(InChart.Provider, {value: true}, drawn));
232
+ }
233
+ return RenderedChart;
234
+ };
235
+
236
+ /**
237
+ * Point the page's recharts charts, and its ResponsiveContainer, at the
238
+ * wrapper the worker installs.
239
+ * @param {any} p the import of 'recharts'
240
+ * @param {any} t Babel's types
241
+ */
242
+ function setChartsAside(p, t) {
243
+ for (const s of p.node.specifiers) {
244
+ if (s.type !== 'ImportSpecifier') continue;
245
+ const name = s.imported.name ?? s.imported.value;
246
+ const container = name === 'ResponsiveContainer';
247
+ if (!container && !CHART_ROOTS.has(name)) continue;
248
+ const original = p.scope.generateUidIdentifier(name);
249
+ const local = s.local.name;
250
+ s.local = original;
251
+ p.insertAfter(
252
+ t.variableDeclaration('const', [
253
+ t.variableDeclarator(
254
+ t.identifier(local),
255
+ t.callExpression(t.memberExpression(t.identifier('globalThis'), t.identifier(CHART_GLOBAL)), [
256
+ original,
257
+ t.booleanLiteral(container),
258
+ ]),
259
+ ),
260
+ ]),
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Draw the charts the server render set aside, and put each drawing where
267
+ * its placeholder is.
268
+ *
269
+ * Draw the whole page first. A chart's own parts then see the React context
270
+ * the page gives them. If that draw throws, or the page's effects change a
271
+ * chart (its props, or which charts there are), draw each chart alone, from
272
+ * the props of the first render. A chart that fails stays empty and is
273
+ * counted.
274
+ * @param {string} markup
275
+ * @param {any} Page
276
+ * @returns {Promise<{markup: string, failed: number, alone: false | 'error' | 'changed' | 'rerendered'}>}
277
+ */
278
+ async function drawCharts(markup, Page) {
279
+ const placeholder = /<astryx-chart data-chart="(\d+)"><\/astryx-chart>/g;
280
+ /** @type {any} */
281
+ let client;
282
+ /** @type {any} */
283
+ let flushSync;
284
+ try {
285
+ await chartDocument();
286
+ client = await jiti.import('react-dom/client');
287
+ ({flushSync} = /** @type {any} */ (await jiti.import('react-dom')));
288
+ // recharts decided it runs on a server when it loaded for the server
289
+ // render. Now there is a document, so it measures text and lays out
290
+ // axis labels as in a browser.
291
+ const recharts = /** @type {any} */ (await jiti.import('recharts'));
292
+ if (recharts?.Global) recharts.Global.isSsr = false;
293
+ } catch {
294
+ // Without a document to draw in, every chart stays empty and is counted.
295
+ return {markup: markup.replace(placeholder, ''), failed: charts.length, alone: /** @type {false} */ (false)};
296
+ }
297
+ /**
298
+ * Render one element in its own root and hand its host to `read`.
299
+ * @param {unknown} element
300
+ * @param {(host: any) => string[]} read
301
+ * @returns {string[] | null} null when the render failed
302
+ */
303
+ const drawIn = (element, read) => {
304
+ /** @type {unknown} */
305
+ let error = null;
306
+ const host = /** @type {any} */ (globalThis).document.createElement('div');
307
+ const root = client.createRoot(host, {
308
+ onUncaughtError: (/** @type {unknown} */ e) => {
309
+ error = e;
310
+ },
311
+ onRecoverableError: () => {},
312
+ });
313
+ try {
314
+ flushSync(() => root.render(element));
315
+ } catch (e) {
316
+ error = e;
317
+ }
318
+ if (!error) markLegends(host);
319
+ const out = error ? null : read(host);
320
+ root.unmount();
321
+ return out;
322
+ };
323
+ mode = 'page';
324
+ let drawn = drawIn(createElement(Page), host => [...host.querySelectorAll('astryx-chart')].map(el => el.innerHTML));
325
+ /** @type {false | 'error' | 'changed' | 'rerendered'} */
326
+ let alone = false;
327
+ if (!drawn) alone = 'error';
328
+ // The page's effects changed a chart: its props, which charts there are,
329
+ // or the state of the component that made it, which a closure or context
330
+ // in the chart may read.
331
+ else if (
332
+ drawn.length !== charts.length ||
333
+ pageCharts.length !== charts.length ||
334
+ charts.some((chart, i) => sameKey(chart.props) !== sameKey(pageCharts[i]))
335
+ ) {
336
+ alone = 'changed';
337
+ } else if (renderedAfterCommit) {
338
+ alone = 'rerendered';
339
+ }
340
+ if (alone) {
341
+ mode = 'alone';
342
+ drawn = charts.map(chart => drawIn(chart, host => [host.innerHTML])?.[0] ?? '');
343
+ }
344
+ let failed = 0;
345
+ // A chart that drew no picture counts as failed, once.
346
+ const pictures = /** @type {string[]} */ (drawn).map(html => {
347
+ if (html.includes('<svg')) return html;
348
+ failed += 1;
349
+ return '';
350
+ });
351
+ return {
352
+ markup: markup.replace(placeholder, (_, i) => pictures[Number(i)] ?? ''),
353
+ failed,
354
+ alone,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Babel plugin: a bare import that a path alias places loads from that file,
360
+ * as in TypeScript, before any installed copy. An import the aliases do not
361
+ * place is left alone, so an installed package loads. It runs on the page's
362
+ * own files, before {@link pagePlugin}, and on every module the page loads
363
+ * from outside them and outside `node_modules` (a sibling package's source,
364
+ * say).
365
+ */
366
+ function aliasPlugin() {
367
+ return () => {
368
+ /** @param {any} node a string literal to point at the file an alias places */
369
+ const point = node => {
370
+ if (node?.type !== 'StringLiteral') return;
371
+ const file = resolvePath(PATHS, node.value);
372
+ if (file) node.value = file;
373
+ };
374
+ return {
375
+ visitor: {
376
+ 'ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration'(/** @type {any} */ p) {
377
+ point(p.node.source);
378
+ },
379
+ CallExpression(/** @type {any} */ p) {
380
+ const callee = p.node.callee;
381
+ if (callee?.type === 'Import' || (callee?.type === 'Identifier' && callee.name === 'require')) point(p.node.arguments?.[0]);
382
+ },
383
+ ImportExpression(/** @type {any} */ p) {
384
+ point(p.node.source);
385
+ },
386
+ },
387
+ };
388
+ };
389
+ }
390
+
139
391
  /**
140
392
  * Babel plugin: replace asset imports with data: URIs and stylesheet imports
141
393
  * with nothing (their CSS is inlined into the page), and record static text
@@ -148,6 +400,7 @@ function pagePlugin(filename) {
148
400
  visitor: {
149
401
  ImportDeclaration(/** @type {any} */ p) {
150
402
  const spec = /** @type {string} */ (p.node.source.value);
403
+ if (spec === 'recharts') setChartsAside(p, t);
151
404
  const raw = spec.split('?')[0];
152
405
  // A stylesheet, image or JSON file imported through a project alias
153
406
  // (`@/styles/site.css`) resolves the way modules do.
@@ -179,6 +432,23 @@ function pagePlugin(filename) {
179
432
  p.remove();
180
433
  return;
181
434
  }
435
+ if (/\.css$/i.test(bare) && bare === raw && namesAlias(raw)) {
436
+ // An aliased stylesheet with no target falls back to a package, as in
437
+ // TypeScript; when there is none either, the alias is wrong.
438
+ let found = true;
439
+ try {
440
+ createRequire(filename).resolve(raw);
441
+ } catch {
442
+ found = false;
443
+ }
444
+ if (!found) {
445
+ const err = new Error(
446
+ `${path.basename(filename)} imports ${spec} through a path alias that does not lead to a file.`,
447
+ );
448
+ /** @type {any} */ (err).renderCode = 'ERR_FILE_NOT_FOUND';
449
+ throw err;
450
+ }
451
+ }
182
452
  if (/\.css$/i.test(bare)) {
183
453
  if (bare.startsWith('.') || path.isAbsolute(bare)) {
184
454
  const file = path.resolve(dir, bare);
@@ -303,61 +573,34 @@ function pagePlugin(filename) {
303
573
  }
304
574
 
305
575
  /**
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>}
576
+ * The project's path aliases (`compilerOptions.paths` in tsconfig.json or
577
+ * jsconfig.json), matched by TypeScript's rules (see ts-paths.mjs).
310
578
  */
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();
579
+ const PATHS = readPathsIn(root) ?? [];
346
580
 
347
581
  /** @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;
582
+ const isAliased = spec => resolvePath(PATHS, spec) !== null;
583
+
584
+ /**
585
+ * Whether a specifier that resolves to no file was meant for an alias: it
586
+ * matches a key with a literal prefix. A catch-all key (`"*"`) matches every
587
+ * package name too, so an import it cannot place is a package that is not
588
+ * installed.
589
+ * @param {string} spec
590
+ */
591
+ const namesAlias = spec => {
592
+ const match = matchPath(PATHS, spec);
593
+ return match !== null && match.pattern.prefix !== '';
359
594
  };
360
595
 
596
+ /**
597
+ * A specifier through a project alias, as the absolute path of the first
598
+ * target that exists; the specifier itself when none does, so it resolves as
599
+ * a package.
600
+ * @param {string} spec
601
+ */
602
+ const resolveAlias = spec => resolvePath(PATHS, spec) ?? spec;
603
+
361
604
  /** Packages the page took from the package set, by name: their folders. */
362
605
  /** @type {Map<string, string>} */
363
606
  const fromSet = new Map();
@@ -381,6 +624,7 @@ function packageDirOf(file, name) {
381
624
 
382
625
  const moduleApi = /** @type {any} */ (Module);
383
626
 
627
+
384
628
  /** A module at the served Core's folder: what resolves as the Core would. */
385
629
  const servedParent = served ? new moduleApi(path.join(served.dir, 'package.json')) : null;
386
630
  if (servedParent && served) {
@@ -456,7 +700,7 @@ const jiti = createJiti(entry, {
456
700
  extensions: ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts', '.cts', '.json'],
457
701
  moduleCache: true,
458
702
  interopDefault: true,
459
- alias: {...SINGLE_COPY_ALIASES, ...ALIASES},
703
+ alias: SINGLE_COPY_ALIASES,
460
704
  jsx: {runtime: 'automatic'},
461
705
  transform(opts) {
462
706
  const filename = opts.filename ?? '';
@@ -476,6 +720,7 @@ const jiti = createJiti(entry, {
476
720
  generatorOpts: {retainLines: true},
477
721
  compact: false,
478
722
  plugins: [
723
+ aliasPlugin(),
479
724
  pagePlugin(filename),
480
725
  ...(source.includes('stylex')
481
726
  ? [
@@ -495,6 +740,31 @@ const jiti = createJiti(entry, {
495
740
  const rules = /** @type {any} */ (result?.metadata)?.stylex;
496
741
  if (Array.isArray(rules)) stylexRules.push(...rules);
497
742
  source = result?.code ?? source;
743
+ } else if (
744
+ PATHS.length > 0 &&
745
+ filename &&
746
+ !filename.includes(NODE_MODULES) &&
747
+ /\.(tsx?|jsx?|mjs|cjs|mts|cts)$/.test(filename)
748
+ ) {
749
+ const ts = /\.(tsx?|mts|cts)$/.test(filename);
750
+ const result = transformSync(source, {
751
+ filename,
752
+ babelrc: false,
753
+ configFile: false,
754
+ sourceType: 'unambiguous',
755
+ // The options jiti compiles these files with: JSX only in .jsx and
756
+ // .tsx files (so `<string>value` stays a type assertion), and legacy
757
+ // decorators.
758
+ parserOpts: {
759
+ plugins: /** @type {import('@babel/core').ParserOptions['plugins']} */ (
760
+ ts ? [...(/\.[cm]?tsx$/.test(filename) ? ['jsx'] : []), 'typescript', 'decorators-legacy'] : ['jsx']
761
+ ),
762
+ },
763
+ generatorOpts: {retainLines: true},
764
+ compact: false,
765
+ plugins: [aliasPlugin()],
766
+ });
767
+ source = result?.code ?? source;
498
768
  }
499
769
  return {code: plain.transform({...opts, source, babel: {...opts.babel, compact: false}})};
500
770
  },
@@ -524,7 +794,7 @@ function classify(e) {
524
794
  const missing =
525
795
  /Cannot find (?:module|package) ['"]([^'"]+)['"]/.exec(message) ??
526
796
  /Failed to resolve import ['"]([^'"]+)['"]/.exec(message);
527
- if (missing && (isAliased(missing[1]) || /^[@~#]\/|^#/.test(missing[1]))) {
797
+ if (missing && (namesAlias(missing[1]) || /^[@~#]\/|^#/.test(missing[1]))) {
528
798
  return fail(
529
799
  'ERR_FILE_NOT_FOUND',
530
800
  `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 +852,11 @@ try {
582
852
  let server;
583
853
  try {
584
854
  React = await jiti.import('react');
855
+ createElement = React.createElement;
856
+ useContext = React.useContext;
857
+ useRef = React.useRef;
858
+ useLayoutEffect = React.useLayoutEffect;
859
+ InChart = React.createContext(false);
585
860
  server = await jiti.import('react-dom/server');
586
861
  } catch (e) {
587
862
  classify(e);
@@ -589,8 +864,18 @@ try {
589
864
  }
590
865
  /** @type {string} */
591
866
  let markup;
867
+ let chartsFailed = 0;
868
+ /** @type {false | 'error' | 'changed' | 'rerendered'} */
869
+ let chartsAlone = false;
592
870
  try {
593
871
  markup = server.renderToStaticMarkup(React.createElement(Page));
872
+ if (charts.length > 0) {
873
+ // Drawing the page again re-runs its code: what it prints then was
874
+ // already reported by the server render.
875
+ const printed = logs.length;
876
+ ({markup, failed: chartsFailed, alone: chartsAlone} = await drawCharts(markup, Page));
877
+ logs.length = printed;
878
+ }
594
879
  } catch (e) {
595
880
  classify(e);
596
881
  throw null;
@@ -613,6 +898,8 @@ try {
613
898
  packageCss,
614
899
  skippedCss,
615
900
  fromSet: [...fromSet].map(([name, dir]) => ({name, dir})),
901
+ chartsFailed,
902
+ chartsAlone,
616
903
  assets: [...assetsInlined],
617
904
  files: localFiles,
618
905
  texts,
@@ -25,6 +25,7 @@ import {classesIn, cutCss, layerOrderOf} from './css-cut.mjs';
25
25
  import {sourceBlock} from './source-block.mjs';
26
26
  import {embeddedPathProblem} from './embedded-files.mjs';
27
27
  import {resolveServed} from './served-packages.mjs';
28
+ import {CHART_CSS} from './charts.mjs';
28
29
 
29
30
  /** How long one page may take to render before it is stopped. */
30
31
  export const RENDER_TIMEOUT_MS = 60_000;
@@ -522,15 +523,30 @@ export async function renderPage(entry, options = {}) {
522
523
  message: `<${t.component}> on line ${t.line} did not render the text "${t.text.length > 60 ? `${t.text.slice(0, 57)}...` : t.text}" it was given; it takes that text through a prop (often label=), so check its props.`,
523
524
  });
524
525
  }
525
- for (const f of /** @type {string[]} */ (message.files ?? [])) {
526
- const src = fs.readFileSync(f, 'utf8');
527
- if (/\bResponsiveContainer\b/.test(src)) {
528
- warnings.push({
529
- code: 'chart-size',
530
- message: `${path.basename(f)} sizes a chart with ResponsiveContainer, which has no size until a browser measures it, so the chart is empty here; give the chart a fixed width and height.`,
531
- });
532
- break;
533
- }
526
+ // A chart the render did not draw is an empty box: a ResponsiveContainer
527
+ // holds only its sizing box, and a chart of fixed size an empty wrapper.
528
+ const blankCharts =
529
+ (markup.match(/<div class="recharts-responsive-container[" ][^>]*><div[^>]*><\/div><\/div>/g) ?? []).length +
530
+ (markup.match(/<div\b[^>]*\bclass="recharts-wrapper"[^>]*><\/div>/g) ?? []).length +
531
+ (message.chartsFailed ?? 0);
532
+ if (blankCharts > 0) {
533
+ warnings.push({
534
+ code: 'chart-size',
535
+ message: `${blankCharts === 1 ? 'A chart' : `${blankCharts} charts`} could not be drawn without a browser, so ${blankCharts === 1 ? 'it is' : 'they are'} empty here: the render draws a chart only when a module of the page (not an installed package) imports the chart, and its ResponsiveContainer, from recharts by name (\`import {LineChart} from 'recharts'\`).`,
536
+ });
537
+ }
538
+ if (message.chartsAlone) {
539
+ /** @type {Record<string, string>} */
540
+ const causes = {
541
+ changed: 'The page changed its charts once its effects ran (their data or props, or which charts it shows)',
542
+ rerendered: 'A component that makes a chart rendered again once the page\'s effects ran',
543
+ error: 'The page could not be drawn again without a browser',
544
+ };
545
+ const why = causes[message.chartsAlone] ?? causes.error;
546
+ warnings.push({
547
+ code: 'chart-size',
548
+ message: `${why}, so each chart was drawn by itself, as the first render gave it: a custom tick, label or shape in a chart sees React context defaults, not the values the page provides.`,
549
+ });
534
550
  }
535
551
  for (const sheet of /** @type {string[]} */ (message.skippedCss ?? [])) {
536
552
  warnings.push({
@@ -600,7 +616,7 @@ export async function renderPage(entry, options = {}) {
600
616
  '<meta name="color-scheme" content="light dark">' +
601
617
  `<meta name="generator" content="Astryx${options.version ? ` ${escapeHtml(options.version)}` : ''}">` +
602
618
  `<title>${escapeHtml(title)}</title>` +
603
- `<style>${SCHEME_CSS}${css}</style></head><body>${markup}` +
619
+ `<style>${SCHEME_CSS}${markup.includes('class="recharts-responsive-container"') ? CHART_CSS : ''}${css}</style></head><body>${markup}` +
604
620
  `${sourceBlock(path.basename(entry), embedded)}</body></html>\n`;
605
621
  return {
606
622
  html,
@@ -0,0 +1,63 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * The path patterns of the config in `dir` (tsconfig.json, else
6
+ * jsconfig.json), or none. Comments and trailing commas are allowed.
7
+ * @param {string} dir
8
+ * @returns {PathPattern[] | null} null when neither file is there
9
+ */
10
+ export function readPathsIn(dir: string): PathPattern[] | null;
11
+ /**
12
+ * The pattern a specifier matches by TypeScript's rules, and what its `*`
13
+ * matched: an exact key first, else the wildcard key with the longest prefix.
14
+ * @param {PathPattern[]} patterns
15
+ * @param {string} spec
16
+ * @returns {{pattern: PathPattern, capture: string} | null}
17
+ */
18
+ export function matchPath(patterns: PathPattern[], spec: string): {
19
+ pattern: PathPattern;
20
+ capture: string;
21
+ } | null;
22
+ /**
23
+ * The file a candidate path stands for: itself, with an extension, or its
24
+ * folder's index; null when none exists.
25
+ * @param {string} candidate
26
+ * @returns {string | null}
27
+ */
28
+ export function existingFile(candidate: string): string | null;
29
+ /**
30
+ * Resolve a specifier through path patterns: the first target that names an
31
+ * existing file wins.
32
+ * @param {PathPattern[]} patterns
33
+ * @param {string} spec
34
+ * @returns {string | null} the file, or null when the specifier matches no
35
+ * pattern or no target exists
36
+ */
37
+ export function resolvePath(patterns: PathPattern[], spec: string): string | null;
38
+ export function candidatesOf(match: {
39
+ pattern: PathPattern;
40
+ capture: string;
41
+ }): string[];
42
+ export type PathPattern = {
43
+ /**
44
+ * - The key as written.
45
+ */
46
+ key: string;
47
+ /**
48
+ * - What comes before the key's `*` (all of it, for an exact key).
49
+ */
50
+ prefix: string;
51
+ /**
52
+ * - What comes after the key's `*`.
53
+ */
54
+ suffix: string;
55
+ /**
56
+ * - Whether the key holds a `*`.
57
+ */
58
+ wildcard: boolean;
59
+ /**
60
+ * - Absolute target patterns, in order.
61
+ */
62
+ targets: string[];
63
+ };