@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.
- package/CHANGELOG.md +107 -0
- package/api/build/blocks/blocks.d.mts +41 -0
- package/api/build/blocks/blocks.mjs +118 -68
- package/api/component/__fixtures__/resolutions/modules.txt +3 -3
- 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/doctor/doctor.d.mts +4 -4
- package/api/doctor/doctor.mjs +248 -78
- package/api/icon/_authoring.mjs +8 -1
- package/api/icon/add/add.mjs +45 -5
- package/api/icon/icon.type.d.mts +24 -0
- package/api/icon/icon.type.mjs +6 -0
- package/api/icon/iconAdd.doc.mjs +1 -1
- package/api/icon/map/map.mjs +11 -4
- package/api/index.d.mts +1 -0
- package/api/index.mjs +1 -0
- package/api/integration/add-helpers.d.mts +5 -1
- package/api/integration/add-helpers.mjs +1 -1
- package/api/render/_adapter.mjs +9 -0
- package/api/render/render.doc.mjs +3 -3
- package/api/render/render.type.d.mts +1 -1
- package/api/render/render.type.mjs +1 -1
- package/api/search/ORDERING.md +3 -0
- package/api/search/_adapter.d.mts +32 -3
- package/api/search/_adapter.mjs +50 -22
- package/api/search/_fit-order.d.mts +47 -0
- package/api/search/_fit-order.mjs +257 -0
- package/api/search/_main-searcher.mjs +15 -5
- 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 +28 -6
- package/api/template/skeleton/skeleton.mjs +1 -1
- package/api/template/template.mjs +1 -0
- package/assets/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +1 -1
- package/assets/codemods/transforms/v0.1.0/index.mjs +1 -1
- package/assets/discover/official-packages.json +16 -0
- package/assets/docs/styling-overview.doc.mjs +3 -3
- package/assets/docs/tree/icon-sets.doc.mjs +4 -4
- package/assets/docs/tree/render-pages.doc.mjs +3 -3
- package/assets/docs/tree/tailwind.doc.mjs +41 -1
- package/assets/docs/tree/type-scale.doc.mjs +19 -2
- 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 +41 -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-grouped/page.tsx +30 -30
- package/assets/templates/pages/table-inbox/page.tsx +3 -6
- package/assets/templates/themes/butter/butterTheme.ts +1 -1
- package/assets/templates/themes/gothic/gothicTheme.ts +16 -0
- package/assets/templates/themes/matcha/matchaTheme.ts +1 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +6 -0
- package/assets/templates/themes/y2k/y2kPaletteRefs.generated.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/experimental/icon.mjs +25 -4
- package/clients/cli/commands/experimental-render.doc.mjs +1 -1
- package/clients/cli/commands/template.mjs +204 -181
- package/clients/cli/lib/resolve-theme.mjs +16 -7
- package/foundation/discovery/component-discovery.mjs +1 -1
- package/foundation/discovery/hook-discovery.mjs +31 -8
- package/foundation/discovery/icon-set-writer.d.mts +21 -5
- package/foundation/discovery/icon-set-writer.mjs +114 -13
- package/foundation/discovery/template-adapter.mjs +2 -2
- 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-host.d.mts +4 -0
- package/foundation/render/page-host.mjs +45 -0
- package/foundation/render/page-worker.mjs +350 -57
- package/foundation/render/render-page.d.mts +11 -6
- package/foundation/render/render-page.mjs +164 -54
- package/foundation/render/ts-paths.d.mts +63 -0
- package/foundation/render/ts-paths.mjs +138 -0
- package/foundation/response/error-codes.doc.mjs +3 -3
- 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/foundation/xle/registry-core.mjs +1 -1
- 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
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
307
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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:
|
|
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 && (
|
|
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
|
+
};
|