@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.
- package/api/component/__fixtures__/resolutions/modules.txt +5 -5
- package/api/docs/_adapter.d.mts +22 -0
- package/api/docs/_adapter.mjs +81 -3
- package/api/docs/docs.doc.mjs +5 -5
- package/api/docs/docs.type.d.mts +21 -0
- package/api/docs/docs.type.mjs +11 -0
- package/api/docs/list/list.d.mts +5 -1
- package/api/docs/list/list.mjs +72 -6
- package/api/docs/node/node.mjs +2 -40
- package/api/index.d.mts +1 -0
- package/api/index.mjs +1 -0
- package/api/render/_adapter.mjs +9 -0
- package/api/render/render.doc.mjs +2 -2
- package/api/search/_adapter.d.mts +22 -1
- package/api/search/_adapter.mjs +37 -20
- package/api/search/exportSearchIndex.doc.d.mts +10 -0
- package/api/search/exportSearchIndex.doc.mjs +58 -0
- package/api/search/rankSearchIndex.doc.d.mts +10 -0
- package/api/search/rankSearchIndex.doc.mjs +79 -0
- package/api/search/search-index.d.mts +44 -0
- package/api/search/search-index.mjs +270 -0
- package/api/search/search.type.d.mts +28 -0
- package/api/search/search.type.mjs +17 -0
- package/api/swizzle/_moduleGraph.d.mts +7 -0
- package/api/swizzle/_moduleGraph.mjs +191 -83
- package/api/swizzle/copy/copy.mjs +100 -9
- package/api/swizzle/swizzle.doc.mjs +7 -3
- package/api/swizzle/swizzle.type.d.mts +5 -0
- package/api/swizzle/swizzle.type.mjs +1 -0
- package/api/template/template.mjs +1 -0
- package/assets/discover/official-packages.json +16 -0
- package/assets/docs/browser-support.doc.mjs +2 -2
- package/assets/docs/styling-overview.doc.mjs +6 -2
- package/assets/docs/tree/render-pages.doc.mjs +2 -2
- package/assets/docs/tree/tailwind.doc.mjs +41 -1
- package/assets/docs/tree/type-scale.doc.mjs +19 -2
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarConstraintsHookUsage.tsx +146 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Calendar/useCalendarNavigationHookUsage.tsx +173 -0
- package/assets/templates/blocks/components/Hooks/useInputStatusIconHookUsage.tsx +1 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Hooks/useInteractiveRoleHookUsage.tsx +56 -0
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.doc.mjs +2 -2
- package/assets/templates/blocks/components/Hooks/useStreamingTextHookUsage.tsx +66 -10
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Link/useLinkifyHookUsage.tsx +39 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/PowerSearch/usePowerSearchConfigHookUsage.tsx +83 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/RowsVirtualizationHookUsage.tsx +35 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.doc.mjs +14 -0
- package/assets/templates/blocks/components/Table/useTableColumnSettingsStateHookUsage.tsx +97 -0
- package/assets/templates/pages/table-filter/page.tsx +15 -23
- package/assets/templates/pages/table-inbox/page.tsx +3 -6
- package/assets/templates/themes/butter/butterTheme.ts +2 -2
- package/assets/templates/themes/stone/stoneTheme.ts +1 -1
- package/assets/templates/themes/y2k/y2kTheme.ts +1 -1
- package/clients/cli/commands/docs.doc.mjs +5 -1
- package/clients/cli/commands/docs.mjs +30 -2
- package/clients/cli/commands/swizzle.doc.mjs +4 -0
- package/clients/cli/commands/swizzle.mjs +12 -0
- package/clients/cli/lib/resolve-theme.mjs +16 -7
- package/foundation/discovery/hook-discovery.mjs +31 -8
- package/foundation/doc-compiler/import.d.mts +5 -1
- package/foundation/doc-compiler/import.mjs +40 -6
- package/foundation/fs/import-url.d.mts +20 -0
- package/foundation/fs/import-url.mjs +22 -0
- package/foundation/fs/module-loader.d.mts +24 -3
- package/foundation/fs/module-loader.mjs +69 -47
- package/foundation/render/charts.d.mts +59 -0
- package/foundation/render/charts.mjs +216 -0
- package/foundation/render/embedded-imports.d.mts +7 -7
- package/foundation/render/embedded-imports.mjs +63 -54
- package/foundation/render/page-worker.mjs +339 -52
- package/foundation/render/render-page.mjs +26 -10
- package/foundation/render/ts-paths.d.mts +63 -0
- package/foundation/render/ts-paths.mjs +138 -0
- package/foundation/response/error-codes.doc.mjs +2 -2
- package/foundation/response/error-codes.mjs +3 -2
- package/foundation/text/levenshtein.d.mts +6 -1
- package/foundation/text/levenshtein.mjs +25 -9
- package/foundation/text/suggest-order.d.mts +8 -1
- package/foundation/text/suggest-order.mjs +72 -6
- 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
|
-
*
|
|
307
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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:
|
|
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 && (
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
+
};
|