handoff-app 1.2.0-0 → 1.2.0-2
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/dist/app-builder/build.js +24 -10
- package/dist/app-builder/config-diff/index.d.ts +1 -1
- package/dist/app-builder/config-diff/registry.d.ts +2 -2
- package/dist/app-builder/config-diff/registry.js +2 -2
- package/dist/app-builder/config-diff/strategies/pattern.js +14 -1
- package/dist/app-builder/config-diff/types.d.ts +15 -1
- package/dist/app-builder/paths.js +18 -1
- package/dist/app-builder/watchers.js +35 -3
- package/dist/app-builder/websocket.js +10 -4
- package/dist/cli.js +1 -1
- package/dist/config/normalizers/pattern.d.ts +0 -1
- package/dist/config/normalizers/pattern.js +0 -3
- package/dist/config/runtime.js +10 -4
- package/dist/pipeline/patterns.d.ts +3 -1
- package/dist/pipeline/patterns.js +2 -2
- package/dist/transformers/plugins/csf-render.js +2 -1
- package/dist/transformers/plugins/ssr-render.js +2 -1
- package/dist/transformers/preview/component/api.d.ts +2 -13
- package/dist/transformers/preview/component/api.js +7 -46
- package/dist/transformers/preview/component/artifacts.d.ts +4 -0
- package/dist/transformers/preview/component/artifacts.js +126 -0
- package/dist/transformers/preview/component/builder.js +62 -6
- package/dist/transformers/preview/component/css.d.ts +2 -0
- package/dist/transformers/preview/component/css.js +25 -9
- package/dist/transformers/preview/component/javascript.d.ts +1 -1
- package/dist/transformers/preview/component/javascript.js +45 -19
- package/dist/transformers/preview/component/summary.d.ts +6 -0
- package/dist/transformers/preview/component/summary.js +55 -0
- package/dist/transformers/preview/component.js +3 -25
- package/dist/transformers/preview/pattern/api.d.ts +4 -0
- package/dist/transformers/preview/pattern/api.js +19 -1
- package/dist/transformers/preview/pattern/builder.d.ts +9 -1
- package/dist/transformers/preview/pattern/builder.js +123 -42
- package/dist/transformers/utils/vite-logger.js +4 -4
- package/dist/transformers/vite-config.js +2 -0
- package/dist/utils/duration.d.ts +2 -0
- package/dist/utils/duration.js +6 -0
- package/dist/utils/logger.d.ts +42 -5
- package/dist/utils/logger.js +135 -15
- package/package.json +1 -1
- package/src/app/pages/system/pattern/[pattern]/index.tsx +178 -55
- package/src/app-builder/build.ts +26 -10
- package/src/app-builder/config-diff/index.ts +1 -1
- package/src/app-builder/config-diff/registry.ts +3 -3
- package/src/app-builder/config-diff/strategies/pattern.ts +17 -2
- package/src/app-builder/config-diff/types.ts +16 -1
- package/src/app-builder/paths.ts +22 -1
- package/src/app-builder/watchers.ts +15 -3
- package/src/app-builder/websocket.ts +16 -5
- package/src/cli.ts +1 -1
- package/src/config/normalizers/pattern.ts +0 -7
- package/src/config/runtime.ts +11 -4
- package/src/pipeline/patterns.ts +5 -3
- package/src/transformers/plugins/csf-render.ts +2 -1
- package/src/transformers/plugins/ssr-render.ts +2 -1
- package/src/transformers/preview/component/api.ts +2 -50
- package/src/transformers/preview/component/artifacts.ts +131 -0
- package/src/transformers/preview/component/builder.ts +72 -6
- package/src/transformers/preview/component/css.ts +26 -10
- package/src/transformers/preview/component/javascript.ts +50 -23
- package/src/transformers/preview/component/summary.ts +54 -0
- package/src/transformers/preview/component.ts +2 -1
- package/src/transformers/preview/pattern/api.ts +17 -0
- package/src/transformers/preview/pattern/builder.ts +156 -58
- package/src/transformers/utils/vite-logger.ts +4 -4
- package/src/transformers/vite-config.ts +2 -0
- package/src/utils/duration.ts +3 -0
- package/src/utils/logger.ts +143 -16
|
@@ -8,6 +8,9 @@ import Layout from '../../../../components/Layout/Main';
|
|
|
8
8
|
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
9
9
|
import PrevNextNav from '../../../../components/Navigation/PrevNextNav';
|
|
10
10
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
11
|
+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '../../../../components/ui/accordion';
|
|
12
|
+
import { Badge } from '../../../../components/ui/badge';
|
|
13
|
+
import { JsonTreeView } from '../../../../components/ui/json-tree-view';
|
|
11
14
|
import {
|
|
12
15
|
DocumentationProps,
|
|
13
16
|
fetchPatterns,
|
|
@@ -25,6 +28,66 @@ interface PatternPageProps extends DocumentationProps {
|
|
|
25
28
|
nextPattern: PatternNavItem | null;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
const SYNTHETIC_PATTERN_PREVIEW_PREFIX = '__pattern_';
|
|
32
|
+
|
|
33
|
+
const isSyntheticPatternPreview = (previewKey?: string) => Boolean(previewKey?.startsWith(SYNTHETIC_PATTERN_PREVIEW_PREFIX));
|
|
34
|
+
|
|
35
|
+
const formatPreviewLabel = (component: PatternListObject['components'][number], index: number) => {
|
|
36
|
+
const previewKey = component.preview || component.resolvedPreview;
|
|
37
|
+
|
|
38
|
+
if (!previewKey) {
|
|
39
|
+
return {
|
|
40
|
+
title: 'Default preview',
|
|
41
|
+
subtitle: 'Uses the component default preview configuration.',
|
|
42
|
+
rawKey: null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isSyntheticPatternPreview(previewKey)) {
|
|
47
|
+
return {
|
|
48
|
+
title: component.args ? 'Custom configuration' : `Generated preview ${index + 1}`,
|
|
49
|
+
subtitle: component.args ? 'Built from inline args for this pattern.' : 'Generated specifically for this pattern composition.',
|
|
50
|
+
rawKey: previewKey,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
title: startCase(previewKey),
|
|
56
|
+
subtitle: 'Named preview from the component docs.',
|
|
57
|
+
rawKey: previewKey,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const formatArgValue = (value: unknown) => {
|
|
62
|
+
if (typeof value === 'string') {
|
|
63
|
+
return value.length > 24 ? `${value.slice(0, 21)}...` : value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return `${value.length} item${value.length === 1 ? '' : 's'}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (value && typeof value === 'object') {
|
|
75
|
+
return `${Object.keys(value as Record<string, unknown>).length} fields`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value === null) {
|
|
79
|
+
return 'null';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return 'set';
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const getArgSummary = (args?: Record<string, unknown>) => {
|
|
86
|
+
if (!args) return [];
|
|
87
|
+
|
|
88
|
+
return Object.entries(args).slice(0, 3);
|
|
89
|
+
};
|
|
90
|
+
|
|
28
91
|
export async function getStaticPaths() {
|
|
29
92
|
return {
|
|
30
93
|
paths: fetchPatterns()?.map((p) => ({ params: { pattern: p.id } })) ?? [],
|
|
@@ -74,12 +137,8 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
74
137
|
const normalizedBasePath = appBasePath ? `/${appBasePath.replace(/^\/+|\/+$/g, '')}` : '';
|
|
75
138
|
const patternRoute = (patternId: string) => `${normalizedBasePath}/system/pattern/${patternId}`;
|
|
76
139
|
|
|
77
|
-
const previousLink = previousPattern
|
|
78
|
-
|
|
79
|
-
: null;
|
|
80
|
-
const nextLink = nextPattern
|
|
81
|
-
? { href: patternRoute(nextPattern.id), title: nextPattern.title }
|
|
82
|
-
: null;
|
|
140
|
+
const previousLink = previousPattern ? { href: patternRoute(previousPattern.id), title: previousPattern.title } : null;
|
|
141
|
+
const nextLink = nextPattern ? { href: patternRoute(nextPattern.id), title: nextPattern.title } : null;
|
|
83
142
|
const invalidComponents = pattern?.components?.filter((comp) => comp.resolved === false) ?? [];
|
|
84
143
|
|
|
85
144
|
useEffect(() => {
|
|
@@ -115,7 +174,15 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
115
174
|
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
116
175
|
<div className="flex flex-col gap-3 pb-14">
|
|
117
176
|
<small className="text-sm font-medium text-sky-600 dark:text-gray-300">Patterns</small>
|
|
118
|
-
<
|
|
177
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
178
|
+
<HeadersType.H1>{metadata.title}</HeadersType.H1>
|
|
179
|
+
<Badge
|
|
180
|
+
variant="outline"
|
|
181
|
+
className="rounded-full border border-gray-200 px-3 py-1 text-[11px] font-medium text-gray-600 dark:border-gray-700 dark:text-gray-300"
|
|
182
|
+
>
|
|
183
|
+
{pattern.components?.length ?? 0} component{(pattern.components?.length ?? 0) === 1 ? '' : 's'}
|
|
184
|
+
</Badge>
|
|
185
|
+
</div>
|
|
119
186
|
{metadata.description && (
|
|
120
187
|
<div className="prose max-w-[800px] text-xl font-light leading-relaxed text-gray-600 dark:text-gray-300">
|
|
121
188
|
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
@@ -126,20 +193,10 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
126
193
|
</div>
|
|
127
194
|
|
|
128
195
|
<div className="max-w-[1100px]">
|
|
129
|
-
{/* Pattern composition info */}
|
|
130
|
-
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500">
|
|
131
|
-
<span>{pattern.components?.length ?? 0} component(s)</span>
|
|
132
|
-
{pattern.group && (
|
|
133
|
-
<>
|
|
134
|
-
<span className="text-gray-300">|</span>
|
|
135
|
-
<span>{pattern.group}</span>
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
|
|
140
196
|
{invalidComponents.length > 0 && (
|
|
141
197
|
<div className="mb-4 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-200">
|
|
142
|
-
{invalidComponents.length} pattern component{invalidComponents.length > 1 ? 's could' : ' could'} not be resolved and
|
|
198
|
+
{invalidComponents.length} pattern component{invalidComponents.length > 1 ? 's could' : ' could'} not be resolved and{' '}
|
|
199
|
+
{invalidComponents.length > 1 ? 'were' : 'was'} skipped.
|
|
143
200
|
</div>
|
|
144
201
|
)}
|
|
145
202
|
|
|
@@ -163,9 +220,7 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
163
220
|
src={`${normalizedBasePath}/api/pattern/${pattern.url}`}
|
|
164
221
|
/>
|
|
165
222
|
) : (
|
|
166
|
-
<div className="flex items-center justify-center p-8 text-sm text-gray-500">
|
|
167
|
-
No preview available for this pattern.
|
|
168
|
-
</div>
|
|
223
|
+
<div className="flex items-center justify-center p-8 text-sm text-gray-500">No preview available for this pattern.</div>
|
|
169
224
|
)}
|
|
170
225
|
</div>
|
|
171
226
|
</div>
|
|
@@ -173,42 +228,110 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
173
228
|
{/* Component list */}
|
|
174
229
|
{pattern.components && pattern.components.length > 0 && (
|
|
175
230
|
<div className="mt-8">
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
231
|
+
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
|
|
232
|
+
<div>
|
|
233
|
+
<HeadersType.H3>Components in this pattern</HeadersType.H3>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div className="mt-5 overflow-hidden rounded-xl border border-gray-200 dark:border-gray-800">
|
|
238
|
+
{pattern.components.map((comp, i) => {
|
|
239
|
+
const previewMeta = formatPreviewLabel(comp, i);
|
|
240
|
+
const argSummary = getArgSummary(comp.args);
|
|
241
|
+
const hasArgs = Boolean(comp.args);
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<div
|
|
245
|
+
key={`${comp.id}-${i}`}
|
|
246
|
+
className="border-b border-gray-200 bg-white last:border-b-0 dark:border-gray-800 dark:bg-transparent"
|
|
247
|
+
>
|
|
248
|
+
<div className="grid gap-4 px-5 py-4 md:grid-cols-[minmax(0,220px)_minmax(0,1fr)] md:px-6">
|
|
249
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
250
|
+
<span className="text-xs font-medium text-gray-400 dark:text-gray-500">{String(i + 1).padStart(2, '0')}</span>
|
|
251
|
+
<code className="font-mono text-[13px] text-gray-900 dark:text-gray-100">{comp.id}</code>
|
|
252
|
+
{comp.resolved === false && (
|
|
253
|
+
<Badge variant="warning" className="rounded-full px-2.5 py-0.5 text-[11px] font-medium">
|
|
197
254
|
Unresolved
|
|
198
|
-
</
|
|
255
|
+
</Badge>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div className="min-w-0">
|
|
260
|
+
{comp.resolved === false ? (
|
|
261
|
+
<div className="rounded-md bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:bg-amber-950/40 dark:text-amber-200">
|
|
262
|
+
Could not resolve component preview.
|
|
263
|
+
</div>
|
|
199
264
|
) : (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
265
|
+
<>
|
|
266
|
+
<div className="space-y-2">
|
|
267
|
+
<div>
|
|
268
|
+
<p className="text-[10px] font-medium uppercase tracking-[0.1em] text-gray-400 dark:text-gray-500">
|
|
269
|
+
Preview
|
|
270
|
+
</p>
|
|
271
|
+
<p className="mt-1 text-sm font-medium text-gray-900 dark:text-gray-100">{previewMeta.title}</p>
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{previewMeta.rawKey && !hasArgs && (
|
|
275
|
+
<div className="inline-flex max-w-full items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 dark:bg-gray-900">
|
|
276
|
+
<span className="text-[10px] font-medium uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">
|
|
277
|
+
Key
|
|
278
|
+
</span>
|
|
279
|
+
<code className="truncate font-mono text-xs text-gray-500 dark:text-gray-400">{previewMeta.rawKey}</code>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{hasArgs ? (
|
|
285
|
+
<div className="mt-2">
|
|
286
|
+
<Accordion type="single" collapsible className="mt-1">
|
|
287
|
+
<AccordionItem value={`args-${i}`} className="border-none">
|
|
288
|
+
<div className="flex flex-wrap gap-2">
|
|
289
|
+
{argSummary.length > 0 && (
|
|
290
|
+
<dl className="flex flex-wrap gap-2">
|
|
291
|
+
{argSummary.map(([key, value]) => (
|
|
292
|
+
<div
|
|
293
|
+
key={key}
|
|
294
|
+
className="inline-flex max-w-full items-center gap-2 rounded-md bg-gray-50 px-2.5 py-1.5 dark:bg-gray-900"
|
|
295
|
+
>
|
|
296
|
+
<dt className="text-[10px] font-medium uppercase tracking-[0.12em] text-gray-400 dark:text-gray-500">
|
|
297
|
+
{key}
|
|
298
|
+
</dt>
|
|
299
|
+
<dd className="truncate text-xs text-gray-900 dark:text-gray-100">{formatArgValue(value)}</dd>
|
|
300
|
+
</div>
|
|
301
|
+
))}
|
|
302
|
+
{Object.keys(comp.args ?? {}).length > argSummary.length && (
|
|
303
|
+
<div className="inline-flex max-w-full items-center rounded-md bg-gray-50 px-2.5 py-1.5 text-xs text-gray-500 dark:bg-gray-900 dark:text-gray-400">
|
|
304
|
+
+{Object.keys(comp.args ?? {}).length - argSummary.length} more
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
</dl>
|
|
308
|
+
)}
|
|
309
|
+
|
|
310
|
+
<AccordionTrigger className="inline-flex w-auto flex-none items-center gap-1 rounded-md bg-gray-50 px-2.5 py-1.5 text-[10px] font-medium uppercase tracking-[0.12em] text-gray-400 no-underline hover:bg-gray-100 hover:text-gray-600 hover:no-underline dark:bg-gray-900 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-300 [&>svg]:h-3.5 [&>svg]:w-3.5">
|
|
311
|
+
All args
|
|
312
|
+
</AccordionTrigger>
|
|
313
|
+
</div>
|
|
314
|
+
<AccordionContent className="pt-2">
|
|
315
|
+
<JsonTreeView
|
|
316
|
+
data={comp.args}
|
|
317
|
+
defaultExpanded
|
|
318
|
+
maxInitialDepth={1}
|
|
319
|
+
className="rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-900"
|
|
320
|
+
/>
|
|
321
|
+
</AccordionContent>
|
|
322
|
+
</AccordionItem>
|
|
323
|
+
</Accordion>
|
|
324
|
+
</div>
|
|
325
|
+
) : (
|
|
326
|
+
<></>
|
|
327
|
+
)}
|
|
328
|
+
</>
|
|
203
329
|
)}
|
|
204
|
-
</
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
))}
|
|
210
|
-
</tbody>
|
|
211
|
-
</table>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
212
335
|
</div>
|
|
213
336
|
</div>
|
|
214
337
|
)}
|
package/src/app-builder/build.ts
CHANGED
|
@@ -105,13 +105,16 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
105
105
|
// Build app
|
|
106
106
|
const buildResult = spawn.sync('npx', ['next', 'build'], {
|
|
107
107
|
cwd: appPath,
|
|
108
|
-
stdio: 'inherit',
|
|
108
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
109
109
|
env: {
|
|
110
110
|
...process.env,
|
|
111
111
|
NODE_ENV: 'production',
|
|
112
112
|
},
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
Logger.childProcessBuffer(buildResult.stdout);
|
|
116
|
+
Logger.childProcessBuffer(buildResult.stderr);
|
|
117
|
+
|
|
115
118
|
if (buildResult.status !== 0) {
|
|
116
119
|
let errorMsg = `Next.js build failed with exit code ${buildResult.status}`;
|
|
117
120
|
if (buildResult.error) {
|
|
@@ -166,24 +169,32 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
166
169
|
// create empty directory
|
|
167
170
|
await fs.ensureDir(moduleOutput);
|
|
168
171
|
}
|
|
172
|
+
Logger.info(`Starting Next.js dev server (Turbopack) at http://${hostname}:${port}…`);
|
|
173
|
+
|
|
169
174
|
const nextProcess = spawn('npx', ['next', 'dev', '--turbopack', '--port', String(port)], {
|
|
170
175
|
cwd: appPath,
|
|
171
|
-
stdio: 'inherit',
|
|
176
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
172
177
|
env: {
|
|
173
178
|
...process.env,
|
|
174
179
|
NODE_ENV: 'development',
|
|
175
180
|
},
|
|
176
181
|
});
|
|
177
|
-
Logger.
|
|
182
|
+
Logger.pipeChildStreams(nextProcess.stdout, nextProcess.stderr);
|
|
178
183
|
|
|
179
184
|
nextProcess.on('error', (error) => {
|
|
180
|
-
Logger.error(`Next.js dev process
|
|
185
|
+
Logger.error(`Next.js dev process failed to start: ${error}`);
|
|
181
186
|
process.exit(1);
|
|
182
187
|
});
|
|
183
188
|
|
|
184
|
-
nextProcess.on('close', (code) => {
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
nextProcess.on('close', (code, signal) => {
|
|
190
|
+
if (code === 0) {
|
|
191
|
+
Logger.success(`Next.js dev process exited normally`);
|
|
192
|
+
} else if (signal) {
|
|
193
|
+
Logger.warn(`Next.js dev process stopped (${signal})`);
|
|
194
|
+
} else {
|
|
195
|
+
Logger.error(`Next.js dev process exited with code ${code}`);
|
|
196
|
+
}
|
|
197
|
+
process.exit(code ?? 1);
|
|
187
198
|
});
|
|
188
199
|
|
|
189
200
|
const wss = await createWebSocketServer(handoff.config.app.ports?.websocket ?? 3001);
|
|
@@ -227,16 +238,21 @@ export const devApp = async (handoff: Handoff): Promise<void> => {
|
|
|
227
238
|
// Persist client configuration
|
|
228
239
|
await persistClientConfig(handoff);
|
|
229
240
|
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
const devPort = handoff.config.app.ports?.app ?? 3000;
|
|
242
|
+
Logger.info(`Starting Next.js dev server (Turbopack) on port ${devPort}…`);
|
|
243
|
+
|
|
244
|
+
const devResult = spawn.sync('npx', ['next', 'dev', '--turbopack', '--port', String(devPort)], {
|
|
232
245
|
cwd: appPath,
|
|
233
|
-
stdio: 'inherit',
|
|
246
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
234
247
|
env: {
|
|
235
248
|
...process.env,
|
|
236
249
|
NODE_ENV: 'development',
|
|
237
250
|
},
|
|
238
251
|
});
|
|
239
252
|
|
|
253
|
+
Logger.childProcessBuffer(devResult.stdout);
|
|
254
|
+
Logger.childProcessBuffer(devResult.stderr);
|
|
255
|
+
|
|
240
256
|
if (devResult.status !== 0) {
|
|
241
257
|
let errorMsg = `Next.js dev failed with exit code ${devResult.status}`;
|
|
242
258
|
if (devResult.error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ConfigDiffStrategy, RebuildHandle } from './types';
|
|
1
|
+
export type { ConfigDiffStrategy, FinalizeContext, RebuildHandle } from './types';
|
|
2
2
|
export { type MapSnapshot, diffMapSnapshots, stableStringify } from './snapshot';
|
|
3
3
|
export { patternDiffStrategy } from './strategies/pattern';
|
|
4
4
|
export { getAllStrategies, getStrategy, runAllFinalizers } from './registry';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Handoff from '../..';
|
|
2
2
|
import { patternDiffStrategy } from './strategies/pattern';
|
|
3
|
-
import { ConfigDiffStrategy } from './types';
|
|
3
|
+
import { ConfigDiffStrategy, FinalizeContext } from './types';
|
|
4
4
|
|
|
5
5
|
const strategies = new Map<string, ConfigDiffStrategy>([
|
|
6
6
|
[patternDiffStrategy.kind, patternDiffStrategy],
|
|
@@ -16,8 +16,8 @@ export const getAllStrategies = (): ConfigDiffStrategy[] =>
|
|
|
16
16
|
* Runs every registered strategy finalizer.
|
|
17
17
|
* Called after component rebuilds so derived artifacts stay up-to-date.
|
|
18
18
|
*/
|
|
19
|
-
export const runAllFinalizers = async (handoff: Handoff): Promise<void> => {
|
|
19
|
+
export const runAllFinalizers = async (handoff: Handoff, context?: FinalizeContext): Promise<void> => {
|
|
20
20
|
for (const strategy of strategies.values()) {
|
|
21
|
-
await strategy.finalize(handoff);
|
|
21
|
+
await strategy.finalize(handoff, context);
|
|
22
22
|
}
|
|
23
23
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import Handoff from '../../..';
|
|
2
2
|
import { buildPatterns } from '../../../pipeline/patterns';
|
|
3
3
|
import processComponents, { ComponentSegment } from '../../../transformers/preview/component/builder';
|
|
4
|
+
import { getPatternIdsReferencingComponents } from '../../../transformers/preview/pattern/builder';
|
|
4
5
|
import { Logger } from '../../../utils/logger';
|
|
5
6
|
import { diffMapSnapshots, MapSnapshot, stableStringify } from '../snapshot';
|
|
6
|
-
import { ConfigDiffStrategy } from '../types';
|
|
7
|
+
import { ConfigDiffStrategy, FinalizeContext } from '../types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Snapshots every __pattern_{patternId}_* synthetic preview entry across all
|
|
@@ -59,7 +60,21 @@ export const patternDiffStrategy: ConfigDiffStrategy = {
|
|
|
59
60
|
};
|
|
60
61
|
},
|
|
61
62
|
|
|
62
|
-
async finalize(handoff) {
|
|
63
|
+
async finalize(handoff, context?: FinalizeContext) {
|
|
64
|
+
if (context?.skipPatternFinalizer) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const componentIds = context?.patternRebuildComponentIds;
|
|
69
|
+
if (componentIds?.length) {
|
|
70
|
+
const patternIds = getPatternIdsReferencingComponents(handoff, componentIds);
|
|
71
|
+
if (patternIds.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await buildPatterns(handoff, { onlyPatternIds: new Set(patternIds) });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
await buildPatterns(handoff);
|
|
64
79
|
},
|
|
65
80
|
};
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import Handoff from '../..';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Optional hints for post-build finalizers (e.g. selective pattern rebuild).
|
|
5
|
+
*/
|
|
6
|
+
export type FinalizeContext = {
|
|
7
|
+
/**
|
|
8
|
+
* Only patterns that reference at least one of these component ids are rebuilt.
|
|
9
|
+
* When omitted, all patterns are rebuilt.
|
|
10
|
+
*/
|
|
11
|
+
patternRebuildComponentIds?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* When true, skip pattern artifact generation (no component preview HTML changed).
|
|
14
|
+
*/
|
|
15
|
+
skipPatternFinalizer?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* Handle returned by ConfigDiffStrategy.capture().
|
|
5
20
|
*
|
|
@@ -17,5 +32,5 @@ export interface RebuildHandle {
|
|
|
17
32
|
export interface ConfigDiffStrategy {
|
|
18
33
|
readonly kind: string;
|
|
19
34
|
capture(handoff: Handoff, entityId: string): RebuildHandle;
|
|
20
|
-
finalize(handoff: Handoff): Promise<void>;
|
|
35
|
+
finalize(handoff: Handoff, context?: FinalizeContext): Promise<void>;
|
|
21
36
|
}
|
package/src/app-builder/paths.ts
CHANGED
|
@@ -30,6 +30,17 @@ export const getAppPath = (handoff: Handoff): string => {
|
|
|
30
30
|
return path.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const mirrorDirectory = async (sourcePath: string, destinationPath: string): Promise<void> => {
|
|
34
|
+
if (!(await fs.pathExists(sourcePath))) {
|
|
35
|
+
await fs.remove(destinationPath);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await fs.remove(destinationPath);
|
|
40
|
+
await fs.ensureDir(path.dirname(destinationPath));
|
|
41
|
+
await fs.copy(sourcePath, destinationPath, { overwrite: true });
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Copy the public dir from the working dir to the module dir.
|
|
35
46
|
*/
|
|
@@ -37,8 +48,18 @@ export const syncPublicFiles = async (handoff: Handoff): Promise<void> => {
|
|
|
37
48
|
const appPath = getAppPath(handoff);
|
|
38
49
|
const workingPublicPath = getWorkingPublicPath(handoff);
|
|
39
50
|
if (workingPublicPath) {
|
|
40
|
-
|
|
51
|
+
const destinationPublicPath = path.resolve(appPath, 'public');
|
|
52
|
+
const sourceApiPath = path.resolve(workingPublicPath, 'api');
|
|
53
|
+
const destinationApiPath = path.resolve(destinationPublicPath, 'api');
|
|
54
|
+
|
|
55
|
+
await fs.copy(workingPublicPath, destinationPublicPath, {
|
|
41
56
|
overwrite: true,
|
|
57
|
+
filter: (file) => {
|
|
58
|
+
const relativePath = path.relative(workingPublicPath, file);
|
|
59
|
+
return relativePath !== 'api' && !relativePath.startsWith(`api${path.sep}`);
|
|
60
|
+
},
|
|
42
61
|
});
|
|
62
|
+
|
|
63
|
+
await mirrorDirectory(sourceApiPath, destinationApiPath);
|
|
43
64
|
}
|
|
44
65
|
};
|
|
@@ -8,7 +8,7 @@ import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
|
8
8
|
import { Logger } from '../utils/logger';
|
|
9
9
|
import { isPathInside, normalizePathForCompare } from '../utils/path';
|
|
10
10
|
import { persistClientConfig } from './client-config';
|
|
11
|
-
import { getStrategy, runAllFinalizers } from './config-diff';
|
|
11
|
+
import { getStrategy, runAllFinalizers, type FinalizeContext } from './config-diff';
|
|
12
12
|
import { syncPublicFiles } from './paths';
|
|
13
13
|
import {
|
|
14
14
|
getRuntimeComponentsPathsToWatch,
|
|
@@ -185,8 +185,20 @@ export const watchRuntimeComponents = (
|
|
|
185
185
|
const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
|
|
186
186
|
const segmentToUpdate: ComponentSegment | undefined = entryType ? mapEntryTypeToSegment(entryType) : undefined;
|
|
187
187
|
const componentId = resolveComponentIdForChangedFile(handoff, file);
|
|
188
|
+
|
|
189
|
+
const skipPatterns =
|
|
190
|
+
segmentToUpdate === ComponentSegment.JavaScript ||
|
|
191
|
+
segmentToUpdate === ComponentSegment.Style;
|
|
192
|
+
|
|
193
|
+
let finalizeContext: FinalizeContext | undefined;
|
|
194
|
+
if (skipPatterns) {
|
|
195
|
+
finalizeContext = { skipPatternFinalizer: true };
|
|
196
|
+
} else if (componentId) {
|
|
197
|
+
finalizeContext = { patternRebuildComponentIds: [componentId] };
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
await processComponents(handoff, componentId, segmentToUpdate);
|
|
189
|
-
await runAllFinalizers(handoff);
|
|
201
|
+
await runAllFinalizers(handoff, finalizeContext);
|
|
190
202
|
} catch (e) {
|
|
191
203
|
Logger.error('Error processing component:', e);
|
|
192
204
|
}
|
|
@@ -314,7 +326,7 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
|
|
|
314
326
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
315
327
|
watchRuntimeConfiguration(handoff, state);
|
|
316
328
|
await processComponents(handoff, dirName);
|
|
317
|
-
await runAllFinalizers(handoff);
|
|
329
|
+
await runAllFinalizers(handoff, { patternRebuildComponentIds: [dirName] });
|
|
318
330
|
} catch (e) {
|
|
319
331
|
Logger.error('Error processing new component:', e);
|
|
320
332
|
}
|
|
@@ -13,8 +13,21 @@ export interface HandoffWebSocket extends WebSocket {
|
|
|
13
13
|
* @returns A function that accepts a message string and broadcasts it to all connected clients.
|
|
14
14
|
*/
|
|
15
15
|
export const createWebSocketServer = async (port: number = 3001) => {
|
|
16
|
+
Logger.info(`Starting WebSocket server for live reload on port ${port}…`);
|
|
17
|
+
|
|
16
18
|
const wss = new WebSocket.Server({ port });
|
|
17
19
|
|
|
20
|
+
wss.on('listening', () => {
|
|
21
|
+
Logger.info(`WebSocket server listening on ws://localhost:${port} (live reload)`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
wss.on('error', (err) => {
|
|
25
|
+
Logger.error(
|
|
26
|
+
`WebSocket server failed${err instanceof Error ? `: ${err.message}` : ''}`,
|
|
27
|
+
err instanceof Error ? err : undefined
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
18
31
|
// Heartbeat function to mark a connection as alive.
|
|
19
32
|
const heartbeat = function (this: HandoffWebSocket) {
|
|
20
33
|
this.isAlive = true;
|
|
@@ -25,7 +38,7 @@ export const createWebSocketServer = async (port: number = 3001) => {
|
|
|
25
38
|
const extWs = ws as HandoffWebSocket;
|
|
26
39
|
extWs.isAlive = true;
|
|
27
40
|
extWs.send(JSON.stringify({ type: 'WELCOME' }));
|
|
28
|
-
extWs.on('error', (error) => Logger.error('WebSocket error:', error));
|
|
41
|
+
extWs.on('error', (error) => Logger.error('WebSocket client error:', error));
|
|
29
42
|
extWs.on('pong', heartbeat);
|
|
30
43
|
});
|
|
31
44
|
|
|
@@ -34,7 +47,7 @@ export const createWebSocketServer = async (port: number = 3001) => {
|
|
|
34
47
|
wss.clients.forEach((client) => {
|
|
35
48
|
const extWs = client as HandoffWebSocket;
|
|
36
49
|
if (!extWs.isAlive) {
|
|
37
|
-
Logger.
|
|
50
|
+
Logger.debug('Terminating inactive WebSocket client');
|
|
38
51
|
return client.terminate();
|
|
39
52
|
}
|
|
40
53
|
extWs.isAlive = false;
|
|
@@ -47,11 +60,9 @@ export const createWebSocketServer = async (port: number = 3001) => {
|
|
|
47
60
|
clearInterval(pingInterval);
|
|
48
61
|
});
|
|
49
62
|
|
|
50
|
-
Logger.success(`WebSocket server listening on ws://localhost:${port}`);
|
|
51
|
-
|
|
52
63
|
// Return a function to broadcast a message to all connected clients
|
|
53
64
|
return (message: string) => {
|
|
54
|
-
Logger.
|
|
65
|
+
Logger.debug(`Broadcasting reload to ${wss.clients.size} WebSocket client(s)`);
|
|
55
66
|
wss.clients.forEach((client) => {
|
|
56
67
|
if (client.readyState === WebSocket.OPEN) {
|
|
57
68
|
client.send(message);
|
package/src/cli.ts
CHANGED
|
@@ -5,7 +5,6 @@ type RawPatternDeclaration = Record<string, any>;
|
|
|
5
5
|
type NormalizePatternOptions = {
|
|
6
6
|
declarationPath: string;
|
|
7
7
|
fallbackId: string;
|
|
8
|
-
warn: (message: string) => void;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
export const normalizePatternDeclaration = (
|
|
@@ -33,12 +32,6 @@ export const normalizePatternDeclaration = (
|
|
|
33
32
|
|
|
34
33
|
const preview = typeof ref.preview === 'string' ? ref.preview.trim() : undefined;
|
|
35
34
|
|
|
36
|
-
if (!preview && !ref.args) {
|
|
37
|
-
options.warn(
|
|
38
|
-
`Pattern "${options.fallbackId}" component ref "${ref.id}" at index ${index} has neither "preview" nor "args". The component's default preview will be used.`
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
return {
|
|
43
36
|
id: ref.id.trim(),
|
|
44
37
|
preview,
|
package/src/config/runtime.ts
CHANGED
|
@@ -282,7 +282,6 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
282
282
|
pattern = normalizePatternDeclaration(rawPattern, {
|
|
283
283
|
declarationPath: resolvedPatternConfigPath,
|
|
284
284
|
fallbackId: patternBaseName,
|
|
285
|
-
warn: (message) => Logger.warn(message),
|
|
286
285
|
});
|
|
287
286
|
} catch (err) {
|
|
288
287
|
Logger.error(`Failed to read or parse pattern config: ${resolvedPatternConfigPath}`, err);
|
|
@@ -387,11 +386,19 @@ const injectPatternPreviews = (result: RuntimeConfig): void => {
|
|
|
387
386
|
continue;
|
|
388
387
|
}
|
|
389
388
|
|
|
390
|
-
// Case 2: no preview AND no args -> use the component's first existing preview
|
|
389
|
+
// Case 2: no preview AND no args -> use the component's first existing preview (never assume "default")
|
|
391
390
|
if (!ref.preview && !ref.args) {
|
|
392
391
|
const previewKeys = Object.keys(component.previews || {});
|
|
393
|
-
|
|
394
|
-
|
|
392
|
+
const firstPreview = previewKeys[0];
|
|
393
|
+
if (firstPreview) {
|
|
394
|
+
ref.resolvedPreview = firstPreview;
|
|
395
|
+
ref.resolved = true;
|
|
396
|
+
} else {
|
|
397
|
+
Logger.warn(
|
|
398
|
+
`Pattern "${patternId}" component ref "${ref.id}" has neither "preview" nor "args", and the component has no previews. This fragment will be skipped.`
|
|
399
|
+
);
|
|
400
|
+
ref.resolved = false;
|
|
401
|
+
}
|
|
395
402
|
continue;
|
|
396
403
|
}
|
|
397
404
|
|