handoff-app 1.1.3 → 1.2.0-1
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/config/config.template.js +21 -14
- package/config/config.template.ts +54 -0
- package/config/docs/system.md +2 -0
- package/dist/app-builder/build.js +31 -12
- package/dist/app-builder/config-diff/index.d.ts +4 -0
- package/dist/app-builder/config-diff/index.js +12 -0
- package/dist/app-builder/config-diff/registry.d.ts +9 -0
- package/dist/app-builder/config-diff/registry.js +30 -0
- package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
- package/dist/app-builder/config-diff/snapshot.js +43 -0
- package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +106 -0
- package/dist/app-builder/config-diff/types.d.ts +33 -0
- package/dist/app-builder/config-diff/types.js +2 -0
- package/dist/app-builder/watchers/component.d.ts +15 -0
- package/dist/app-builder/watchers/component.js +69 -0
- package/dist/app-builder/watchers/utils.d.ts +14 -0
- package/dist/app-builder/watchers/utils.js +44 -0
- package/dist/app-builder/watchers.d.ts +10 -19
- package/dist/app-builder/watchers.js +65 -79
- package/dist/app-builder/websocket.js +10 -4
- package/dist/cache/build-cache.js +14 -5
- package/dist/cli/create.js +67 -23
- package/dist/cli/eject.js +48 -7
- package/dist/cli/make.js +29 -4
- package/dist/cli/scaffold.js +60 -19
- package/dist/cli.js +1 -1
- package/dist/config/helpers.d.ts +7 -0
- package/dist/config/helpers.js +31 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +5 -1
- package/dist/config/loader.d.ts +10 -1
- package/dist/config/loader.js +92 -28
- package/dist/config/normalizers/declaration.d.ts +11 -0
- package/dist/config/normalizers/declaration.js +119 -0
- package/dist/config/normalizers/pattern.d.ts +8 -0
- package/dist/config/normalizers/pattern.js +35 -0
- package/dist/config/runtime.d.ts +9 -3
- package/dist/config/runtime.js +260 -33
- package/dist/declarations/helpers.d.ts +6 -0
- package/dist/declarations/helpers.js +19 -0
- package/dist/declarations/index.d.ts +2 -0
- package/dist/declarations/index.js +9 -0
- package/dist/declarations/types.d.ts +60 -0
- package/dist/declarations/types.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +34 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline/patterns.d.ts +8 -0
- package/dist/pipeline/patterns.js +21 -0
- package/dist/transformers/docgen/index.js +12 -2
- package/dist/transformers/plugins/csf-render.js +170 -34
- package/dist/transformers/plugins/ssr-render.js +18 -2
- package/dist/transformers/preview/component/api.js +16 -7
- package/dist/transformers/preview/component/builder.js +61 -8
- package/dist/transformers/preview/component/css.d.ts +8 -0
- package/dist/transformers/preview/component/css.js +6 -4
- package/dist/transformers/preview/component/html.js +14 -5
- package/dist/transformers/preview/component/javascript.d.ts +0 -1
- package/dist/transformers/preview/component/javascript.js +25 -14
- package/dist/transformers/preview/component/previews.d.ts +9 -0
- package/dist/transformers/preview/component/previews.js +10 -0
- package/dist/transformers/preview/pattern/api.d.ts +11 -0
- package/dist/transformers/preview/pattern/api.js +79 -0
- package/dist/transformers/preview/pattern/builder.d.ts +17 -0
- package/dist/transformers/preview/pattern/builder.js +167 -0
- package/dist/transformers/preview/pattern/html.d.ts +19 -0
- package/dist/transformers/preview/pattern/html.js +94 -0
- package/dist/transformers/preview/types.d.ts +54 -0
- package/dist/transformers/utils/csf.d.ts +16 -0
- package/dist/transformers/utils/csf.js +30 -0
- package/dist/transformers/utils/index.d.ts +1 -0
- package/dist/transformers/utils/index.js +2 -0
- package/dist/transformers/utils/module.d.ts +5 -0
- package/dist/transformers/utils/module.js +26 -13
- package/dist/transformers/utils/usage.d.ts +23 -0
- package/dist/transformers/utils/usage.js +294 -0
- package/dist/transformers/utils/vite-logger.js +4 -4
- package/dist/transformers/vite-config.js +2 -0
- package/dist/types/config.d.ts +32 -7
- package/dist/types/preview.d.ts +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/docs/cli.md +3 -3
- package/package.json +3 -1
- package/src/app/components/Component/PageSliceResolver.tsx +6 -6
- package/src/app/components/Component/Preview.tsx +17 -12
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
- package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
- package/src/app/components/util/index.ts +71 -2
- package/src/app/pages/system/pattern/[pattern]/index.tsx +346 -0
- package/src/app/pages/system/pattern/index.tsx +14 -0
- package/src/app-builder/README.md +37 -9
- package/src/app-builder/build.ts +34 -12
- package/src/app-builder/config-diff/index.ts +4 -0
- package/src/app-builder/config-diff/registry.ts +23 -0
- package/src/app-builder/config-diff/snapshot.ts +51 -0
- package/src/app-builder/config-diff/strategies/pattern.ts +80 -0
- package/src/app-builder/config-diff/types.ts +36 -0
- package/src/app-builder/watchers/component.ts +72 -0
- package/src/app-builder/watchers/utils.ts +44 -0
- package/src/app-builder/watchers.ts +74 -80
- package/src/app-builder/websocket.ts +16 -5
- package/src/cache/build-cache.ts +14 -5
- package/src/cli/create.ts +86 -25
- package/src/cli/eject.ts +29 -7
- package/src/cli/make.ts +32 -5
- package/src/cli/scaffold.ts +68 -21
- package/src/cli.ts +1 -1
- package/src/config/README.md +10 -1
- package/src/config/helpers.ts +43 -0
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +110 -31
- package/src/config/normalizers/declaration.ts +176 -0
- package/src/config/normalizers/pattern.ts +56 -0
- package/src/config/runtime.ts +317 -35
- package/src/declarations/helpers.ts +36 -0
- package/src/declarations/index.ts +19 -0
- package/src/declarations/types.ts +70 -0
- package/src/index.ts +47 -6
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/patterns.ts +12 -0
- package/src/transformers/README.md +2 -2
- package/src/transformers/docgen/index.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +216 -49
- package/src/transformers/plugins/ssr-render.ts +23 -7
- package/src/transformers/preview/component/api.ts +27 -7
- package/src/transformers/preview/component/builder.ts +82 -9
- package/src/transformers/preview/component/css.ts +6 -5
- package/src/transformers/preview/component/html.ts +18 -5
- package/src/transformers/preview/component/javascript.ts +29 -19
- package/src/transformers/preview/component/previews.ts +12 -0
- package/src/transformers/preview/pattern/api.ts +83 -0
- package/src/transformers/preview/pattern/builder.ts +192 -0
- package/src/transformers/preview/pattern/html.ts +107 -0
- package/src/transformers/preview/types.ts +58 -1
- package/src/transformers/utils/csf.ts +48 -0
- package/src/transformers/utils/index.ts +3 -0
- package/src/transformers/utils/module.ts +33 -18
- package/src/transformers/utils/usage.ts +319 -0
- package/src/transformers/utils/vite-logger.ts +4 -4
- package/src/transformers/vite-config.ts +2 -0
- package/src/types/README.md +1 -1
- package/src/types/config.ts +33 -7
- package/src/types/preview.ts +2 -0
- package/src/utils/duration.ts +3 -0
- package/src/utils/logger.ts +143 -16
- package/templates/default/components/button/Button.jsx.tpl +64 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
- package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
- package/templates/default/handoff.config.js.tpl +29 -16
- package/templates/default/handoff.config.ts.tpl +196 -0
- package/templates/default/handoff.config.with-examples.js.tpl +28 -17
- package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
- package/templates/default/package.json.tpl +1 -3
- package/handoff.config.js +0 -90
- package/templates/default/components/button/button.js.tpl +0 -62
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { PatternListObject } from '@handoff/transformers/preview/types';
|
|
3
|
+
import { startCase } from 'lodash';
|
|
4
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
6
|
+
import rehypeRaw from 'rehype-raw';
|
|
7
|
+
import Layout from '../../../../components/Layout/Main';
|
|
8
|
+
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
9
|
+
import PrevNextNav from '../../../../components/Navigation/PrevNextNav';
|
|
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';
|
|
14
|
+
import {
|
|
15
|
+
DocumentationProps,
|
|
16
|
+
fetchPatterns,
|
|
17
|
+
getClientRuntimeConfig,
|
|
18
|
+
getCurrentSection,
|
|
19
|
+
IParams,
|
|
20
|
+
staticBuildMenu,
|
|
21
|
+
} from '../../../../components/util';
|
|
22
|
+
|
|
23
|
+
type PatternNavItem = { id: string; title: string; description: string; group: string };
|
|
24
|
+
|
|
25
|
+
interface PatternPageProps extends DocumentationProps {
|
|
26
|
+
id: string;
|
|
27
|
+
previousPattern: PatternNavItem | null;
|
|
28
|
+
nextPattern: PatternNavItem | null;
|
|
29
|
+
}
|
|
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
|
+
|
|
91
|
+
export async function getStaticPaths() {
|
|
92
|
+
return {
|
|
93
|
+
paths: fetchPatterns()?.map((p) => ({ params: { pattern: p.id } })) ?? [],
|
|
94
|
+
fallback: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const getStaticProps = async (context: { params: IParams }) => {
|
|
99
|
+
const { pattern: patternId } = context.params;
|
|
100
|
+
const patterns = fetchPatterns() ?? [];
|
|
101
|
+
const menu = staticBuildMenu();
|
|
102
|
+
const config = getClientRuntimeConfig();
|
|
103
|
+
const patternData = patterns.find((p) => p.id === patternId);
|
|
104
|
+
|
|
105
|
+
const sameGroupPatterns = patterns.filter((p) => p.group === patternData?.group);
|
|
106
|
+
const groupIndex = sameGroupPatterns.findIndex((p) => p.id === patternId);
|
|
107
|
+
const previousPattern = sameGroupPatterns[groupIndex - 1] ?? null;
|
|
108
|
+
const nextPattern = sameGroupPatterns[groupIndex + 1] ?? null;
|
|
109
|
+
|
|
110
|
+
const fallbackTitle = patternData?.title || startCase(patternId as string);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
props: {
|
|
114
|
+
id: patternId,
|
|
115
|
+
menu,
|
|
116
|
+
config,
|
|
117
|
+
current: getCurrentSection(menu, '/system') ?? [],
|
|
118
|
+
metadata: {
|
|
119
|
+
title: fallbackTitle,
|
|
120
|
+
description: patternData?.description || '',
|
|
121
|
+
metaTitle: `${fallbackTitle}${config?.app?.client ? ` | ${config.app.client} Design System` : ''}`,
|
|
122
|
+
metaDescription: patternData?.description || '',
|
|
123
|
+
},
|
|
124
|
+
previousPattern,
|
|
125
|
+
nextPattern,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nextPattern }: PatternPageProps) => {
|
|
131
|
+
const [pattern, setPattern] = useState<PatternListObject | undefined>(undefined);
|
|
132
|
+
const [fetchError, setFetchError] = useState<string | undefined>(undefined);
|
|
133
|
+
const [iframeHeight, setIframeHeight] = useState('400px');
|
|
134
|
+
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
|
135
|
+
|
|
136
|
+
const appBasePath = process.env.HANDOFF_APP_BASE_PATH ?? '';
|
|
137
|
+
const normalizedBasePath = appBasePath ? `/${appBasePath.replace(/^\/+|\/+$/g, '')}` : '';
|
|
138
|
+
const patternRoute = (patternId: string) => `${normalizedBasePath}/system/pattern/${patternId}`;
|
|
139
|
+
|
|
140
|
+
const previousLink = previousPattern ? { href: patternRoute(previousPattern.id), title: previousPattern.title } : null;
|
|
141
|
+
const nextLink = nextPattern ? { href: patternRoute(nextPattern.id), title: nextPattern.title } : null;
|
|
142
|
+
const invalidComponents = pattern?.components?.filter((comp) => comp.resolved === false) ?? [];
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
setPattern(undefined);
|
|
147
|
+
setFetchError(undefined);
|
|
148
|
+
|
|
149
|
+
fetch(`${normalizedBasePath}/api/pattern/${id}.json`, { signal: controller.signal })
|
|
150
|
+
.then((res) => {
|
|
151
|
+
if (!res.ok) throw new Error(`Failed to load pattern: ${res.status} ${res.statusText}`);
|
|
152
|
+
return res.json();
|
|
153
|
+
})
|
|
154
|
+
.then((data) => setPattern(data as PatternListObject))
|
|
155
|
+
.catch((err) => {
|
|
156
|
+
if (err.name !== 'AbortError') {
|
|
157
|
+
setFetchError(err instanceof Error ? err.message : 'Failed to load pattern data.');
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return () => controller.abort();
|
|
162
|
+
}, [id, normalizedBasePath]);
|
|
163
|
+
|
|
164
|
+
const onIframeLoad = useCallback(() => {
|
|
165
|
+
if (iframeRef.current?.contentWindow?.document?.body) {
|
|
166
|
+
setIframeHeight(iframeRef.current.contentWindow.document.body.scrollHeight + 'px');
|
|
167
|
+
}
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
if (fetchError) return <p className="p-4 text-red-500">{fetchError}</p>;
|
|
171
|
+
if (!pattern) return <p>Loading...</p>;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
175
|
+
<div className="flex flex-col gap-3 pb-14">
|
|
176
|
+
<small className="text-sm font-medium text-sky-600 dark:text-gray-300">Patterns</small>
|
|
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>
|
|
186
|
+
{metadata.description && (
|
|
187
|
+
<div className="prose max-w-[800px] text-xl font-light leading-relaxed text-gray-600 dark:text-gray-300">
|
|
188
|
+
<ReactMarkdown components={MarkdownComponents} rehypePlugins={[rehypeRaw]}>
|
|
189
|
+
{metadata.description}
|
|
190
|
+
</ReactMarkdown>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="max-w-[1100px]">
|
|
196
|
+
{invalidComponents.length > 0 && (
|
|
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">
|
|
198
|
+
{invalidComponents.length} pattern component{invalidComponents.length > 1 ? 's could' : ' could'} not be resolved and{' '}
|
|
199
|
+
{invalidComponents.length > 1 ? 'were' : 'was'} skipped.
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{/* Full-page pattern preview */}
|
|
204
|
+
<div className="rounded-lg border border-gray-200 dark:border-gray-900">
|
|
205
|
+
<div className="flex w-full items-center justify-between rounded-t-lg bg-gray-50 px-6 py-2 dark:bg-gray-800">
|
|
206
|
+
<p className="font-monospace text-[11px] text-accent-foreground">Pattern Preview</p>
|
|
207
|
+
</div>
|
|
208
|
+
<div className="dotted-bg w-full p-4">
|
|
209
|
+
{pattern.url ? (
|
|
210
|
+
<iframe
|
|
211
|
+
ref={iframeRef}
|
|
212
|
+
onLoad={onIframeLoad}
|
|
213
|
+
height={iframeHeight}
|
|
214
|
+
style={{
|
|
215
|
+
width: '100%',
|
|
216
|
+
height: iframeHeight,
|
|
217
|
+
display: 'block',
|
|
218
|
+
border: 'none',
|
|
219
|
+
}}
|
|
220
|
+
src={`${normalizedBasePath}/api/pattern/${pattern.url}`}
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="flex items-center justify-center p-8 text-sm text-gray-500">No preview available for this pattern.</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{/* Component list */}
|
|
229
|
+
{pattern.components && pattern.components.length > 0 && (
|
|
230
|
+
<div className="mt-8">
|
|
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">
|
|
254
|
+
Unresolved
|
|
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>
|
|
264
|
+
) : (
|
|
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
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
<hr className="mt-8" />
|
|
340
|
+
<PrevNextNav previous={previousLink} next={nextLink} />
|
|
341
|
+
</div>
|
|
342
|
+
</Layout>
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
export default PatternPage;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useRouter } from 'next/router';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
const PatternIndexRedirect = () => {
|
|
5
|
+
const router = useRouter();
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
router.replace('/system');
|
|
9
|
+
}, [router]);
|
|
10
|
+
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default PatternIndexRedirect;
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
# App Builder Module
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Builds and runs the Next.js documentation app from runtime config + generated preview output.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## End-to-End Flow
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. **Initialize app shell** (`build.ts`)
|
|
8
|
+
- Prepare `.handoff/<project>/app`, copy app sources, sync `public`, persist client config.
|
|
9
|
+
|
|
10
|
+
2. **Build component artifacts**
|
|
11
|
+
- Production: `buildComponents` + `buildPatterns`.
|
|
12
|
+
- Watch mode: `processComponents(..., { useCache: true })` to skip unchanged components.
|
|
13
|
+
|
|
14
|
+
3. **Build global bundles**
|
|
15
|
+
- Build main JS/CSS entry bundles when configured.
|
|
16
|
+
|
|
17
|
+
4. **Run/watch**
|
|
18
|
+
- Production: run `next build` and publish static output.
|
|
19
|
+
- Watch: run `next dev` + WebSocket + chokidar watchers.
|
|
20
|
+
|
|
21
|
+
5. **Incremental rebuild behavior** (`watchers.ts`)
|
|
22
|
+
- Source entry change -> targeted component segment rebuild.
|
|
23
|
+
- Main config/global entry change -> full component rebuild.
|
|
24
|
+
- Pattern config change -> diff-aware selective preview rebuild only for affected components.
|
|
25
|
+
- After component rebuilds -> run config-diff finalizers (e.g. pattern composition).
|
|
26
|
+
|
|
27
|
+
6. **Event coalescing**
|
|
28
|
+
- Watchers share a single scheduler lock; rapid updates from the same source coalesce.
|
|
29
|
+
|
|
30
|
+
## Key Files
|
|
31
|
+
|
|
32
|
+
| Path | Purpose |
|
|
8
33
|
|------|---------|
|
|
9
|
-
| `build.ts` | `buildApp()
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
34
|
+
| `build.ts` | `buildApp()`, `watchApp()`, `devApp()` orchestration |
|
|
35
|
+
| `watchers.ts` | Watcher orchestration and rebuild routing |
|
|
36
|
+
| `watchers/component.ts` | Runtime component path mapping + segment resolution helpers |
|
|
37
|
+
| `watchers/utils.ts` | Coalescing scheduler + watcher state |
|
|
38
|
+
| `config-diff/index.ts` | Diff strategy exports + registry/finalizers |
|
|
39
|
+
| `config-diff/strategies/pattern.ts` | Pattern snapshot/diff/apply strategy |
|
|
40
|
+
| `client-config.ts` | Persist client runtime config + token API payload |
|
|
41
|
+
| `paths.ts` | App/public path helpers + public sync |
|
|
42
|
+
| `websocket.ts` | Dev reload WebSocket server |
|
package/src/app-builder/build.ts
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import Handoff from '..';
|
|
5
5
|
import { buildComponents } from '../pipeline/components';
|
|
6
|
+
import { buildPatterns } from '../pipeline/patterns';
|
|
6
7
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
7
8
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
8
9
|
import processComponents from '../transformers/preview/component/builder';
|
|
@@ -90,9 +91,10 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
90
91
|
// Perform cleanup
|
|
91
92
|
await cleanupAppDirectory(handoff);
|
|
92
93
|
|
|
93
|
-
// Build components
|
|
94
|
+
// Build components, then patterns (patterns depend on component output)
|
|
94
95
|
if (!skipComponents) {
|
|
95
96
|
await buildComponents(handoff);
|
|
97
|
+
await buildPatterns(handoff);
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
// Prepare app
|
|
@@ -103,13 +105,16 @@ const buildApp = async (handoff: Handoff, skipComponents?: boolean): Promise<voi
|
|
|
103
105
|
// Build app
|
|
104
106
|
const buildResult = spawn.sync('npx', ['next', 'build'], {
|
|
105
107
|
cwd: appPath,
|
|
106
|
-
stdio: 'inherit',
|
|
108
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
107
109
|
env: {
|
|
108
110
|
...process.env,
|
|
109
111
|
NODE_ENV: 'production',
|
|
110
112
|
},
|
|
111
113
|
});
|
|
112
114
|
|
|
115
|
+
Logger.childProcessBuffer(buildResult.stdout);
|
|
116
|
+
Logger.childProcessBuffer(buildResult.stderr);
|
|
117
|
+
|
|
113
118
|
if (buildResult.status !== 0) {
|
|
114
119
|
let errorMsg = `Next.js build failed with exit code ${buildResult.status}`;
|
|
115
120
|
if (buildResult.error) {
|
|
@@ -143,6 +148,9 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
143
148
|
await buildMainJS(handoff);
|
|
144
149
|
await buildMainCss(handoff);
|
|
145
150
|
|
|
151
|
+
// Build patterns after components are ready
|
|
152
|
+
await buildPatterns(handoff);
|
|
153
|
+
|
|
146
154
|
const appPath = await initializeProjectApp(handoff);
|
|
147
155
|
|
|
148
156
|
// Persist client configuration
|
|
@@ -161,24 +169,32 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
161
169
|
// create empty directory
|
|
162
170
|
await fs.ensureDir(moduleOutput);
|
|
163
171
|
}
|
|
172
|
+
Logger.info(`Starting Next.js dev server (Turbopack) at http://${hostname}:${port}…`);
|
|
173
|
+
|
|
164
174
|
const nextProcess = spawn('npx', ['next', 'dev', '--turbopack', '--port', String(port)], {
|
|
165
175
|
cwd: appPath,
|
|
166
|
-
stdio: 'inherit',
|
|
176
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
167
177
|
env: {
|
|
168
178
|
...process.env,
|
|
169
179
|
NODE_ENV: 'development',
|
|
170
180
|
},
|
|
171
181
|
});
|
|
172
|
-
Logger.
|
|
182
|
+
Logger.pipeChildStreams(nextProcess.stdout, nextProcess.stderr);
|
|
173
183
|
|
|
174
184
|
nextProcess.on('error', (error) => {
|
|
175
|
-
Logger.error(`Next.js dev process
|
|
185
|
+
Logger.error(`Next.js dev process failed to start: ${error}`);
|
|
176
186
|
process.exit(1);
|
|
177
187
|
});
|
|
178
188
|
|
|
179
|
-
nextProcess.on('close', (code) => {
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
182
198
|
});
|
|
183
199
|
|
|
184
200
|
const wss = await createWebSocketServer(handoff.config.app.ports?.websocket ?? 3001);
|
|
@@ -190,7 +206,8 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
190
206
|
};
|
|
191
207
|
|
|
192
208
|
const state: WatcherState = {
|
|
193
|
-
|
|
209
|
+
busy: false,
|
|
210
|
+
pendingHandlers: new Map(),
|
|
194
211
|
runtimeComponentsWatcher: null,
|
|
195
212
|
runtimeConfigurationWatcher: null,
|
|
196
213
|
componentDirectoriesWatcher: null,
|
|
@@ -221,16 +238,21 @@ export const devApp = async (handoff: Handoff): Promise<void> => {
|
|
|
221
238
|
// Persist client configuration
|
|
222
239
|
await persistClientConfig(handoff);
|
|
223
240
|
|
|
224
|
-
|
|
225
|
-
|
|
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)], {
|
|
226
245
|
cwd: appPath,
|
|
227
|
-
stdio: 'inherit',
|
|
246
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
228
247
|
env: {
|
|
229
248
|
...process.env,
|
|
230
249
|
NODE_ENV: 'development',
|
|
231
250
|
},
|
|
232
251
|
});
|
|
233
252
|
|
|
253
|
+
Logger.childProcessBuffer(devResult.stdout);
|
|
254
|
+
Logger.childProcessBuffer(devResult.stderr);
|
|
255
|
+
|
|
234
256
|
if (devResult.status !== 0) {
|
|
235
257
|
let errorMsg = `Next.js dev failed with exit code ${devResult.status}`;
|
|
236
258
|
if (devResult.error) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { ConfigDiffStrategy, FinalizeContext, RebuildHandle } from './types';
|
|
2
|
+
export { type MapSnapshot, diffMapSnapshots, stableStringify } from './snapshot';
|
|
3
|
+
export { patternDiffStrategy } from './strategies/pattern';
|
|
4
|
+
export { getAllStrategies, getStrategy, runAllFinalizers } from './registry';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Handoff from '../..';
|
|
2
|
+
import { patternDiffStrategy } from './strategies/pattern';
|
|
3
|
+
import { ConfigDiffStrategy, FinalizeContext } from './types';
|
|
4
|
+
|
|
5
|
+
const strategies = new Map<string, ConfigDiffStrategy>([
|
|
6
|
+
[patternDiffStrategy.kind, patternDiffStrategy],
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const getStrategy = (kind: string): ConfigDiffStrategy | undefined =>
|
|
10
|
+
strategies.get(kind);
|
|
11
|
+
|
|
12
|
+
export const getAllStrategies = (): ConfigDiffStrategy[] =>
|
|
13
|
+
Array.from(strategies.values());
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runs every registered strategy finalizer.
|
|
17
|
+
* Called after component rebuilds so derived artifacts stay up-to-date.
|
|
18
|
+
*/
|
|
19
|
+
export const runAllFinalizers = async (handoff: Handoff, context?: FinalizeContext): Promise<void> => {
|
|
20
|
+
for (const strategy of strategies.values()) {
|
|
21
|
+
await strategy.finalize(handoff, context);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** outerKey -> Map<innerKey, serializedValue> */
|
|
2
|
+
export type MapSnapshot = Map<string, Map<string, string>>;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic JSON serializer that sorts object keys at every nesting level.
|
|
6
|
+
* Prevents false-positive diffs when identical values are constructed with
|
|
7
|
+
* different key insertion order across reloads.
|
|
8
|
+
*/
|
|
9
|
+
export const stableStringify = (value: unknown): string =>
|
|
10
|
+
JSON.stringify(value, (_, v) =>
|
|
11
|
+
v && typeof v === 'object' && !Array.isArray(v)
|
|
12
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => a.localeCompare(b)))
|
|
13
|
+
: v
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compares two MapSnapshots and returns the outer keys where the inner maps
|
|
18
|
+
* differ (added, removed, or values changed).
|
|
19
|
+
*/
|
|
20
|
+
export const diffMapSnapshots = (
|
|
21
|
+
oldSnapshot: MapSnapshot,
|
|
22
|
+
newSnapshot: MapSnapshot
|
|
23
|
+
): string[] => {
|
|
24
|
+
const affected = new Set<string>();
|
|
25
|
+
|
|
26
|
+
for (const [key, oldEntries] of oldSnapshot) {
|
|
27
|
+
const newEntries = newSnapshot.get(key);
|
|
28
|
+
if (!newEntries) {
|
|
29
|
+
affected.add(key);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (oldEntries.size !== newEntries.size) {
|
|
33
|
+
affected.add(key);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
for (const [subKey, oldValue] of oldEntries) {
|
|
37
|
+
if (newEntries.get(subKey) !== oldValue) {
|
|
38
|
+
affected.add(key);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const key of newSnapshot.keys()) {
|
|
45
|
+
if (!oldSnapshot.has(key)) {
|
|
46
|
+
affected.add(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return Array.from(affected);
|
|
51
|
+
};
|