@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Charts in a rendered page. A recharts chart draws only after its
|
|
5
|
+
* effects run, and a ResponsiveContainer measures its parent in a browser, so
|
|
6
|
+
* a server render leaves them empty. The render worker draws each one again
|
|
7
|
+
* in a small document, at the size the container starts at, and puts the
|
|
8
|
+
* drawing in its place.
|
|
9
|
+
*
|
|
10
|
+
* @input The chart elements the server render set aside.
|
|
11
|
+
* @output Their markup, and the CSS that lets a drawn chart fit its width.
|
|
12
|
+
* @position foundation/render — read by page-worker.mjs and render-page.mjs.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The global the render worker installs to set charts aside. SYNC: page-worker.mjs assigns it by this name. */
|
|
16
|
+
export const CHART_GLOBAL = '__astryxRenderChart';
|
|
17
|
+
|
|
18
|
+
/** The recharts exports that draw a chart, besides ResponsiveContainer. */
|
|
19
|
+
export const CHART_ROOTS = new Set([
|
|
20
|
+
'AreaChart',
|
|
21
|
+
'BarChart',
|
|
22
|
+
'ComposedChart',
|
|
23
|
+
'FunnelChart',
|
|
24
|
+
'LineChart',
|
|
25
|
+
'PieChart',
|
|
26
|
+
'RadarChart',
|
|
27
|
+
'RadialBarChart',
|
|
28
|
+
'Sankey',
|
|
29
|
+
'ScatterChart',
|
|
30
|
+
'SunburstChart',
|
|
31
|
+
'Treemap',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
/** Widest starting size for a chart its page lets fill the width. */
|
|
35
|
+
const CHART_MAX_WIDTH = 800;
|
|
36
|
+
|
|
37
|
+
/** Height of a chart that names none. */
|
|
38
|
+
const CHART_HEIGHT = 300;
|
|
39
|
+
|
|
40
|
+
/** Height up to which a chart counts as a sparkline. */
|
|
41
|
+
const SPARKLINE_HEIGHT = 60;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The size a ResponsiveContainer starts at: its own numbers where it has
|
|
45
|
+
* them. Otherwise the width follows the height: six times it for a
|
|
46
|
+
* sparkline, which sits in a narrow card or a table cell, and two and a half
|
|
47
|
+
* times it for a taller chart. The drawing then scales to the width the
|
|
48
|
+
* page gives it, so its text stays near its own size in most layouts.
|
|
49
|
+
* @param {{width?: unknown, height?: unknown, aspect?: unknown}} props
|
|
50
|
+
* @returns {{width: number, height: number}}
|
|
51
|
+
*/
|
|
52
|
+
export function chartSize({width, height, aspect}) {
|
|
53
|
+
const ratio = typeof aspect === 'number' && aspect > 0 ? aspect : null;
|
|
54
|
+
const w = typeof width === 'number' && width > 0 ? width : null;
|
|
55
|
+
const h =
|
|
56
|
+
typeof height === 'number' && height > 0
|
|
57
|
+
? height
|
|
58
|
+
: ratio
|
|
59
|
+
? (w ?? CHART_MAX_WIDTH) / ratio
|
|
60
|
+
: CHART_HEIGHT;
|
|
61
|
+
return {
|
|
62
|
+
width:
|
|
63
|
+
w ??
|
|
64
|
+
(ratio
|
|
65
|
+
? h * ratio
|
|
66
|
+
: Math.min(CHART_MAX_WIDTH, Math.round(h * (h <= SPARKLINE_HEIGHT ? 6 : 2.5)))),
|
|
67
|
+
height: h,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A chart drawn at its starting size scales to the width the page gives it
|
|
73
|
+
* and keeps its shape, so it never overflows its card. Its legend stays out
|
|
74
|
+
* of the scaled picture, so its text never covers the chart however narrow
|
|
75
|
+
* the page is. A horizontal legend takes its own row under the picture, or
|
|
76
|
+
* above it when it asks for the top. A vertical legend on one side sits
|
|
77
|
+
* beside the picture, on that side.
|
|
78
|
+
*/
|
|
79
|
+
export const CHART_CSS =
|
|
80
|
+
'.recharts-responsive-container{width:100%}' +
|
|
81
|
+
'.recharts-responsive-container>.recharts-wrapper{width:100%!important;height:auto!important;display:flex;flex-direction:column}' +
|
|
82
|
+
// Only the chart's own picture scales; a legend's icons keep their size.
|
|
83
|
+
'.recharts-responsive-container>.recharts-wrapper>svg.recharts-surface{width:100%!important;height:auto!important}' +
|
|
84
|
+
'.recharts-responsive-container>.recharts-wrapper>.recharts-legend-wrapper{position:static!important;width:auto!important;height:auto!important}' +
|
|
85
|
+
'.recharts-responsive-container>.recharts-wrapper:not([data-legend-side])>.recharts-legend-wrapper[style*="bottom"]{order:1}' +
|
|
86
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side]{flex-direction:row;align-items:flex-start}' +
|
|
87
|
+
// A side is physical, as in a chart of fixed size: a right-to-left page
|
|
88
|
+
// reverses the row so the legend stays on the side it asked for.
|
|
89
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side]:dir(rtl){flex-direction:row-reverse}' +
|
|
90
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side][data-legend-middle]{align-items:center}' +
|
|
91
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side][data-legend-bottom]{align-items:flex-end}' +
|
|
92
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side]>svg.recharts-surface{flex:1 1 0;min-width:0}' +
|
|
93
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side]>.recharts-legend-wrapper{flex:none}' +
|
|
94
|
+
// recharts puts the legend before the picture; one on the right goes after it.
|
|
95
|
+
'.recharts-responsive-container>.recharts-wrapper[data-legend-side="right"]>.recharts-legend-wrapper{order:1}';
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mark each container chart whose legend is vertical and on one side with
|
|
99
|
+
* that side, and whether it sits at the bottom or the middle, for
|
|
100
|
+
* {@link CHART_CSS}. A vertically centered legend has a `top` near the
|
|
101
|
+
* middle (no room is kept for it in a container chart). A vertical legend
|
|
102
|
+
* centered across takes its own row, as a horizontal one does.
|
|
103
|
+
* @param {any} root the element the charts were drawn in
|
|
104
|
+
*/
|
|
105
|
+
export function markLegends(root) {
|
|
106
|
+
for (const wrapper of root.querySelectorAll('.recharts-responsive-container > .recharts-wrapper')) {
|
|
107
|
+
const legend = wrapper.querySelector(':scope > .recharts-legend-wrapper');
|
|
108
|
+
if (!legend) continue;
|
|
109
|
+
const vertical = [...legend.querySelectorAll('.recharts-legend-item')].some(item =>
|
|
110
|
+
/display:\s*block/.test(item.getAttribute('style') ?? ''),
|
|
111
|
+
);
|
|
112
|
+
if (!vertical) continue;
|
|
113
|
+
const style = legend.getAttribute('style') ?? '';
|
|
114
|
+
// recharts places a right-aligned legend with `right`, a left-aligned one
|
|
115
|
+
// at the chart's left margin, and a centered one half way across.
|
|
116
|
+
const width = Number(wrapper.getAttribute('width'));
|
|
117
|
+
const left = Number(/(?:^|;)\s*left\s*:\s*(-?[\d.]+)px/.exec(style)?.[1]);
|
|
118
|
+
const side = /(?:^|;)\s*right\s*:/.test(style)
|
|
119
|
+
? 'right'
|
|
120
|
+
: Number.isFinite(left) && width > 0 && left < width * 0.2
|
|
121
|
+
? 'left'
|
|
122
|
+
: null;
|
|
123
|
+
if (!side) continue;
|
|
124
|
+
wrapper.setAttribute('data-legend-side', side);
|
|
125
|
+
const top = Number(/(?:^|;)\s*top\s*:\s*(-?[\d.]+)px/.exec(style)?.[1]);
|
|
126
|
+
const height = Number(wrapper.getAttribute('height'));
|
|
127
|
+
if (/(?:^|;)\s*bottom\s*:/.test(style)) wrapper.setAttribute('data-legend-bottom', '');
|
|
128
|
+
else if (top > 0 && height > 0 && top > height * 0.2) wrapper.setAttribute('data-legend-middle', '');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The box a legend would measure in a browser, estimated from its items, so
|
|
134
|
+
* a chart of fixed size keeps room for it where its props place it: rows for
|
|
135
|
+
* a horizontal legend, one line per item for a vertical one. A chart sized by
|
|
136
|
+
* its container keeps no room, because its legend takes its own row.
|
|
137
|
+
* @param {any} el the legend wrapper
|
|
138
|
+
*/
|
|
139
|
+
function legendBox(el) {
|
|
140
|
+
if (el.closest?.('.recharts-responsive-container')) return box(0, 0);
|
|
141
|
+
const items = [...el.querySelectorAll('.recharts-legend-item')];
|
|
142
|
+
const widths = items.map(item => 14 + 4 + 7 * (item.textContent ?? '').length + 10);
|
|
143
|
+
const vertical = items.some(item => /display:\s*block/.test(item.getAttribute('style') ?? ''));
|
|
144
|
+
if (vertical) return box(Math.max(0, ...widths), 20 * items.length + 10);
|
|
145
|
+
// A row as wide as the legend's own width holds the items it can; the rest
|
|
146
|
+
// wrap onto more rows.
|
|
147
|
+
const total = widths.reduce((a, b) => a + b, 0);
|
|
148
|
+
const room = Number(/(?:^|;)\s*width\s*:\s*([\d.]+)px/.exec(el.getAttribute('style') ?? '')?.[1]);
|
|
149
|
+
const rows = room > 0 ? Math.max(1, Math.ceil(total / room)) : 1;
|
|
150
|
+
return box(Math.min(total, room > 0 ? room : total), 24 * rows);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @param {number} width @param {number} height */
|
|
154
|
+
const box = (width, height) => ({x: 0, y: 0, top: 0, left: 0, right: width, bottom: height, width, height});
|
|
155
|
+
|
|
156
|
+
/** Font size of text that names none, as an axis tick's. */
|
|
157
|
+
const TEXT_SIZE = 14;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The box recharts' measurement span would measure: its text at its font
|
|
161
|
+
* size, about 0.6 em per character and 1.2 em high.
|
|
162
|
+
* @param {any} el
|
|
163
|
+
*/
|
|
164
|
+
function textBox(el) {
|
|
165
|
+
const fromStyle = el.style?.fontSize ?? /(?:^|;)\s*font-size\s*:\s*([^;]+)/.exec(el.getAttribute?.('style') ?? '')?.[1];
|
|
166
|
+
const size = Number.parseFloat(String(fromStyle ?? '')) || TEXT_SIZE;
|
|
167
|
+
return box(Math.round(String(el.textContent ?? '').length * size * 0.6), Math.round(size * 1.2));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Give this thread the small document a chart draws in: no layout, no
|
|
172
|
+
* motion, and no frames to wait for.
|
|
173
|
+
* @returns {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
export async function chartDocument() {
|
|
176
|
+
const {parseHTML} = await import('linkedom');
|
|
177
|
+
const {window} = parseHTML('<!doctype html><html><head></head><body></body></html>');
|
|
178
|
+
const g = /** @type {any} */ (globalThis);
|
|
179
|
+
const quiet = () => ({
|
|
180
|
+
matches: true,
|
|
181
|
+
addEventListener() {},
|
|
182
|
+
removeEventListener() {},
|
|
183
|
+
addListener() {},
|
|
184
|
+
removeListener() {},
|
|
185
|
+
});
|
|
186
|
+
Object.assign(window, {
|
|
187
|
+
matchMedia: quiet,
|
|
188
|
+
// Every style property reads as empty, as for an element with no CSS.
|
|
189
|
+
getComputedStyle: () => new Proxy({getPropertyValue: () => ''}, {get: (o, k) => (k in o ? /** @type {any} */ (o)[k] : '')}),
|
|
190
|
+
requestAnimationFrame: (/** @type {(t: number) => void} */ cb) => setTimeout(() => cb(Date.now()), 0),
|
|
191
|
+
cancelAnimationFrame: (/** @type {any} */ id) => clearTimeout(id),
|
|
192
|
+
ResizeObserver: class {
|
|
193
|
+
observe() {}
|
|
194
|
+
unobserve() {}
|
|
195
|
+
disconnect() {}
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
g.window = window;
|
|
199
|
+
g.document = window.document;
|
|
200
|
+
g.matchMedia = window.matchMedia;
|
|
201
|
+
g.getComputedStyle = window.getComputedStyle;
|
|
202
|
+
g.requestAnimationFrame = window.requestAnimationFrame;
|
|
203
|
+
g.cancelAnimationFrame = window.cancelAnimationFrame;
|
|
204
|
+
g.ResizeObserver = window.ResizeObserver;
|
|
205
|
+
// Nothing is laid out, so every box measures zero, except a legend's and
|
|
206
|
+
// the span recharts measures text with, which are estimated: the chart
|
|
207
|
+
// keeps room for its legend, and lays out its axis labels as in a browser.
|
|
208
|
+
Object.defineProperty(window.Element.prototype, 'getBoundingClientRect', {
|
|
209
|
+
configurable: true,
|
|
210
|
+
writable: true,
|
|
211
|
+
value() {
|
|
212
|
+
if (this.getAttribute?.('id') === 'recharts_measurement_span') return textBox(this);
|
|
213
|
+
return String(this.getAttribute?.('class') ?? '').includes('recharts-legend-wrapper') ? legendBox(this) : box(0, 0);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
@@ -23,12 +23,12 @@ export function importSpecifiers(source: string, file: string): string[];
|
|
|
23
23
|
*/
|
|
24
24
|
export function assetRefsIn(text: string): string[];
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* The path aliases (`compilerOptions.paths`) of the project that holds
|
|
27
|
+
* `dir`, by TypeScript's rules (see ts-paths.mjs).
|
|
28
28
|
* @param {string} dir
|
|
29
|
-
* @returns {[
|
|
29
|
+
* @returns {import('./ts-paths.mjs').PathPattern[]}
|
|
30
30
|
*/
|
|
31
|
-
export function receivingAliases(dir: string): [
|
|
31
|
+
export function receivingAliases(dir: string): import("./ts-paths.mjs").PathPattern[];
|
|
32
32
|
/**
|
|
33
33
|
* @typedef {object} Reach
|
|
34
34
|
* @property {boolean} relative - Reached by a relative import (or it is the
|
|
@@ -40,8 +40,8 @@ export function receivingAliases(dir: string): [string, string][];
|
|
|
40
40
|
* The embedded files the page reaches through its imports.
|
|
41
41
|
* @param {{path: string, base64: string}[]} files the embedded files, page
|
|
42
42
|
* first; every path appears once (callers refuse duplicates first)
|
|
43
|
-
* @param {{aliases?: [
|
|
44
|
-
* path aliases
|
|
43
|
+
* @param {{aliases?: import('./ts-paths.mjs').PathPattern[]}} [options] the
|
|
44
|
+
* receiving project's path aliases (see receivingAliases)
|
|
45
45
|
* @returns {{reached: Map<string, Reach>, missing: string[]}} the reached
|
|
46
46
|
* tree paths, and the relative imports the file does not carry
|
|
47
47
|
*/
|
|
@@ -49,7 +49,7 @@ export function reachableFiles(files: {
|
|
|
49
49
|
path: string;
|
|
50
50
|
base64: string;
|
|
51
51
|
}[], { aliases }?: {
|
|
52
|
-
aliases?: [
|
|
52
|
+
aliases?: import("./ts-paths.mjs").PathPattern[];
|
|
53
53
|
}): {
|
|
54
54
|
reached: Map<string, Reach>;
|
|
55
55
|
missing: string[];
|
|
@@ -18,6 +18,7 @@ import * as path from 'node:path';
|
|
|
18
18
|
import {parse} from '@babel/parser';
|
|
19
19
|
import {PAGE_EXTENSIONS} from './embedded-files.mjs';
|
|
20
20
|
import postcss from 'postcss';
|
|
21
|
+
import {matchPath, readPathsIn} from './ts-paths.mjs';
|
|
21
22
|
import {isAssetPath} from './assets.mjs';
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -131,41 +132,17 @@ function scanModule(source, file) {
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
134
|
-
*
|
|
135
|
-
*
|
|
135
|
+
* The path aliases (`compilerOptions.paths`) of the project that holds
|
|
136
|
+
* `dir`, by TypeScript's rules (see ts-paths.mjs).
|
|
136
137
|
* @param {string} dir
|
|
137
|
-
* @returns {[
|
|
138
|
+
* @returns {import('./ts-paths.mjs').PathPattern[]}
|
|
138
139
|
*/
|
|
139
140
|
export function receivingAliases(dir) {
|
|
140
141
|
let d = dir;
|
|
141
142
|
while (!fs.existsSync(d) && path.dirname(d) !== d) d = path.dirname(d);
|
|
142
143
|
for (;;) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (!fs.existsSync(file)) continue;
|
|
146
|
-
try {
|
|
147
|
-
const text = fs
|
|
148
|
-
.readFileSync(file, 'utf8')
|
|
149
|
-
.replace(/("(?:[^"\\]|\\.)*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str) => str ?? '')
|
|
150
|
-
.replace(/,(\s*[}\]])/g, '$1');
|
|
151
|
-
const options = JSON.parse(text)?.compilerOptions ?? {};
|
|
152
|
-
const base = path.resolve(d, typeof options.baseUrl === 'string' ? options.baseUrl : '.');
|
|
153
|
-
/** @type {[string, string][]} */
|
|
154
|
-
const out = [];
|
|
155
|
-
for (const [key, targets] of Object.entries(options.paths ?? {})) {
|
|
156
|
-
const target = Array.isArray(targets) ? targets[0] : undefined;
|
|
157
|
-
if (typeof target !== 'string') continue;
|
|
158
|
-
if (key.endsWith('/*') && target.endsWith('/*')) {
|
|
159
|
-
out.push([key.slice(0, -1), `${path.resolve(base, target.slice(0, -2))}${path.sep}`]);
|
|
160
|
-
} else if (!key.includes('*') && !target.includes('*')) {
|
|
161
|
-
out.push([key, path.resolve(base, target)]);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
} catch {
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
}
|
|
144
|
+
const found = readPathsIn(d);
|
|
145
|
+
if (found) return found;
|
|
169
146
|
if (fs.existsSync(path.join(d, 'package.json'))) return [];
|
|
170
147
|
const up = path.dirname(d);
|
|
171
148
|
if (up === d) return [];
|
|
@@ -185,8 +162,8 @@ export function receivingAliases(dir) {
|
|
|
185
162
|
* The embedded files the page reaches through its imports.
|
|
186
163
|
* @param {{path: string, base64: string}[]} files the embedded files, page
|
|
187
164
|
* first; every path appears once (callers refuse duplicates first)
|
|
188
|
-
* @param {{aliases?: [
|
|
189
|
-
* path aliases
|
|
165
|
+
* @param {{aliases?: import('./ts-paths.mjs').PathPattern[]}} [options] the
|
|
166
|
+
* receiving project's path aliases (see receivingAliases)
|
|
190
167
|
* @returns {{reached: Map<string, Reach>, missing: string[]}} the reached
|
|
191
168
|
* tree paths, and the relative imports the file does not carry
|
|
192
169
|
*/
|
|
@@ -285,29 +262,61 @@ export function reachableFiles(files, {aliases = []} = {}) {
|
|
|
285
262
|
if (fresh) queue.push(target);
|
|
286
263
|
continue;
|
|
287
264
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
// (`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
?
|
|
304
|
-
:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
265
|
+
const match = matchPath(aliases, bare);
|
|
266
|
+
if (match) {
|
|
267
|
+
// Each target in order: the file the alias names, matched in the
|
|
268
|
+
// tree by the path's tail, and placed where this project's target
|
|
269
|
+
// puts it (`@/c/Header` -> `<target>/c/Header.tsx`). The target whose
|
|
270
|
+
// folders match the most whole segments of the sent path above the
|
|
271
|
+
// file wins; with no match, the first target does.
|
|
272
|
+
/** @type {{abs: string, hit: {path: string, tail: string}, score: number}[]} */
|
|
273
|
+
const options = [];
|
|
274
|
+
for (const target of match.pattern.targets) {
|
|
275
|
+
const named = target.replace('*', match.capture);
|
|
276
|
+
const ext = path.extname(named);
|
|
277
|
+
const star = target.indexOf('*');
|
|
278
|
+
// The tail starts at the path segment that holds the `*`, so text
|
|
279
|
+
// before it (`icons/icon-*`) is part of the file name.
|
|
280
|
+
const cut = star === -1 ? -1 : target.lastIndexOf(path.sep, star) + 1;
|
|
281
|
+
const rest = (star === -1 ? path.basename(named, ext) : target.slice(cut).replace('*', match.capture))
|
|
282
|
+
.split(path.sep)
|
|
283
|
+
.join('/')
|
|
284
|
+
.replace(/^\/+/, '');
|
|
285
|
+
const hit = rest ? bySuffix(rest) : null;
|
|
286
|
+
if (!hit) continue;
|
|
287
|
+
// The file keeps its own extension or index: `<target>/c/Header` ->
|
|
288
|
+
// `<target>/c/Header.tsx`.
|
|
289
|
+
const abs =
|
|
290
|
+
star !== -1
|
|
291
|
+
? target.slice(0, cut) + hit.tail
|
|
292
|
+
: ext
|
|
293
|
+
? named
|
|
294
|
+
: named + hit.tail.slice(rest.length);
|
|
295
|
+
// The folders that hold the target's `*` (or, for a target with
|
|
296
|
+
// none, the target's own folders), against the folders of the sent
|
|
297
|
+
// path above the part the alias named, from the nearest up.
|
|
298
|
+
const targetFolders = (star === -1 ? path.dirname(named) : target.slice(0, cut)).split(path.sep).filter(Boolean);
|
|
299
|
+
const sentTail = hit.tail;
|
|
300
|
+
const sentFolders = hit.path.slice(0, hit.path.length - sentTail.length).split('/').filter(Boolean);
|
|
301
|
+
let score = 0;
|
|
302
|
+
while (
|
|
303
|
+
score < sentFolders.length &&
|
|
304
|
+
score < targetFolders.length &&
|
|
305
|
+
sentFolders[sentFolders.length - 1 - score] === targetFolders[targetFolders.length - 1 - score]
|
|
306
|
+
) {
|
|
307
|
+
score += 1;
|
|
308
|
+
}
|
|
309
|
+
options.push({abs, hit, score});
|
|
310
|
+
}
|
|
311
|
+
const chosen = options.reduce(
|
|
312
|
+
(/** @type {(typeof options)[number] | undefined} */ best, o) => (!best || o.score > best.score ? o : best),
|
|
313
|
+
undefined,
|
|
314
|
+
);
|
|
315
|
+
if (chosen) {
|
|
316
|
+
const {r, fresh} = reach(chosen.hit.path);
|
|
317
|
+
r.aliased.add(chosen.abs);
|
|
318
|
+
if (fresh) queue.push(chosen.hit.path);
|
|
319
|
+
}
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Host process for `astryx experimental render`: runs page-worker.mjs
|
|
5
|
+
* in a worker thread and sends its one answer to the CLI.
|
|
6
|
+
*
|
|
7
|
+
* A crash of Node itself ends only this process, never the CLI. The page runs
|
|
8
|
+
* in a thread, so this process can still act while the page loops: it exits
|
|
9
|
+
* when the CLI goes away (the IPC channel closes) or when its own time limit
|
|
10
|
+
* passes, so a page that never finishes never outlives the CLI.
|
|
11
|
+
*
|
|
12
|
+
* @input argv[2]: JSON {entry, root, served, withoutSegmenter, timeoutMs}
|
|
13
|
+
* @output one IPC message: the worker's answer, or {hostError: message}, or
|
|
14
|
+
* {hostExit: code}
|
|
15
|
+
* @position foundation/render — started by render-page.mjs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {Worker} from 'node:worker_threads';
|
|
19
|
+
|
|
20
|
+
/** @type {{entry: string, root: string, served: {dir: string, integrations: string[]} | null, withoutSegmenter: boolean, timeoutMs: number}} */
|
|
21
|
+
const input = JSON.parse(process.argv[2] ?? '{}');
|
|
22
|
+
|
|
23
|
+
/** @param {unknown} message */
|
|
24
|
+
function answer(message) {
|
|
25
|
+
if (!process.connected) process.exit(0);
|
|
26
|
+
process.send?.(message, () => process.exit(0));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.once('disconnect', () => process.exit(0));
|
|
30
|
+
// A margin past the CLI's own limit, so the CLI reports the timeout first.
|
|
31
|
+
setTimeout(() => process.exit(0), input.timeoutMs + 5000);
|
|
32
|
+
|
|
33
|
+
const worker = new Worker(new URL('./page-worker.mjs', import.meta.url), {
|
|
34
|
+
workerData: input,
|
|
35
|
+
execArgv: [],
|
|
36
|
+
stdout: true,
|
|
37
|
+
stderr: true,
|
|
38
|
+
resourceLimits: {maxOldGenerationSizeMb: 4096},
|
|
39
|
+
});
|
|
40
|
+
// The page's own writes to stdout/stderr never reach the CLI's output.
|
|
41
|
+
worker.stdout.resume();
|
|
42
|
+
worker.stderr.resume();
|
|
43
|
+
worker.once('message', answer);
|
|
44
|
+
worker.once('error', (/** @type {any} */ error) => answer({hostError: String(error?.message ?? error)}));
|
|
45
|
+
worker.once('exit', code => answer({hostExit: code}));
|