@astryxdesign/cli 0.4.3 → 0.4.4-canary.0f4d45f
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 +40 -0
- package/README.md +45 -43
- package/api/discover/_adapter.d.mts +5 -3
- package/api/discover/_adapter.mjs +6 -4
- package/api/template/cdn/cdn.d.mts +23 -0
- package/api/template/cdn/cdn.mjs +86 -0
- package/api/template/cdn/cdn.test.mjs +108 -0
- package/api/template/template.d.mts +2 -0
- package/api/template/template.doc.mjs +21 -3
- package/api/template/template.mjs +16 -2
- package/api/template/template.type.d.mts +19 -0
- package/api/template/template.type.mjs +12 -0
- package/api/theme/theme.type.d.mts +16 -0
- package/api/theme/theme.type.mjs +11 -0
- package/assets/cdn.template.html +124 -0
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +5 -4
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.doc.mjs +21 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetHeights.tsx +46 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.doc.mjs +23 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetMobileKeyboard.tsx +100 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetNoScrim.tsx +47 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetShowcase.tsx +51 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.doc.mjs +26 -0
- package/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherShowcase.tsx +221 -0
- package/assets/theme.template.ts +4 -3
- package/authoring/doctypes/base/type.ts +9 -0
- package/clients/cli/commands/build-theme.mjs +154 -65
- package/clients/cli/commands/build-theme.multi.test.mjs +148 -0
- package/clients/cli/commands/build-theme.watch.test.mjs +67 -0
- package/clients/cli/commands/discover.broken-integration.test.mjs +112 -0
- package/clients/cli/commands/discover.mjs +12 -0
- package/clients/cli/commands/search.mjs +9 -0
- package/clients/cli/commands/template-cdn.behavior.test.mjs +113 -0
- package/clients/cli/commands/template.doc.mjs +12 -2
- package/clients/cli/commands/template.mjs +24 -3
- package/clients/cli/commands/theme-build.doc.mjs +11 -5
- package/clients/cli/lib/json-shim.test.mjs +1 -1
- package/clients/cli/lib/manifest.mjs +3 -2
- package/clients/cli/lib/manifest.test.mjs +4 -2
- package/foundation/integrations/integration-warnings.test.mjs +17 -0
- package/foundation/integrations/validate-contributions.mjs +7 -0
- package/foundation/response/response-types.doc.mjs +11 -0
- package/foundation/text/copyright-header.mjs +11 -4
- package/package.json +12 -9
- package/assets/templates/pages/table-page-chart/page.tsx +0 -577
- package/assets/templates/pages/table-page-chart/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-heatmap-status/page.tsx +0 -467
- package/assets/templates/pages/table-page-heatmap-status/template.doc.mjs +0 -13
- package/assets/templates/pages/table-page-shoe-store-heatmap/page.tsx +0 -931
- package/assets/templates/pages/table-page-shoe-store-heatmap/template.doc.mjs +0 -13
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import {useState} from 'react';
|
|
6
|
+
import {BottomSheet, BottomSheetSwitcher} from '@astryxdesign/core/BottomSheet';
|
|
7
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
8
|
+
import {CheckboxInput} from '@astryxdesign/core/CheckboxInput';
|
|
9
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
10
|
+
import {Heading} from '@astryxdesign/core/Heading';
|
|
11
|
+
import {Section} from '@astryxdesign/core/Section';
|
|
12
|
+
import {HStack, VStack} from '@astryxdesign/core/Stack';
|
|
13
|
+
import {Text} from '@astryxdesign/core/Text';
|
|
14
|
+
import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
|
|
15
|
+
|
|
16
|
+
type NotificationSheetHeight = 'hug' | 'capped';
|
|
17
|
+
|
|
18
|
+
interface NotificationOverviewSheetProps {
|
|
19
|
+
height: NotificationSheetHeight;
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
onContinue: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function NotificationOverviewSheet({
|
|
25
|
+
height,
|
|
26
|
+
onCancel,
|
|
27
|
+
onContinue,
|
|
28
|
+
}: NotificationOverviewSheetProps) {
|
|
29
|
+
return (
|
|
30
|
+
<BottomSheet
|
|
31
|
+
sheetId="overview"
|
|
32
|
+
label="Set up notifications"
|
|
33
|
+
height={height}>
|
|
34
|
+
<Section padding={4}>
|
|
35
|
+
<VStack gap={4}>
|
|
36
|
+
<VStack gap={1}>
|
|
37
|
+
<Heading level={3}>Set up notifications</Heading>
|
|
38
|
+
<Text type="supporting" color="secondary">
|
|
39
|
+
Step 1 of 3
|
|
40
|
+
</Text>
|
|
41
|
+
</VStack>
|
|
42
|
+
<Divider />
|
|
43
|
+
<Text type="supporting" color="secondary">
|
|
44
|
+
Stay informed about activity that matters without checking back
|
|
45
|
+
throughout the day.
|
|
46
|
+
</Text>
|
|
47
|
+
<VStack gap={3}>
|
|
48
|
+
<VStack gap={1}>
|
|
49
|
+
<Text type="label">Important activity</Text>
|
|
50
|
+
<Text type="supporting" color="secondary">
|
|
51
|
+
Know when someone mentions you or needs your attention.
|
|
52
|
+
</Text>
|
|
53
|
+
</VStack>
|
|
54
|
+
<VStack gap={1}>
|
|
55
|
+
<Text type="label">Timely reminders</Text>
|
|
56
|
+
<Text type="supporting" color="secondary">
|
|
57
|
+
Get a reminder before work reaches its due date.
|
|
58
|
+
</Text>
|
|
59
|
+
</VStack>
|
|
60
|
+
<VStack gap={1}>
|
|
61
|
+
<Text type="label">Useful summaries</Text>
|
|
62
|
+
<Text type="supporting" color="secondary">
|
|
63
|
+
Catch up on anything you may have missed.
|
|
64
|
+
</Text>
|
|
65
|
+
</VStack>
|
|
66
|
+
</VStack>
|
|
67
|
+
<HStack gap={2} hAlign="end">
|
|
68
|
+
<Button label="Cancel" variant="secondary" onClick={onCancel} />
|
|
69
|
+
<Button label="Continue" onClick={onContinue} />
|
|
70
|
+
</HStack>
|
|
71
|
+
</VStack>
|
|
72
|
+
</Section>
|
|
73
|
+
</BottomSheet>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface NotificationFrequencySheetProps {
|
|
78
|
+
height: NotificationSheetHeight;
|
|
79
|
+
onBack: () => void;
|
|
80
|
+
onContinue: () => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function NotificationFrequencySheet({
|
|
84
|
+
height,
|
|
85
|
+
onBack,
|
|
86
|
+
onContinue,
|
|
87
|
+
}: NotificationFrequencySheetProps) {
|
|
88
|
+
const [frequency, setFrequency] = useState('daily');
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<BottomSheet
|
|
92
|
+
sheetId="frequency"
|
|
93
|
+
label="Notification frequency"
|
|
94
|
+
height={height}>
|
|
95
|
+
<Section padding={4}>
|
|
96
|
+
<VStack gap={4}>
|
|
97
|
+
<VStack gap={1}>
|
|
98
|
+
<Heading level={3}>How often?</Heading>
|
|
99
|
+
<Text type="supporting" color="secondary">
|
|
100
|
+
Step 2 of 3
|
|
101
|
+
</Text>
|
|
102
|
+
</VStack>
|
|
103
|
+
<Divider />
|
|
104
|
+
<RadioList
|
|
105
|
+
label="Notification frequency"
|
|
106
|
+
isLabelHidden
|
|
107
|
+
value={frequency}
|
|
108
|
+
onChange={setFrequency}>
|
|
109
|
+
<RadioListItem label="Immediately" value="immediately" />
|
|
110
|
+
<RadioListItem label="Daily" value="daily" />
|
|
111
|
+
<RadioListItem label="Weekly" value="weekly" />
|
|
112
|
+
</RadioList>
|
|
113
|
+
<HStack gap={2} hAlign="end">
|
|
114
|
+
<Button label="Back" variant="secondary" onClick={onBack} />
|
|
115
|
+
<Button label="Continue" onClick={onContinue} />
|
|
116
|
+
</HStack>
|
|
117
|
+
</VStack>
|
|
118
|
+
</Section>
|
|
119
|
+
</BottomSheet>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface NotificationChannelsSheetProps {
|
|
124
|
+
height: NotificationSheetHeight;
|
|
125
|
+
onBack: () => void;
|
|
126
|
+
onFinish: () => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function NotificationChannelsSheet({
|
|
130
|
+
height,
|
|
131
|
+
onBack,
|
|
132
|
+
onFinish,
|
|
133
|
+
}: NotificationChannelsSheetProps) {
|
|
134
|
+
const [email, setEmail] = useState(true);
|
|
135
|
+
const [pushNotifications, setPushNotifications] = useState(true);
|
|
136
|
+
const [textMessages, setTextMessages] = useState(false);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<BottomSheet
|
|
140
|
+
sheetId="channels"
|
|
141
|
+
label="Notification channels"
|
|
142
|
+
height={height}>
|
|
143
|
+
<Section padding={4}>
|
|
144
|
+
<VStack gap={4}>
|
|
145
|
+
<VStack gap={1}>
|
|
146
|
+
<Heading level={3}>Where should we notify you?</Heading>
|
|
147
|
+
<Text type="supporting" color="secondary">
|
|
148
|
+
Step 3 of 3
|
|
149
|
+
</Text>
|
|
150
|
+
</VStack>
|
|
151
|
+
<Divider />
|
|
152
|
+
<Text type="supporting" color="secondary">
|
|
153
|
+
Choose any combination. You can change these preferences later.
|
|
154
|
+
</Text>
|
|
155
|
+
<VStack gap={2}>
|
|
156
|
+
<CheckboxInput label="Email" value={email} onChange={setEmail} />
|
|
157
|
+
<CheckboxInput
|
|
158
|
+
label="Push notifications"
|
|
159
|
+
value={pushNotifications}
|
|
160
|
+
onChange={setPushNotifications}
|
|
161
|
+
/>
|
|
162
|
+
<CheckboxInput
|
|
163
|
+
label="Text messages"
|
|
164
|
+
value={textMessages}
|
|
165
|
+
onChange={setTextMessages}
|
|
166
|
+
/>
|
|
167
|
+
</VStack>
|
|
168
|
+
<HStack gap={2} hAlign="end">
|
|
169
|
+
<Button label="Back" variant="secondary" onClick={onBack} />
|
|
170
|
+
<Button label="Finish" onClick={onFinish} />
|
|
171
|
+
</HStack>
|
|
172
|
+
</VStack>
|
|
173
|
+
</Section>
|
|
174
|
+
</BottomSheet>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface MultiStepSwitcherExampleProps {
|
|
179
|
+
height: NotificationSheetHeight;
|
|
180
|
+
hasScrim?: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function MultiStepSwitcherExample({
|
|
184
|
+
height,
|
|
185
|
+
hasScrim = true,
|
|
186
|
+
}: MultiStepSwitcherExampleProps) {
|
|
187
|
+
const [activeSheet, setActiveSheet] = useState<string | null>(null);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<Button
|
|
192
|
+
label="Set up notifications"
|
|
193
|
+
onClick={() => setActiveSheet('overview')}
|
|
194
|
+
/>
|
|
195
|
+
<BottomSheetSwitcher
|
|
196
|
+
activeSheet={activeSheet}
|
|
197
|
+
onActiveSheetChange={setActiveSheet}
|
|
198
|
+
hasScrim={hasScrim}>
|
|
199
|
+
<NotificationOverviewSheet
|
|
200
|
+
height={height}
|
|
201
|
+
onCancel={() => setActiveSheet(null)}
|
|
202
|
+
onContinue={() => setActiveSheet('frequency')}
|
|
203
|
+
/>
|
|
204
|
+
<NotificationFrequencySheet
|
|
205
|
+
height={height}
|
|
206
|
+
onBack={() => setActiveSheet('overview')}
|
|
207
|
+
onContinue={() => setActiveSheet('channels')}
|
|
208
|
+
/>
|
|
209
|
+
<NotificationChannelsSheet
|
|
210
|
+
height={height}
|
|
211
|
+
onBack={() => setActiveSheet('frequency')}
|
|
212
|
+
onFinish={() => setActiveSheet(null)}
|
|
213
|
+
/>
|
|
214
|
+
</BottomSheetSwitcher>
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default function BottomSheetSwitcherShowcase() {
|
|
220
|
+
return <MultiStepSwitcherExample height="hug" />;
|
|
221
|
+
}
|
package/assets/theme.template.ts
CHANGED
|
@@ -105,12 +105,13 @@ export const myTheme = defineTheme({
|
|
|
105
105
|
// ───────────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Generates the neutral ramp and the accent tokens from
|
|
108
|
+
* Generates the neutral ramp and the accent tokens from a seed colour
|
|
109
109
|
* using the HCT perceptual model: surfaces, text, icons, borders, muted
|
|
110
110
|
* fills, hover and pressed overlays — light and dark both.
|
|
111
111
|
*
|
|
112
|
-
* accent seed hex
|
|
113
|
-
* the
|
|
112
|
+
* accent seed hex, or a [light, dark] pair to seed each scheme's
|
|
113
|
+
* palette from its own colour; omit to keep the default
|
|
114
|
+
* accent and re-tone only the neutrals
|
|
114
115
|
* neutralStyle 'warm' | 'cool' | 'neutral' — the temperature of the greys
|
|
115
116
|
* contrast 'standard' | 'high' — 'high' widens the text/surface tone
|
|
116
117
|
* gap, for dense data UI or bright and clinical screens
|
|
@@ -117,6 +117,15 @@ export interface ComponentPlaygroundConfig {
|
|
|
117
117
|
* the component is visible on load and knobs stay usable, whereas a real
|
|
118
118
|
* top-layer modal makes the rest of the page inert (#3657). */
|
|
119
119
|
overlay?: boolean;
|
|
120
|
+
/** Override the controlled prop and open value used by an overlay preview.
|
|
121
|
+
* Use with `overlay: true` when the component does not use `isOpen: true`
|
|
122
|
+
* to represent its open state. */
|
|
123
|
+
overlayControl?: {
|
|
124
|
+
/** Controlled prop that determines whether the overlay is open. */
|
|
125
|
+
stateProp: string;
|
|
126
|
+
/** Value assigned to stateProp when the preview's open button is used. */
|
|
127
|
+
openValue: unknown;
|
|
128
|
+
};
|
|
120
129
|
/** The component reads AppShell mobile context and renders nothing
|
|
121
130
|
* without it (e.g. `MobileNavToggle` returns null unless the context
|
|
122
131
|
* reports an enabled mobile viewport — the default value outside
|
|
@@ -17,6 +17,12 @@
|
|
|
17
17
|
* Usage:
|
|
18
18
|
* astryx theme build ./src/themes/ocean.ts
|
|
19
19
|
* astryx theme build ./src/themes/ocean.ts --out ./dist/ocean.css
|
|
20
|
+
* astryx theme build ./src/themes/*.ts
|
|
21
|
+
*
|
|
22
|
+
* `build` takes one or more theme files. Each is compiled by the same
|
|
23
|
+
* single-file API call, in argument order, in one process — so the outputs are
|
|
24
|
+
* byte-identical to running the CLI once per theme, and the first failure stops
|
|
25
|
+
* the run exactly as a shell loop under `set -e` would.
|
|
20
26
|
*/
|
|
21
27
|
|
|
22
28
|
import * as fs from 'node:fs';
|
|
@@ -80,66 +86,80 @@ function runThemeBuildOnceChild(file, options) {
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
/**
|
|
83
|
-
* Watch
|
|
84
|
-
* rebuilds (debounced)
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
89
|
+
* Watch theme files and rebuild on change. Runs an initial build of each, then
|
|
90
|
+
* rebuilds (debounced) the file that changed, until interrupted with Ctrl-C.
|
|
91
|
+
* Rebuilds are serialized: one at a time, in the order the changes arrived, so
|
|
92
|
+
* the log stays readable. Each rebuild runs in a child process so a build error
|
|
93
|
+
* (which the single-build path reports via a hard exit) is contained and the
|
|
94
|
+
* watcher keeps running.
|
|
88
95
|
*
|
|
89
|
-
* @param {string}
|
|
90
|
-
*
|
|
96
|
+
* @param {Array<{file: string, filePath: string}>} entries - The theme file
|
|
97
|
+
* arguments as the user passed them, with their resolved absolute paths.
|
|
91
98
|
* @param {{out?: string, iconsSpecifier?: string}} options - Parsed command options.
|
|
92
99
|
* @returns {Promise<void>} Resolves when the watcher is stopped (Ctrl-C).
|
|
93
100
|
*/
|
|
94
|
-
async function runThemeBuildWatch(
|
|
95
|
-
const rel =
|
|
101
|
+
async function runThemeBuildWatch(entries, options) {
|
|
102
|
+
const rel = (/** @type {string} */ filePath) =>
|
|
103
|
+
path.relative(process.cwd(), filePath);
|
|
104
|
+
const watchingLine = `\nWatching ${entries
|
|
105
|
+
.map(e => rel(e.filePath))
|
|
106
|
+
.join(', ')} for changes — press Ctrl-C to stop.`;
|
|
96
107
|
|
|
97
108
|
// Initial build.
|
|
98
|
-
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
await runThemeBuildOnceChild(entry.file, options);
|
|
111
|
+
}
|
|
99
112
|
|
|
100
113
|
let building = false;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
/** @type {Set<{file: string, filePath: string}>} */
|
|
115
|
+
const queued = new Set();
|
|
116
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
117
|
+
const debounces = new Map();
|
|
104
118
|
|
|
105
|
-
|
|
119
|
+
/** @param {{file: string, filePath: string}} entry */
|
|
120
|
+
const rebuild = async entry => {
|
|
106
121
|
if (building) {
|
|
107
122
|
// Coalesce changes that land mid-build into a single follow-up run.
|
|
108
|
-
queued
|
|
123
|
+
queued.add(entry);
|
|
109
124
|
return;
|
|
110
125
|
}
|
|
111
126
|
building = true;
|
|
112
|
-
emit(text(`\nChange detected — rebuilding ${rel}...`));
|
|
113
|
-
await runThemeBuildOnceChild(file, options);
|
|
127
|
+
emit(text(`\nChange detected — rebuilding ${rel(entry.filePath)}...`));
|
|
128
|
+
await runThemeBuildOnceChild(entry.file, options);
|
|
114
129
|
building = false;
|
|
115
|
-
emit(text(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
130
|
+
emit(text(watchingLine));
|
|
131
|
+
const next = queued.values().next();
|
|
132
|
+
if (!next.done) {
|
|
133
|
+
queued.delete(next.value);
|
|
134
|
+
rebuild(next.value);
|
|
119
135
|
}
|
|
120
136
|
};
|
|
121
137
|
|
|
122
138
|
// Some editors replace the file (rename) rather than writing in place, which
|
|
123
139
|
// can drop the watch. Watch the containing directory and filter to our file
|
|
124
140
|
// so edits survive atomic-save/rename.
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
const watchers = entries.map(entry => {
|
|
142
|
+
const baseName = path.basename(entry.filePath);
|
|
143
|
+
return fs.watch(path.dirname(entry.filePath), (_eventType, changed) => {
|
|
144
|
+
if (changed && changed !== baseName) return;
|
|
145
|
+
clearTimeout(debounces.get(entry.filePath));
|
|
146
|
+
// Debounce: editors often emit several events per save.
|
|
147
|
+
debounces.set(
|
|
148
|
+
entry.filePath,
|
|
149
|
+
setTimeout(() => rebuild(entry), 100),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
132
152
|
});
|
|
133
153
|
|
|
134
154
|
// Announce readiness only AFTER fs.watch is armed — the log is the "safe to
|
|
135
155
|
// edit" signal (tests and humans rely on it), so printing it before the watch
|
|
136
156
|
// is registered would race: a change in that gap is silently missed.
|
|
137
|
-
emit(text(
|
|
157
|
+
emit(text(watchingLine));
|
|
138
158
|
|
|
139
159
|
await new Promise((/** @type {(value?: void) => void} */ resolve) => {
|
|
140
160
|
const stop = () => {
|
|
141
|
-
clearTimeout(
|
|
142
|
-
|
|
161
|
+
for (const d of debounces.values()) clearTimeout(d);
|
|
162
|
+
for (const w of watchers) w.close();
|
|
143
163
|
emit(text('\nStopped watching.'));
|
|
144
164
|
resolve();
|
|
145
165
|
};
|
|
@@ -208,15 +228,31 @@ export function registerTheme(program) {
|
|
|
208
228
|
defineCommand(theme, themeBuildCommand, {
|
|
209
229
|
fn: themeBuildFn,
|
|
210
230
|
action: async (
|
|
211
|
-
/** @type {string} */
|
|
231
|
+
/** @type {string[]} */ files,
|
|
212
232
|
/** @type {{out?: string, watch?: boolean, check?: boolean, iconsSpecifier?: string}} */ options,
|
|
213
233
|
) => {
|
|
214
|
-
const filePath = path.resolve(process.cwd(), file);
|
|
215
234
|
const json = program.opts().json || false;
|
|
235
|
+
const entries = files.map(file => ({
|
|
236
|
+
file,
|
|
237
|
+
filePath: path.resolve(process.cwd(), file),
|
|
238
|
+
}));
|
|
216
239
|
|
|
217
|
-
|
|
218
|
-
|
|
240
|
+
for (const entry of entries) {
|
|
241
|
+
if (fs.existsSync(entry.filePath)) continue;
|
|
242
|
+
// A quoted glob reaches us unexpanded: say so rather than reporting a
|
|
243
|
+
// literal `themes/*.ts` as a missing file.
|
|
244
|
+
const looksGlobby = /[*?[\]{}]/.test(entry.file);
|
|
245
|
+
cliError(`File not found: ${entry.filePath}`, {
|
|
219
246
|
code: ERROR_CODES.ERR_FILE_NOT_FOUND,
|
|
247
|
+
suggestions: looksGlobby
|
|
248
|
+
? [
|
|
249
|
+
{
|
|
250
|
+
name: `astryx theme build ${entry.file.replace(/['"]/g, '')}`,
|
|
251
|
+
reason:
|
|
252
|
+
'globs are expanded by your shell — pass the pattern unquoted, or list the files',
|
|
253
|
+
},
|
|
254
|
+
]
|
|
255
|
+
: undefined,
|
|
220
256
|
});
|
|
221
257
|
return;
|
|
222
258
|
}
|
|
@@ -230,6 +266,19 @@ export function registerTheme(program) {
|
|
|
230
266
|
return;
|
|
231
267
|
}
|
|
232
268
|
|
|
269
|
+
// --out names one output file, so it cannot describe N themes. Without
|
|
270
|
+
// it each theme writes `<theme name>.css` beside its source, which is
|
|
271
|
+
// what a multi-theme build wants anyway.
|
|
272
|
+
if (options.out && entries.length > 1) {
|
|
273
|
+
cliError(
|
|
274
|
+
`--out takes a single output path and ${entries.length} theme files were given. ` +
|
|
275
|
+
'Build them without --out (each theme writes <name>.css next to its source), ' +
|
|
276
|
+
'or run one invocation per theme.',
|
|
277
|
+
{code: ERROR_CODES.ERR_THEME_INVALID},
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
233
282
|
// Watch mode: run an initial build, then rebuild on every change to the
|
|
234
283
|
// theme file. Watch is a human-interactive, long-running mode — it is not
|
|
235
284
|
// supported in --json (machine) mode, which expects a single envelope.
|
|
@@ -240,41 +289,81 @@ export function registerTheme(program) {
|
|
|
240
289
|
});
|
|
241
290
|
return;
|
|
242
291
|
}
|
|
243
|
-
await runThemeBuildWatch(
|
|
292
|
+
await runThemeBuildWatch(entries, options);
|
|
244
293
|
return;
|
|
245
294
|
}
|
|
246
295
|
|
|
247
|
-
// Non-watch: delegate to the API compiler
|
|
248
|
-
// --json mode (log → stdout via
|
|
249
|
-
// "Building theme from" line, the
|
|
250
|
-
// instructions are all emitted from
|
|
296
|
+
// Non-watch: delegate to the API compiler, once per theme, in argument
|
|
297
|
+
// order. Enable human output unless in --json mode (log → stdout via
|
|
298
|
+
// humanLog, warn/error → stderr). The "Building theme from" line, the
|
|
299
|
+
// ✓/warning lines, and the install instructions are all emitted from
|
|
300
|
+
// inside themeBuild via the shared logger.
|
|
251
301
|
logger.setSilent(json);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
302
|
+
/** @type {Array<{file: string, receipt: import('../../../api/theme/theme.type.mjs').ThemeBuildResponse | import('../../../api/theme/theme.type.mjs').ThemeBuildCheckResponse | null}>} */
|
|
303
|
+
const results = [];
|
|
304
|
+
let stale = false;
|
|
305
|
+
for (const entry of entries) {
|
|
306
|
+
try {
|
|
307
|
+
const result = await themeBuild(
|
|
308
|
+
entry.file,
|
|
309
|
+
{
|
|
310
|
+
out: options.out,
|
|
311
|
+
check: options.check,
|
|
312
|
+
iconsSpecifier: options.iconsSpecifier,
|
|
313
|
+
},
|
|
314
|
+
{cwd: process.cwd()},
|
|
315
|
+
);
|
|
316
|
+
results.push({file: entry.file, receipt: result ?? null});
|
|
317
|
+
if (
|
|
318
|
+
options.check &&
|
|
319
|
+
result &&
|
|
320
|
+
result.type === 'theme.build.check' &&
|
|
321
|
+
!result.data.upToDate
|
|
322
|
+
) {
|
|
323
|
+
stale = true;
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
const err =
|
|
327
|
+
/** @type {import('../../../api/error.mjs').AstryxError} */ (e);
|
|
328
|
+
// Stop at the first failure, as a shell loop under `set -e` does.
|
|
329
|
+
// With several themes in flight the message alone rarely says which
|
|
330
|
+
// one broke, so name it.
|
|
331
|
+
cliError(
|
|
332
|
+
entries.length > 1 ? `${entry.file}: ${err.message}` : err.message,
|
|
333
|
+
{suggestions: err.suggestions, code: err.code},
|
|
334
|
+
);
|
|
335
|
+
return;
|
|
273
336
|
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (json) {
|
|
340
|
+
// One theme keeps the single-envelope contract it has always had; a
|
|
341
|
+
// batch gets its own discriminant rather than N envelopes on stdout.
|
|
342
|
+
if (entries.length === 1) {
|
|
343
|
+
if (results[0].receipt) jsonOut(results[0].receipt);
|
|
344
|
+
} else {
|
|
345
|
+
/** @type {import('../../../api/theme/theme.type.mjs').ThemeBuildBatchResponse} */
|
|
346
|
+
const batch = {
|
|
347
|
+
type: 'theme.build.batch',
|
|
348
|
+
data: {count: results.length, results},
|
|
349
|
+
};
|
|
350
|
+
jsonOut(batch);
|
|
351
|
+
}
|
|
352
|
+
} else if (entries.length > 1) {
|
|
353
|
+
emit(
|
|
354
|
+
text(
|
|
355
|
+
options.check
|
|
356
|
+
? `\n${stale ? '✗' : '✓'} Checked ${entries.length} themes.`
|
|
357
|
+
: `\n✓ Built ${entries.length} themes.`,
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// In --check mode a stale/missing output is a failure: exit non-zero
|
|
363
|
+
// (after emitting the receipt) so CI can gate on it. The receipt is
|
|
364
|
+
// already printed above (shared logger or --json envelope).
|
|
365
|
+
if (options.check && stale) {
|
|
366
|
+
process.exitCode = 1;
|
|
278
367
|
}
|
|
279
368
|
},
|
|
280
369
|
});
|