@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
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import {fork, spawnSync} from 'node:child_process';
|
|
22
|
+
import {fileURLToPath} from 'node:url';
|
|
22
23
|
import postcss from 'postcss';
|
|
23
24
|
import {assetDataUri, isAssetBytes, isAssetPath, RenderAssetError} from './assets.mjs';
|
|
24
25
|
import {classesIn, cutCss, layerOrderOf} from './css-cut.mjs';
|
|
25
26
|
import {sourceBlock} from './source-block.mjs';
|
|
26
27
|
import {embeddedPathProblem} from './embedded-files.mjs';
|
|
27
28
|
import {resolveServed} from './served-packages.mjs';
|
|
29
|
+
import {CHART_CSS} from './charts.mjs';
|
|
28
30
|
|
|
29
31
|
/** How long one page may take to render before it is stopped. */
|
|
30
32
|
export const RENDER_TIMEOUT_MS = 60_000;
|
|
@@ -33,7 +35,7 @@ export {PAGE_EXTENSIONS} from './embedded-files.mjs';
|
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* @typedef {object} RenderWarning
|
|
36
|
-
* @property {'dropped-children' | 'remote-asset' | 'missing-asset' | 'page-output' | 'page-script' | 'no-astryx-styles' | 'chart-size' | 'not-embedded'} code - What kind of problem.
|
|
38
|
+
* @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.
|
|
37
39
|
* @property {string} message - What happened and what to do, in one sentence.
|
|
38
40
|
*/
|
|
39
41
|
|
|
@@ -220,65 +222,158 @@ function expandImports(file, css, ctx) {
|
|
|
220
222
|
|
|
221
223
|
/** @typedef {{dir: string, integrations: readonly string[]}} ServedSet */
|
|
222
224
|
|
|
225
|
+
/** Signals that Node itself or a native module raises when it fails. */
|
|
226
|
+
const CRASH_SIGNALS = new Set(['SIGSEGV', 'SIGBUS', 'SIGILL', 'SIGFPE', 'SIGABRT', 'SIGTRAP']);
|
|
227
|
+
|
|
223
228
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
229
|
+
* How a native crash ended a process, or null when it was not one. On
|
|
230
|
+
* Windows a crash arrives as an exit status of 0xC0000000 and above (such as
|
|
231
|
+
* 0xC0000005, an access violation), not as a signal.
|
|
232
|
+
* @param {number | null} code
|
|
233
|
+
* @param {NodeJS.Signals | null} signal
|
|
234
|
+
*/
|
|
235
|
+
function crashOf(code, signal) {
|
|
236
|
+
if (signal) return CRASH_SIGNALS.has(signal) ? signal : null;
|
|
237
|
+
if (code !== null && code >>> 0 >= 0xc0000000) return `0x${(code >>> 0).toString(16).toUpperCase()}`;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Whether this Node crashes when it splits text with Intl.Segmenter. Some
|
|
243
|
+
* Node builds do, and Astryx components such as Avatar split text with it.
|
|
244
|
+
*/
|
|
245
|
+
function segmenterCrashes() {
|
|
246
|
+
const probe = spawnSync(
|
|
247
|
+
process.execPath,
|
|
248
|
+
['-e', "[...new Intl.Segmenter('en', {granularity: 'grapheme'}).segment('a b')]"],
|
|
249
|
+
{stdio: 'ignore', timeout: 10_000},
|
|
250
|
+
);
|
|
251
|
+
return crashOf(probe.status, probe.signal) !== null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The error for a page process that a crash or a signal ended. A crash comes
|
|
256
|
+
* from Node itself or a native module, never from the page's JavaScript.
|
|
257
|
+
* Every message names this Node.
|
|
258
|
+
* @param {string} how the crash (a signal name or a Windows status) or the stopping signal
|
|
259
|
+
* @param {{crashed: boolean, withoutSegmenter: boolean}} state
|
|
260
|
+
*/
|
|
261
|
+
function crashError(how, {crashed, withoutSegmenter}) {
|
|
262
|
+
const node = `Node ${process.version} (${process.execPath})`;
|
|
263
|
+
const detail = {signal: how, node: process.version, nodePath: process.execPath};
|
|
264
|
+
if (!crashed) {
|
|
265
|
+
const memory = how === 'SIGKILL' ? ' The system stops a process this way when the machine runs out of memory.' : '';
|
|
266
|
+
return new RenderError('ERR_RENDER_FAILED', `${node} was stopped by a signal (${how}) while it rendered the page.${memory}`, detail);
|
|
267
|
+
}
|
|
268
|
+
const cause = withoutSegmenter ? ' It crashed again with Intl.Segmenter removed, so the fault is elsewhere in this Node or in a native module.' : '';
|
|
269
|
+
return new RenderError(
|
|
270
|
+
'ERR_RENDER_FAILED',
|
|
271
|
+
`${node} crashed (${how}) while it rendered the page. A fault in Node or in a native module causes this, not the page's code.${cause} Render with an official Node.js build of another version.`,
|
|
272
|
+
detail,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Run the page in a host process (page-host.mjs), stopping it after
|
|
278
|
+
* `timeoutMs`. The host runs the page in a worker thread, and it exits when
|
|
279
|
+
* this process goes away.
|
|
280
|
+
* @param {{entry: string, root: string, served: ServedSet | null, withoutSegmenter: boolean}} input
|
|
227
281
|
* @param {number} timeoutMs
|
|
228
|
-
* @
|
|
229
|
-
*
|
|
282
|
+
* @returns {Promise<any>} the page's answer, or `{crashed: how}` when a
|
|
283
|
+
* native crash ended the host
|
|
230
284
|
*/
|
|
231
|
-
function runWorker(
|
|
285
|
+
function runWorker(input, timeoutMs) {
|
|
232
286
|
return new Promise((resolve, reject) => {
|
|
233
|
-
const
|
|
234
|
-
|
|
287
|
+
const data = {
|
|
288
|
+
...input,
|
|
289
|
+
served: input.served ? {dir: input.served.dir, integrations: [...input.served.integrations]} : null,
|
|
290
|
+
timeoutMs,
|
|
291
|
+
};
|
|
292
|
+
const host = fork(fileURLToPath(new URL('./page-host.mjs', import.meta.url)), [JSON.stringify(data)], {
|
|
293
|
+
// Never the CLI's own flags (such as --input-type).
|
|
235
294
|
execArgv: [],
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
resourceLimits: {maxOldGenerationSizeMb: 4096},
|
|
295
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
296
|
+
serialization: 'advanced',
|
|
239
297
|
});
|
|
240
|
-
// The page's own writes to stdout/stderr never reach the CLI's output.
|
|
241
|
-
worker.stdout.resume();
|
|
242
|
-
worker.stderr.resume();
|
|
243
298
|
let settled = false;
|
|
244
|
-
|
|
299
|
+
/** @param {() => void} settle */
|
|
300
|
+
const finish = settle => {
|
|
245
301
|
if (settled) return;
|
|
246
302
|
settled = true;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
host.kill('SIGKILL');
|
|
305
|
+
settle();
|
|
306
|
+
};
|
|
307
|
+
const timer = setTimeout(() => {
|
|
308
|
+
finish(() =>
|
|
309
|
+
reject(
|
|
310
|
+
new RenderError(
|
|
311
|
+
'ERR_RENDER_TIMEOUT',
|
|
312
|
+
`The page did not finish rendering within ${timeoutMs / 1000} seconds, so it was stopped. Look for a loop that never ends or a promise the page waits on.`,
|
|
313
|
+
{seconds: timeoutMs / 1000},
|
|
314
|
+
),
|
|
253
315
|
),
|
|
254
316
|
);
|
|
255
317
|
}, timeoutMs);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
318
|
+
host.once('message', (/** @type {any} */ message) => {
|
|
319
|
+
finish(() => {
|
|
320
|
+
if (message?.hostError !== undefined) {
|
|
321
|
+
const text = String(message.hostError);
|
|
322
|
+
reject(
|
|
323
|
+
/ERR_WORKER_OUT_OF_MEMORY|heap out of memory/.test(text)
|
|
324
|
+
? new RenderError('ERR_RENDER_FAILED', 'The page ran out of memory while rendering.')
|
|
325
|
+
: new RenderError('ERR_RENDER_FAILED', `The page could not be rendered: ${text.split('\n')[0]}`),
|
|
326
|
+
);
|
|
327
|
+
} else if (message?.hostExit !== undefined) {
|
|
328
|
+
reject(new RenderError('ERR_RENDER_FAILED', `The page stopped rendering without an answer (exit ${message.hostExit}).`));
|
|
329
|
+
} else {
|
|
330
|
+
resolve(message);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
262
333
|
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
settled = true;
|
|
266
|
-
clearTimeout(timer);
|
|
267
|
-
const out =
|
|
268
|
-
/ERR_WORKER_OUT_OF_MEMORY|heap out of memory/.test(String(error?.message ?? error))
|
|
269
|
-
? new RenderError('ERR_RENDER_FAILED', 'The page ran out of memory while rendering.')
|
|
270
|
-
: new RenderError('ERR_RENDER_FAILED', `The page could not be rendered: ${String(error?.message ?? error).split('\n')[0]}`);
|
|
271
|
-
reject(out);
|
|
334
|
+
host.once('error', error => {
|
|
335
|
+
finish(() => reject(new RenderError('ERR_RENDER_FAILED', `The page could not be rendered: ${String(error?.message ?? error).split('\n')[0]}`)));
|
|
272
336
|
});
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
337
|
+
host.once('exit', (code, signal) => {
|
|
338
|
+
finish(() => {
|
|
339
|
+
const crash = crashOf(code, signal);
|
|
340
|
+
if (crash) resolve({crashed: crash});
|
|
341
|
+
else if (signal) reject(crashError(signal, {crashed: false, withoutSegmenter: input.withoutSegmenter}));
|
|
342
|
+
else reject(new RenderError('ERR_RENDER_FAILED', `The page stopped rendering without an answer (exit ${code}).`));
|
|
343
|
+
});
|
|
278
344
|
});
|
|
279
345
|
});
|
|
280
346
|
}
|
|
281
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Run the page. When this Node crashes in Intl.Segmenter, run it once more
|
|
350
|
+
* without Intl.Segmenter and say so in a warning.
|
|
351
|
+
* @param {{entry: string, root: string, served: ServedSet | null}} input
|
|
352
|
+
* @param {number} timeoutMs
|
|
353
|
+
* @param {RenderWarning[]} warnings
|
|
354
|
+
*/
|
|
355
|
+
async function runPage(input, timeoutMs, warnings) {
|
|
356
|
+
const started = Date.now();
|
|
357
|
+
const first = await runWorker({...input, withoutSegmenter: false}, timeoutMs);
|
|
358
|
+
if (!first.crashed) return first;
|
|
359
|
+
if (!segmenterCrashes()) throw crashError(first.crashed, {crashed: true, withoutSegmenter: false});
|
|
360
|
+
const again = await runWorker({...input, withoutSegmenter: true}, Math.max(1000, timeoutMs - (Date.now() - started)));
|
|
361
|
+
if (again.crashed) throw crashError(again.crashed, {crashed: true, withoutSegmenter: true});
|
|
362
|
+
warnings.push(segmenterWarning());
|
|
363
|
+
return again;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The warning for a page rendered without Intl.Segmenter.
|
|
368
|
+
* @returns {RenderWarning}
|
|
369
|
+
*/
|
|
370
|
+
export function segmenterWarning() {
|
|
371
|
+
return {
|
|
372
|
+
code: 'node-crash',
|
|
373
|
+
message: `This Node build (${process.version} at ${process.execPath}) crashes in Intl.Segmenter, so the page rendered without it, and the output may differ slightly: initials and character counts (such as Avatar initials) can split a joined emoji, a flag, or a letter written with a separate accent mark. Render with an official Node.js build to remove the difference.`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
282
377
|
/**
|
|
283
378
|
* Inline local images (src, srcset, poster and CSS url()) as data: URIs. A
|
|
284
379
|
* link (`href`) loads nothing, so it is left alone.
|
|
@@ -370,9 +465,9 @@ export async function renderPage(entry, options = {}) {
|
|
|
370
465
|
const dir = path.dirname(entry);
|
|
371
466
|
const root = projectRootOf(dir);
|
|
372
467
|
const served = options.served ?? null;
|
|
373
|
-
const message = await runWorker(entry, root, options.timeoutMs ?? RENDER_TIMEOUT_MS, served);
|
|
374
468
|
/** @type {RenderWarning[]} */
|
|
375
469
|
const warnings = [];
|
|
470
|
+
const message = await runPage({entry, root, served}, options.timeoutMs ?? RENDER_TIMEOUT_MS, warnings);
|
|
376
471
|
if (!message.ok) {
|
|
377
472
|
throw new RenderError(message.error.code, message.error.message, message.error.detail);
|
|
378
473
|
}
|
|
@@ -522,15 +617,30 @@ export async function renderPage(entry, options = {}) {
|
|
|
522
617
|
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
618
|
});
|
|
524
619
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
620
|
+
// A chart the render did not draw is an empty box: a ResponsiveContainer
|
|
621
|
+
// holds only its sizing box, and a chart of fixed size an empty wrapper.
|
|
622
|
+
const blankCharts =
|
|
623
|
+
(markup.match(/<div class="recharts-responsive-container[" ][^>]*><div[^>]*><\/div><\/div>/g) ?? []).length +
|
|
624
|
+
(markup.match(/<div\b[^>]*\bclass="recharts-wrapper"[^>]*><\/div>/g) ?? []).length +
|
|
625
|
+
(message.chartsFailed ?? 0);
|
|
626
|
+
if (blankCharts > 0) {
|
|
627
|
+
warnings.push({
|
|
628
|
+
code: 'chart-size',
|
|
629
|
+
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'\`).`,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (message.chartsAlone) {
|
|
633
|
+
/** @type {Record<string, string>} */
|
|
634
|
+
const causes = {
|
|
635
|
+
changed: 'The page changed its charts once its effects ran (their data or props, or which charts it shows)',
|
|
636
|
+
rerendered: 'A component that makes a chart rendered again once the page\'s effects ran',
|
|
637
|
+
error: 'The page could not be drawn again without a browser',
|
|
638
|
+
};
|
|
639
|
+
const why = causes[message.chartsAlone] ?? causes.error;
|
|
640
|
+
warnings.push({
|
|
641
|
+
code: 'chart-size',
|
|
642
|
+
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.`,
|
|
643
|
+
});
|
|
534
644
|
}
|
|
535
645
|
for (const sheet of /** @type {string[]} */ (message.skippedCss ?? [])) {
|
|
536
646
|
warnings.push({
|
|
@@ -600,7 +710,7 @@ export async function renderPage(entry, options = {}) {
|
|
|
600
710
|
'<meta name="color-scheme" content="light dark">' +
|
|
601
711
|
`<meta name="generator" content="Astryx${options.version ? ` ${escapeHtml(options.version)}` : ''}">` +
|
|
602
712
|
`<title>${escapeHtml(title)}</title>` +
|
|
603
|
-
`<style>${SCHEME_CSS}${css}</style></head><body>${markup}` +
|
|
713
|
+
`<style>${SCHEME_CSS}${markup.includes('class="recharts-responsive-container"') ? CHART_CSS : ''}${css}</style></head><body>${markup}` +
|
|
604
714
|
`${sourceBlock(path.basename(entry), embedded)}</body></html>\n`;
|
|
605
715
|
return {
|
|
606
716
|
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
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file A project's `compilerOptions.paths` (tsconfig.json or jsconfig.json),
|
|
5
|
+
* read and matched by TypeScript's rules, for `astryx experimental render`:
|
|
6
|
+
* an exact key matches only itself; a key may hold one `*` anywhere, and the
|
|
7
|
+
* longest matching prefix wins; a target may hold one `*`, which takes what
|
|
8
|
+
* the key's `*` matched; targets are tried in order, and when none exists the
|
|
9
|
+
* import resolves as a package. Only bare specifiers are matched. Targets
|
|
10
|
+
* resolve from `baseUrl` (default: the config's folder).
|
|
11
|
+
*
|
|
12
|
+
* @position foundation/render — used by page-worker.mjs and embedded-imports.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} PathPattern
|
|
20
|
+
* @property {string} key - The key as written.
|
|
21
|
+
* @property {string} prefix - What comes before the key's `*` (all of it, for an exact key).
|
|
22
|
+
* @property {string} suffix - What comes after the key's `*`.
|
|
23
|
+
* @property {boolean} wildcard - Whether the key holds a `*`.
|
|
24
|
+
* @property {string[]} targets - Absolute target patterns, in order.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Extensions an extensionless candidate may stand for, in resolution order. */
|
|
28
|
+
const EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.mts', '.cts', '.json'];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The path patterns of the config in `dir` (tsconfig.json, else
|
|
32
|
+
* jsconfig.json), or none. Comments and trailing commas are allowed.
|
|
33
|
+
* @param {string} dir
|
|
34
|
+
* @returns {PathPattern[] | null} null when neither file is there
|
|
35
|
+
*/
|
|
36
|
+
export function readPathsIn(dir) {
|
|
37
|
+
for (const name of ['tsconfig.json', 'jsconfig.json']) {
|
|
38
|
+
const file = path.join(dir, name);
|
|
39
|
+
if (!fs.existsSync(file)) continue;
|
|
40
|
+
try {
|
|
41
|
+
const text = fs
|
|
42
|
+
.readFileSync(file, 'utf8')
|
|
43
|
+
.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? '')
|
|
44
|
+
.replace(/,(\s*[}\]])/g, '$1');
|
|
45
|
+
const options = JSON.parse(text)?.compilerOptions ?? {};
|
|
46
|
+
const base = path.resolve(dir, typeof options.baseUrl === 'string' ? options.baseUrl : '.');
|
|
47
|
+
/** @type {PathPattern[]} */
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const [key, targets] of Object.entries(options.paths ?? {})) {
|
|
50
|
+
if (!Array.isArray(targets)) continue;
|
|
51
|
+
const star = key.indexOf('*');
|
|
52
|
+
if (star !== key.lastIndexOf('*')) continue;
|
|
53
|
+
out.push({
|
|
54
|
+
key,
|
|
55
|
+
prefix: star === -1 ? key : key.slice(0, star),
|
|
56
|
+
suffix: star === -1 ? '' : key.slice(star + 1),
|
|
57
|
+
wildcard: star !== -1,
|
|
58
|
+
targets: targets
|
|
59
|
+
.filter(t => typeof t === 'string' && t.indexOf('*') === t.lastIndexOf('*'))
|
|
60
|
+
.map(t => path.resolve(base, t)),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The pattern a specifier matches by TypeScript's rules, and what its `*`
|
|
73
|
+
* matched: an exact key first, else the wildcard key with the longest prefix.
|
|
74
|
+
* @param {PathPattern[]} patterns
|
|
75
|
+
* @param {string} spec
|
|
76
|
+
* @returns {{pattern: PathPattern, capture: string} | null}
|
|
77
|
+
*/
|
|
78
|
+
export function matchPath(patterns, spec) {
|
|
79
|
+
// TypeScript applies `paths` only to bare specifiers.
|
|
80
|
+
if (/^\.{1,2}(\/|$)|^\//.test(spec)) return null;
|
|
81
|
+
const exact = patterns.find(p => !p.wildcard && p.key === spec);
|
|
82
|
+
if (exact) return {pattern: exact, capture: ''};
|
|
83
|
+
/** @type {{pattern: PathPattern, capture: string} | null} */
|
|
84
|
+
let best = null;
|
|
85
|
+
for (const p of patterns) {
|
|
86
|
+
if (!p.wildcard) continue;
|
|
87
|
+
if (spec.length < p.prefix.length + p.suffix.length) continue;
|
|
88
|
+
if (!spec.startsWith(p.prefix) || !spec.endsWith(p.suffix)) continue;
|
|
89
|
+
if (best && best.pattern.prefix.length >= p.prefix.length) continue;
|
|
90
|
+
best = {pattern: p, capture: spec.slice(p.prefix.length, spec.length - p.suffix.length)};
|
|
91
|
+
}
|
|
92
|
+
return best;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The absolute paths a match names, one per target, in order.
|
|
97
|
+
* @param {{pattern: PathPattern, capture: string}} match
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export const candidatesOf = match => match.pattern.targets.map(t => t.replace('*', match.capture));
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The file a candidate path stands for: itself, with an extension, or its
|
|
104
|
+
* folder's index; null when none exists.
|
|
105
|
+
* @param {string} candidate
|
|
106
|
+
* @returns {string | null}
|
|
107
|
+
*/
|
|
108
|
+
export function existingFile(candidate) {
|
|
109
|
+
const isFile = (/** @type {string} */ f) => {
|
|
110
|
+
try {
|
|
111
|
+
return fs.statSync(f).isFile();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (isFile(candidate)) return candidate;
|
|
117
|
+
for (const ext of EXTENSIONS) if (isFile(candidate + ext)) return candidate + ext;
|
|
118
|
+
for (const ext of EXTENSIONS) if (isFile(path.join(candidate, `index${ext}`))) return path.join(candidate, `index${ext}`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a specifier through path patterns: the first target that names an
|
|
124
|
+
* existing file wins.
|
|
125
|
+
* @param {PathPattern[]} patterns
|
|
126
|
+
* @param {string} spec
|
|
127
|
+
* @returns {string | null} the file, or null when the specifier matches no
|
|
128
|
+
* pattern or no target exists
|
|
129
|
+
*/
|
|
130
|
+
export function resolvePath(patterns, spec) {
|
|
131
|
+
const match = matchPath(patterns, spec);
|
|
132
|
+
if (!match) return null;
|
|
133
|
+
for (const candidate of candidatesOf(match)) {
|
|
134
|
+
const file = existingFile(candidate);
|
|
135
|
+
if (file) return file;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -332,19 +332,19 @@ export const doc = {
|
|
|
332
332
|
value: 'ERR_RENDER_FAILED',
|
|
333
333
|
stability: 'experimental',
|
|
334
334
|
description:
|
|
335
|
-
'The page threw while rendering. Nothing was written. Raised only by the experimental render command.',
|
|
335
|
+
'The page threw while rendering, or Node crashed while it rendered the page. Nothing was written. Raised only by the experimental render command.',
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
value: 'ERR_RENDER_NOT_ASTRYX',
|
|
339
339
|
stability: 'experimental',
|
|
340
340
|
description:
|
|
341
|
-
'`render --reverse` was given a file with no embedded source and no Astryx markup. Nothing was written. Raised only by the experimental render command.',
|
|
341
|
+
'`render --reverse` was given a file that a render did not write, with no embedded source and no Astryx markup. Nothing was written. Raised only by the experimental render command.',
|
|
342
342
|
},
|
|
343
343
|
{
|
|
344
344
|
value: 'ERR_RENDER_SOURCE_DAMAGED',
|
|
345
345
|
stability: 'experimental',
|
|
346
346
|
description:
|
|
347
|
-
'`render --reverse` refused the received file, and nothing was written: its embedded source is damaged (not valid, or a file does not match its checksum), lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its own imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds: a dotfile or dot-folder, anything under node_modules, a package manifest, lockfile or tool configuration, a file a test, story or benchmark runner would run, or any kind but a page module, stylesheet, JSON file or image. Raised only by the experimental render command.',
|
|
347
|
+
'`render --reverse` refused the received file, and nothing was written: its embedded source was removed (a file a render wrote, with no Astryx components left to rebuild from) or is damaged (not valid, or a file does not match its checksum), lists a path twice, carries a module, stylesheet or JSON file the page does not reach through its own imports, or an image, font or media file whose bytes are not one, or carries a file a render never embeds: a dotfile or dot-folder, anything under node_modules, a package manifest, lockfile or tool configuration, a file a test, story or benchmark runner would run, or any kind but a page module, stylesheet, JSON file or image. Raised only by the experimental render command.',
|
|
348
348
|
},
|
|
349
349
|
],
|
|
350
350
|
};
|
|
@@ -298,8 +298,9 @@ export const ERROR_CODES = Object.freeze({
|
|
|
298
298
|
*/
|
|
299
299
|
ERR_RENDER_FAILED: 'ERR_RENDER_FAILED',
|
|
300
300
|
/**
|
|
301
|
-
* `render --reverse` was given an HTML file that
|
|
302
|
-
* no embedded source, so there is nothing
|
|
301
|
+
* `render --reverse` was given an HTML file that a render did not write and
|
|
302
|
+
* that holds no Astryx markup and no embedded source, so there is nothing
|
|
303
|
+
* to rebuild. Nothing was written.
|
|
303
304
|
*/
|
|
304
305
|
ERR_RENDER_NOT_ASTRYX: 'ERR_RENDER_NOT_ASTRYX',
|
|
305
306
|
/**
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a: string, b: string, max?: number
|
|
27
|
+
export function levenshteinDistance(a: string, b: string, max?: number, { swaps }?: {
|
|
28
|
+
swaps?: boolean;
|
|
29
|
+
}): number;
|
|
@@ -19,11 +19,15 @@
|
|
|
19
19
|
* @param {number} [max] - The largest distance the caller can use. When
|
|
20
20
|
* given, a larger distance answers `max + 1`, and only the cells within
|
|
21
21
|
* `max` of the diagonal are computed.
|
|
22
|
+
* @param {{swaps?: boolean}} [options] - With `swaps` and a `max`, two
|
|
23
|
+
* adjacent letters swapped cost one edit, not two (the optimal string
|
|
24
|
+
* alignment distance).
|
|
22
25
|
* @returns {number}
|
|
23
26
|
*/
|
|
24
|
-
export function levenshteinDistance(a, b, max) {
|
|
25
|
-
if (max !== undefined) return distanceWithin(a, b, max);
|
|
26
|
-
const m = a.length,
|
|
27
|
+
export function levenshteinDistance(a, b, max, {swaps = false} = {}) {
|
|
28
|
+
if (max !== undefined) return distanceWithin(a, b, max, swaps);
|
|
29
|
+
const m = a.length,
|
|
30
|
+
n = b.length;
|
|
27
31
|
// Early exit: the distance is always ≥ |m − n|, so once the length gap
|
|
28
32
|
// exceeds the loosest fuzzy-match threshold any caller uses (the hook
|
|
29
33
|
// suggester keeps distance ≤ 5) the pair can never match — skip the O(m·n)
|
|
@@ -36,9 +40,10 @@ export function levenshteinDistance(a, b, max) {
|
|
|
36
40
|
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
37
41
|
for (let i = 1; i <= m; i++) {
|
|
38
42
|
for (let j = 1; j <= n; j++) {
|
|
39
|
-
dp[i][j] =
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
dp[i][j] =
|
|
44
|
+
a[i - 1] === b[j - 1]
|
|
45
|
+
? dp[i - 1][j - 1]
|
|
46
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
return dp[m][n];
|
|
@@ -51,11 +56,13 @@ export function levenshteinDistance(a, b, max) {
|
|
|
51
56
|
* @param {string} a
|
|
52
57
|
* @param {string} b
|
|
53
58
|
* @param {number} max
|
|
59
|
+
* @param {boolean} swaps - two adjacent letters swapped cost one edit
|
|
54
60
|
* @returns {number}
|
|
55
61
|
*/
|
|
56
|
-
function distanceWithin(a, b, max) {
|
|
62
|
+
function distanceWithin(a, b, max, swaps) {
|
|
57
63
|
const over = max + 1;
|
|
58
64
|
if (Math.abs(a.length - b.length) > max) return over;
|
|
65
|
+
let before = new Array(b.length + 1).fill(over);
|
|
59
66
|
let previous = new Array(b.length + 1).fill(over);
|
|
60
67
|
let current = new Array(b.length + 1).fill(over);
|
|
61
68
|
for (let j = 0; j <= Math.min(b.length, max); j++) previous[j] = j;
|
|
@@ -67,18 +74,27 @@ function distanceWithin(a, b, max) {
|
|
|
67
74
|
let rowMin = current[0];
|
|
68
75
|
for (let j = from; j <= to; j++) {
|
|
69
76
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
70
|
-
|
|
77
|
+
let value = Math.min(
|
|
71
78
|
previous[j - 1] + cost,
|
|
72
79
|
previous[j] + 1,
|
|
73
80
|
current[j - 1] + 1,
|
|
74
81
|
over,
|
|
75
82
|
);
|
|
83
|
+
if (
|
|
84
|
+
swaps &&
|
|
85
|
+
i > 1 &&
|
|
86
|
+
j > 1 &&
|
|
87
|
+
a[i - 1] === b[j - 2] &&
|
|
88
|
+
a[i - 2] === b[j - 1]
|
|
89
|
+
) {
|
|
90
|
+
value = Math.min(value, before[j - 2] + 1);
|
|
91
|
+
}
|
|
76
92
|
current[j] = value;
|
|
77
93
|
if (value < rowMin) rowMin = value;
|
|
78
94
|
}
|
|
79
95
|
// Every later cell costs at least this row's least.
|
|
80
96
|
if (rowMin >= over) return over;
|
|
81
|
-
[previous, current] = [current,
|
|
97
|
+
[before, previous, current] = [previous, current, before];
|
|
82
98
|
}
|
|
83
99
|
return Math.min(previous[b.length], over);
|
|
84
100
|
}
|
|
@@ -18,15 +18,22 @@ export function nameWords(text: string): string[];
|
|
|
18
18
|
* when it carries some of them, the items that carry none are left out. When
|
|
19
19
|
* no item carries one, nothing is left out. The list only ever gets shorter,
|
|
20
20
|
* and its order does not change.
|
|
21
|
+
*
|
|
22
|
+
* With `typos`, a near name (one or two slips from the typed name, as
|
|
23
|
+
* `ButonGroup` or `ButtonGorup` for ButtonGroup) ranks just below the names
|
|
24
|
+
* that carry every typed word and above the rest. With `closest` too, near
|
|
25
|
+
* names stay in the list whatever the cut leaves out, and the list is never
|
|
26
|
+
* longer than it is without `typos`.
|
|
21
27
|
* @template T
|
|
22
28
|
* @param {string} typedName
|
|
23
29
|
* @param {T[]} items
|
|
24
30
|
* @param {(item: T) => Array<string | undefined>} namesOf
|
|
25
|
-
* @param {{closest?: boolean}} [options]
|
|
31
|
+
* @param {{closest?: boolean, typos?: boolean}} [options]
|
|
26
32
|
* @returns {T[]}
|
|
27
33
|
*/
|
|
28
34
|
export function orderByAffinity<T>(typedName: string, items: T[], namesOf: (item: T) => Array<string | undefined>, options?: {
|
|
29
35
|
closest?: boolean;
|
|
36
|
+
typos?: boolean;
|
|
30
37
|
}): T[];
|
|
31
38
|
/**
|
|
32
39
|
* Docs route suggestions, closest to the typed route first. When every
|