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.
Files changed (68) hide show
  1. package/dist/app-builder/build.js +24 -10
  2. package/dist/app-builder/config-diff/index.d.ts +1 -1
  3. package/dist/app-builder/config-diff/registry.d.ts +2 -2
  4. package/dist/app-builder/config-diff/registry.js +2 -2
  5. package/dist/app-builder/config-diff/strategies/pattern.js +14 -1
  6. package/dist/app-builder/config-diff/types.d.ts +15 -1
  7. package/dist/app-builder/paths.js +18 -1
  8. package/dist/app-builder/watchers.js +35 -3
  9. package/dist/app-builder/websocket.js +10 -4
  10. package/dist/cli.js +1 -1
  11. package/dist/config/normalizers/pattern.d.ts +0 -1
  12. package/dist/config/normalizers/pattern.js +0 -3
  13. package/dist/config/runtime.js +10 -4
  14. package/dist/pipeline/patterns.d.ts +3 -1
  15. package/dist/pipeline/patterns.js +2 -2
  16. package/dist/transformers/plugins/csf-render.js +2 -1
  17. package/dist/transformers/plugins/ssr-render.js +2 -1
  18. package/dist/transformers/preview/component/api.d.ts +2 -13
  19. package/dist/transformers/preview/component/api.js +7 -46
  20. package/dist/transformers/preview/component/artifacts.d.ts +4 -0
  21. package/dist/transformers/preview/component/artifacts.js +126 -0
  22. package/dist/transformers/preview/component/builder.js +62 -6
  23. package/dist/transformers/preview/component/css.d.ts +2 -0
  24. package/dist/transformers/preview/component/css.js +25 -9
  25. package/dist/transformers/preview/component/javascript.d.ts +1 -1
  26. package/dist/transformers/preview/component/javascript.js +45 -19
  27. package/dist/transformers/preview/component/summary.d.ts +6 -0
  28. package/dist/transformers/preview/component/summary.js +55 -0
  29. package/dist/transformers/preview/component.js +3 -25
  30. package/dist/transformers/preview/pattern/api.d.ts +4 -0
  31. package/dist/transformers/preview/pattern/api.js +19 -1
  32. package/dist/transformers/preview/pattern/builder.d.ts +9 -1
  33. package/dist/transformers/preview/pattern/builder.js +123 -42
  34. package/dist/transformers/utils/vite-logger.js +4 -4
  35. package/dist/transformers/vite-config.js +2 -0
  36. package/dist/utils/duration.d.ts +2 -0
  37. package/dist/utils/duration.js +6 -0
  38. package/dist/utils/logger.d.ts +42 -5
  39. package/dist/utils/logger.js +135 -15
  40. package/package.json +1 -1
  41. package/src/app/pages/system/pattern/[pattern]/index.tsx +178 -55
  42. package/src/app-builder/build.ts +26 -10
  43. package/src/app-builder/config-diff/index.ts +1 -1
  44. package/src/app-builder/config-diff/registry.ts +3 -3
  45. package/src/app-builder/config-diff/strategies/pattern.ts +17 -2
  46. package/src/app-builder/config-diff/types.ts +16 -1
  47. package/src/app-builder/paths.ts +22 -1
  48. package/src/app-builder/watchers.ts +15 -3
  49. package/src/app-builder/websocket.ts +16 -5
  50. package/src/cli.ts +1 -1
  51. package/src/config/normalizers/pattern.ts +0 -7
  52. package/src/config/runtime.ts +11 -4
  53. package/src/pipeline/patterns.ts +5 -3
  54. package/src/transformers/plugins/csf-render.ts +2 -1
  55. package/src/transformers/plugins/ssr-render.ts +2 -1
  56. package/src/transformers/preview/component/api.ts +2 -50
  57. package/src/transformers/preview/component/artifacts.ts +131 -0
  58. package/src/transformers/preview/component/builder.ts +72 -6
  59. package/src/transformers/preview/component/css.ts +26 -10
  60. package/src/transformers/preview/component/javascript.ts +50 -23
  61. package/src/transformers/preview/component/summary.ts +54 -0
  62. package/src/transformers/preview/component.ts +2 -1
  63. package/src/transformers/preview/pattern/api.ts +17 -0
  64. package/src/transformers/preview/pattern/builder.ts +156 -58
  65. package/src/transformers/utils/vite-logger.ts +4 -4
  66. package/src/transformers/vite-config.ts +2 -0
  67. package/src/utils/duration.ts +3 -0
  68. package/src/utils/logger.ts +143 -16
@@ -107,9 +107,11 @@ const buildApp = (handoff, skipComponents) => __awaiter(void 0, void 0, void 0,
107
107
  // Build app
108
108
  const buildResult = cross_spawn_1.default.sync('npx', ['next', 'build'], {
109
109
  cwd: appPath,
110
- stdio: 'inherit',
110
+ stdio: ['inherit', 'pipe', 'pipe'],
111
111
  env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'production' }),
112
112
  });
113
+ logger_1.Logger.childProcessBuffer(buildResult.stdout);
114
+ logger_1.Logger.childProcessBuffer(buildResult.stderr);
113
115
  if (buildResult.status !== 0) {
114
116
  let errorMsg = `Next.js build failed with exit code ${buildResult.status}`;
115
117
  if (buildResult.error) {
@@ -155,19 +157,28 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
155
157
  // create empty directory
156
158
  yield fs_extra_1.default.ensureDir(moduleOutput);
157
159
  }
160
+ logger_1.Logger.info(`Starting Next.js dev server (Turbopack) at http://${hostname}:${port}…`);
158
161
  const nextProcess = (0, cross_spawn_1.default)('npx', ['next', 'dev', '--turbopack', '--port', String(port)], {
159
162
  cwd: appPath,
160
- stdio: 'inherit',
163
+ stdio: ['inherit', 'pipe', 'pipe'],
161
164
  env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'development' }),
162
165
  });
163
- logger_1.Logger.success(`Ready on http://${hostname}:${port}`);
166
+ logger_1.Logger.pipeChildStreams(nextProcess.stdout, nextProcess.stderr);
164
167
  nextProcess.on('error', (error) => {
165
- logger_1.Logger.error(`Next.js dev process error: ${error}`);
168
+ logger_1.Logger.error(`Next.js dev process failed to start: ${error}`);
166
169
  process.exit(1);
167
170
  });
168
- nextProcess.on('close', (code) => {
169
- logger_1.Logger.success(`Next.js dev process closed with code ${code}`);
170
- process.exit(code);
171
+ nextProcess.on('close', (code, signal) => {
172
+ if (code === 0) {
173
+ logger_1.Logger.success(`Next.js dev process exited normally`);
174
+ }
175
+ else if (signal) {
176
+ logger_1.Logger.warn(`Next.js dev process stopped (${signal})`);
177
+ }
178
+ else {
179
+ logger_1.Logger.error(`Next.js dev process exited with code ${code}`);
180
+ }
181
+ process.exit(code !== null && code !== void 0 ? code : 1);
171
182
  });
172
183
  const wss = yield (0, websocket_1.createWebSocketServer)((_d = (_c = handoff.config.app.ports) === null || _c === void 0 ? void 0 : _c.websocket) !== null && _d !== void 0 ? _d : 3001);
173
184
  const chokidarConfig = {
@@ -205,12 +216,15 @@ const devApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
205
216
  }
206
217
  // Persist client configuration
207
218
  yield (0, client_config_1.persistClientConfig)(handoff);
208
- // Run
209
- const devResult = cross_spawn_1.default.sync('npx', ['next', 'dev', '--turbopack', '--port', String((_b = (_a = handoff.config.app.ports) === null || _a === void 0 ? void 0 : _a.app) !== null && _b !== void 0 ? _b : 3000)], {
219
+ const devPort = (_b = (_a = handoff.config.app.ports) === null || _a === void 0 ? void 0 : _a.app) !== null && _b !== void 0 ? _b : 3000;
220
+ logger_1.Logger.info(`Starting Next.js dev server (Turbopack) on port ${devPort}…`);
221
+ const devResult = cross_spawn_1.default.sync('npx', ['next', 'dev', '--turbopack', '--port', String(devPort)], {
210
222
  cwd: appPath,
211
- stdio: 'inherit',
223
+ stdio: ['inherit', 'pipe', 'pipe'],
212
224
  env: Object.assign(Object.assign({}, process.env), { NODE_ENV: 'development' }),
213
225
  });
226
+ logger_1.Logger.childProcessBuffer(devResult.stdout);
227
+ logger_1.Logger.childProcessBuffer(devResult.stderr);
214
228
  if (devResult.status !== 0) {
215
229
  let errorMsg = `Next.js dev failed with exit code ${devResult.status}`;
216
230
  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,9 +1,9 @@
1
1
  import Handoff from '../..';
2
- import { ConfigDiffStrategy } from './types';
2
+ import { ConfigDiffStrategy, FinalizeContext } from './types';
3
3
  export declare const getStrategy: (kind: string) => ConfigDiffStrategy | undefined;
4
4
  export declare const getAllStrategies: () => ConfigDiffStrategy[];
5
5
  /**
6
6
  * Runs every registered strategy finalizer.
7
7
  * Called after component rebuilds so derived artifacts stay up-to-date.
8
8
  */
9
- export declare const runAllFinalizers: (handoff: Handoff) => Promise<void>;
9
+ export declare const runAllFinalizers: (handoff: Handoff, context?: FinalizeContext) => Promise<void>;
@@ -22,9 +22,9 @@ exports.getAllStrategies = getAllStrategies;
22
22
  * Runs every registered strategy finalizer.
23
23
  * Called after component rebuilds so derived artifacts stay up-to-date.
24
24
  */
25
- const runAllFinalizers = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
25
+ const runAllFinalizers = (handoff, context) => __awaiter(void 0, void 0, void 0, function* () {
26
26
  for (const strategy of strategies.values()) {
27
- yield strategy.finalize(handoff);
27
+ yield strategy.finalize(handoff, context);
28
28
  }
29
29
  });
30
30
  exports.runAllFinalizers = runAllFinalizers;
@@ -35,6 +35,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
35
35
  exports.patternDiffStrategy = void 0;
36
36
  const patterns_1 = require("../../../pipeline/patterns");
37
37
  const builder_1 = __importStar(require("../../../transformers/preview/component/builder"));
38
+ const builder_2 = require("../../../transformers/preview/pattern/builder");
38
39
  const logger_1 = require("../../../utils/logger");
39
40
  const snapshot_1 = require("../snapshot");
40
41
  /**
@@ -85,8 +86,20 @@ exports.patternDiffStrategy = {
85
86
  },
86
87
  };
87
88
  },
88
- finalize(handoff) {
89
+ finalize(handoff, context) {
89
90
  return __awaiter(this, void 0, void 0, function* () {
91
+ if (context === null || context === void 0 ? void 0 : context.skipPatternFinalizer) {
92
+ return;
93
+ }
94
+ const componentIds = context === null || context === void 0 ? void 0 : context.patternRebuildComponentIds;
95
+ if (componentIds === null || componentIds === void 0 ? void 0 : componentIds.length) {
96
+ const patternIds = (0, builder_2.getPatternIdsReferencingComponents)(handoff, componentIds);
97
+ if (patternIds.length === 0) {
98
+ return;
99
+ }
100
+ yield (0, patterns_1.buildPatterns)(handoff, { onlyPatternIds: new Set(patternIds) });
101
+ return;
102
+ }
90
103
  yield (0, patterns_1.buildPatterns)(handoff);
91
104
  });
92
105
  },
@@ -1,4 +1,18 @@
1
1
  import Handoff from '../..';
2
+ /**
3
+ * Optional hints for post-build finalizers (e.g. selective pattern rebuild).
4
+ */
5
+ export type FinalizeContext = {
6
+ /**
7
+ * Only patterns that reference at least one of these component ids are rebuilt.
8
+ * When omitted, all patterns are rebuilt.
9
+ */
10
+ patternRebuildComponentIds?: string[];
11
+ /**
12
+ * When true, skip pattern artifact generation (no component preview HTML changed).
13
+ */
14
+ skipPatternFinalizer?: boolean;
15
+ };
2
16
  /**
3
17
  * Handle returned by ConfigDiffStrategy.capture().
4
18
  *
@@ -15,5 +29,5 @@ export interface RebuildHandle {
15
29
  export interface ConfigDiffStrategy {
16
30
  readonly kind: string;
17
31
  capture(handoff: Handoff, entityId: string): RebuildHandle;
18
- finalize(handoff: Handoff): Promise<void>;
32
+ finalize(handoff: Handoff, context?: FinalizeContext): Promise<void>;
19
33
  }
@@ -41,6 +41,15 @@ const getAppPath = (handoff) => {
41
41
  return path_1.default.resolve(handoff.modulePath, '.handoff', `${handoff.getProjectId()}`);
42
42
  };
43
43
  exports.getAppPath = getAppPath;
44
+ const mirrorDirectory = (sourcePath, destinationPath) => __awaiter(void 0, void 0, void 0, function* () {
45
+ if (!(yield fs_extra_1.default.pathExists(sourcePath))) {
46
+ yield fs_extra_1.default.remove(destinationPath);
47
+ return;
48
+ }
49
+ yield fs_extra_1.default.remove(destinationPath);
50
+ yield fs_extra_1.default.ensureDir(path_1.default.dirname(destinationPath));
51
+ yield fs_extra_1.default.copy(sourcePath, destinationPath, { overwrite: true });
52
+ });
44
53
  /**
45
54
  * Copy the public dir from the working dir to the module dir.
46
55
  */
@@ -48,9 +57,17 @@ const syncPublicFiles = (handoff) => __awaiter(void 0, void 0, void 0, function*
48
57
  const appPath = (0, exports.getAppPath)(handoff);
49
58
  const workingPublicPath = (0, exports.getWorkingPublicPath)(handoff);
50
59
  if (workingPublicPath) {
51
- yield fs_extra_1.default.copy(workingPublicPath, path_1.default.resolve(appPath, 'public'), {
60
+ const destinationPublicPath = path_1.default.resolve(appPath, 'public');
61
+ const sourceApiPath = path_1.default.resolve(workingPublicPath, 'api');
62
+ const destinationApiPath = path_1.default.resolve(destinationPublicPath, 'api');
63
+ yield fs_extra_1.default.copy(workingPublicPath, destinationPublicPath, {
52
64
  overwrite: true,
65
+ filter: (file) => {
66
+ const relativePath = path_1.default.relative(workingPublicPath, file);
67
+ return relativePath !== 'api' && !relativePath.startsWith(`api${path_1.default.sep}`);
68
+ },
53
69
  });
70
+ yield mirrorDirectory(sourceApiPath, destinationApiPath);
54
71
  }
55
72
  });
56
73
  exports.syncPublicFiles = syncPublicFiles;
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
26
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
27
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -16,7 +39,7 @@ exports.watchComponentDirectories = exports.watchRuntimeConfiguration = exports.
16
39
  const chokidar_1 = __importDefault(require("chokidar"));
17
40
  const fs_extra_1 = __importDefault(require("fs-extra"));
18
41
  const path_1 = __importDefault(require("path"));
19
- const builder_1 = __importDefault(require("../transformers/preview/component/builder"));
42
+ const builder_1 = __importStar(require("../transformers/preview/component/builder"));
20
43
  const css_1 = require("../transformers/preview/component/css");
21
44
  const javascript_1 = require("../transformers/preview/component/javascript");
22
45
  const logger_1 = require("../utils/logger");
@@ -181,8 +204,17 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
181
204
  const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
182
205
  const segmentToUpdate = entryType ? (0, component_1.mapEntryTypeToSegment)(entryType) : undefined;
183
206
  const componentId = (0, component_1.resolveComponentIdForChangedFile)(handoff, file);
207
+ const skipPatterns = segmentToUpdate === builder_1.ComponentSegment.JavaScript ||
208
+ segmentToUpdate === builder_1.ComponentSegment.Style;
209
+ let finalizeContext;
210
+ if (skipPatterns) {
211
+ finalizeContext = { skipPatternFinalizer: true };
212
+ }
213
+ else if (componentId) {
214
+ finalizeContext = { patternRebuildComponentIds: [componentId] };
215
+ }
184
216
  yield (0, builder_1.default)(handoff, componentId, segmentToUpdate);
185
- yield (0, config_diff_1.runAllFinalizers)(handoff);
217
+ yield (0, config_diff_1.runAllFinalizers)(handoff, finalizeContext);
186
218
  }
187
219
  catch (e) {
188
220
  logger_1.Logger.error('Error processing component:', e);
@@ -298,7 +330,7 @@ const watchComponentDirectories = (handoff, state, chokidarConfig) => {
298
330
  (0, exports.watchRuntimeComponents)(handoff, state, (0, component_1.getRuntimeComponentsPathsToWatch)(handoff));
299
331
  (0, exports.watchRuntimeConfiguration)(handoff, state);
300
332
  yield (0, builder_1.default)(handoff, dirName);
301
- yield (0, config_diff_1.runAllFinalizers)(handoff);
333
+ yield (0, config_diff_1.runAllFinalizers)(handoff, { patternRebuildComponentIds: [dirName] });
302
334
  }
303
335
  catch (e) {
304
336
  logger_1.Logger.error('Error processing new component:', e);
@@ -20,7 +20,14 @@ const logger_1 = require("../utils/logger");
20
20
  * @returns A function that accepts a message string and broadcasts it to all connected clients.
21
21
  */
22
22
  const createWebSocketServer = (...args_1) => __awaiter(void 0, [...args_1], void 0, function* (port = 3001) {
23
+ logger_1.Logger.info(`Starting WebSocket server for live reload on port ${port}…`);
23
24
  const wss = new ws_1.WebSocket.Server({ port });
25
+ wss.on('listening', () => {
26
+ logger_1.Logger.info(`WebSocket server listening on ws://localhost:${port} (live reload)`);
27
+ });
28
+ wss.on('error', (err) => {
29
+ logger_1.Logger.error(`WebSocket server failed${err instanceof Error ? `: ${err.message}` : ''}`, err instanceof Error ? err : undefined);
30
+ });
24
31
  // Heartbeat function to mark a connection as alive.
25
32
  const heartbeat = function () {
26
33
  this.isAlive = true;
@@ -30,7 +37,7 @@ const createWebSocketServer = (...args_1) => __awaiter(void 0, [...args_1], void
30
37
  const extWs = ws;
31
38
  extWs.isAlive = true;
32
39
  extWs.send(JSON.stringify({ type: 'WELCOME' }));
33
- extWs.on('error', (error) => logger_1.Logger.error('WebSocket error:', error));
40
+ extWs.on('error', (error) => logger_1.Logger.error('WebSocket client error:', error));
34
41
  extWs.on('pong', heartbeat);
35
42
  });
36
43
  // Periodically ping clients to ensure they are still connected
@@ -38,7 +45,7 @@ const createWebSocketServer = (...args_1) => __awaiter(void 0, [...args_1], void
38
45
  wss.clients.forEach((client) => {
39
46
  const extWs = client;
40
47
  if (!extWs.isAlive) {
41
- logger_1.Logger.warn('Terminating inactive client');
48
+ logger_1.Logger.debug('Terminating inactive WebSocket client');
42
49
  return client.terminate();
43
50
  }
44
51
  extWs.isAlive = false;
@@ -49,10 +56,9 @@ const createWebSocketServer = (...args_1) => __awaiter(void 0, [...args_1], void
49
56
  wss.on('close', () => {
50
57
  clearInterval(pingInterval);
51
58
  });
52
- logger_1.Logger.success(`WebSocket server listening on ws://localhost:${port}`);
53
59
  // Return a function to broadcast a message to all connected clients
54
60
  return (message) => {
55
- logger_1.Logger.success(`Broadcasting message to ${wss.clients.size} client(s)`);
61
+ logger_1.Logger.debug(`Broadcasting reload to ${wss.clients.size} WebSocket client(s)`);
56
62
  wss.clients.forEach((client) => {
57
63
  if (client.readyState === ws_1.WebSocket.OPEN) {
58
64
  client.send(message);
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ class HandoffCliError extends Error {
19
19
  * Show the help message
20
20
  */
21
21
  const showVersion = () => {
22
- return 'Handoff App - 1.2.0-0';
22
+ return 'Handoff App - 1.2.0-2';
23
23
  };
24
24
  /**
25
25
  * Define a CLI error
@@ -3,7 +3,6 @@ type RawPatternDeclaration = Record<string, any>;
3
3
  type NormalizePatternOptions = {
4
4
  declarationPath: string;
5
5
  fallbackId: string;
6
- warn: (message: string) => void;
7
6
  };
8
7
  export declare const normalizePatternDeclaration: (raw: RawPatternDeclaration, options: NormalizePatternOptions) => PatternListObject;
9
8
  export {};
@@ -13,9 +13,6 @@ const normalizePatternDeclaration = (raw, options) => {
13
13
  throw new Error(`Pattern "${options.fallbackId}" in "${options.declarationPath}" has an invalid component reference at index ${index}: missing "id".`);
14
14
  }
15
15
  const preview = typeof ref.preview === 'string' ? ref.preview.trim() : undefined;
16
- if (!preview && !ref.args) {
17
- options.warn(`Pattern "${options.fallbackId}" component ref "${ref.id}" at index ${index} has neither "preview" nor "args". The component's default preview will be used.`);
18
- }
19
16
  return {
20
17
  id: ref.id.trim(),
21
18
  preview,
@@ -217,7 +217,6 @@ const initRuntimeConfig = (handoff) => {
217
217
  pattern = (0, pattern_1.normalizePatternDeclaration)(rawPattern, {
218
218
  declarationPath: resolvedPatternConfigPath,
219
219
  fallbackId: patternBaseName,
220
- warn: (message) => logger_1.Logger.warn(message),
221
220
  });
222
221
  }
223
222
  catch (err) {
@@ -313,11 +312,18 @@ const injectPatternPreviews = (result) => {
313
312
  ref.resolvedPreview = ref.preview;
314
313
  continue;
315
314
  }
316
- // Case 2: no preview AND no args -> use the component's first existing preview
315
+ // Case 2: no preview AND no args -> use the component's first existing preview (never assume "default")
317
316
  if (!ref.preview && !ref.args) {
318
317
  const previewKeys = Object.keys(component.previews || {});
319
- ref.resolvedPreview = previewKeys[0] || 'default';
320
- ref.resolved = true;
318
+ const firstPreview = previewKeys[0];
319
+ if (firstPreview) {
320
+ ref.resolvedPreview = firstPreview;
321
+ ref.resolved = true;
322
+ }
323
+ else {
324
+ logger_1.Logger.warn(`Pattern "${patternId}" component ref "${ref.id}" has neither "preview" nor "args", and the component has no previews. This fragment will be skipped.`);
325
+ ref.resolved = false;
326
+ }
321
327
  continue;
322
328
  }
323
329
  // Case 3: args present (with or without preview base) -> create synthetic preview
@@ -1,6 +1,8 @@
1
1
  import Handoff from '..';
2
+ import { ProcessPatternsOptions } from '../transformers/preview/pattern/builder';
3
+ export type { ProcessPatternsOptions };
2
4
  /**
3
5
  * Builds pattern previews by composing pre-built component preview HTML.
4
6
  * Must be called AFTER buildComponents.
5
7
  */
6
- export declare const buildPatterns: (handoff: Handoff) => Promise<void>;
8
+ export declare const buildPatterns: (handoff: Handoff, options?: ProcessPatternsOptions) => Promise<void>;
@@ -15,7 +15,7 @@ const builder_1 = require("../transformers/preview/pattern/builder");
15
15
  * Builds pattern previews by composing pre-built component preview HTML.
16
16
  * Must be called AFTER buildComponents.
17
17
  */
18
- const buildPatterns = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
19
- yield (0, builder_1.processPatterns)(handoff);
18
+ const buildPatterns = (handoff, options) => __awaiter(void 0, void 0, void 0, function* () {
19
+ yield (0, builder_1.processPatterns)(handoff, options);
20
20
  });
21
21
  exports.buildPatterns = buildPatterns;
@@ -22,6 +22,7 @@ const react_element_to_jsx_string_1 = __importDefault(require("react-element-to-
22
22
  const logger_1 = require("../../utils/logger");
23
23
  const docgen_1 = require("../docgen");
24
24
  const component_1 = require("../preview/component");
25
+ const css_1 = require("../preview/component/css");
25
26
  const html_1 = require("../utils/html");
26
27
  const module_1 = require("../utils/module");
27
28
  const schema_1 = require("../utils/schema");
@@ -240,7 +241,7 @@ function createHtmlDocument(componentId, previewTitle, renderedHtml) {
240
241
  <html lang="en">
241
242
  <head>
242
243
  <meta charset="UTF-8" />
243
- <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/main.css" />
244
+ <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/${css_1.MAIN_COMPONENT_CSS_FILE}" />
244
245
  <link rel="stylesheet" href="${(_b = process.env.HANDOFF_APP_BASE_PATH) !== null && _b !== void 0 ? _b : ''}/api/component/${componentId}.css" />
245
246
  <link rel="stylesheet" href="${(_c = process.env.HANDOFF_APP_BASE_PATH) !== null && _c !== void 0 ? _c : ''}/assets/css/preview.css" />
246
247
  <title>${previewTitle}</title>
@@ -21,6 +21,7 @@ const server_1 = __importDefault(require("react-dom/server"));
21
21
  const vite_1 = require("vite");
22
22
  const logger_1 = require("../../utils/logger");
23
23
  const docgen_1 = require("../docgen");
24
+ const css_1 = require("../preview/component/css");
24
25
  const build_1 = require("../utils/build");
25
26
  const html_1 = require("../utils/html");
26
27
  const module_1 = require("../utils/module");
@@ -117,7 +118,7 @@ function generateHtmlDocument(componentId, previewTitle, renderedHtml, clientJs,
117
118
  <html lang="en">
118
119
  <head>
119
120
  <meta charset="UTF-8" />
120
- <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/main.css" />
121
+ <link rel="stylesheet" href="${(_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : ''}/api/component/${css_1.MAIN_COMPONENT_CSS_FILE}" />
121
122
  <link rel="stylesheet" href="${(_b = process.env.HANDOFF_APP_BASE_PATH) !== null && _b !== void 0 ? _b : ''}/api/component/${componentId}.css" />
122
123
  <link rel="stylesheet" href="${(_c = process.env.HANDOFF_APP_BASE_PATH) !== null && _c !== void 0 ? _c : ''}/assets/css/preview.css" />
123
124
  <script id="${PLUGIN_CONSTANTS.PROPS_SCRIPT_ID}" type="application/json">${JSON.stringify(props)}</script>
@@ -1,19 +1,8 @@
1
1
  import Handoff from '../../../index';
2
2
  import { ComponentListObject, TransformComponentTokensResult } from '../types';
3
+ export declare const sanitizeComponentApiData: (component: TransformComponentTokensResult) => TransformComponentTokensResult;
3
4
  export declare const getAPIPath: (handoff: Handoff) => string;
4
- /**
5
- * Build the preview API from the component data
6
- * @param handoff
7
- * @param componentData
8
- */
9
- declare const writeComponentSummaryAPI: (handoff: Handoff, componentData: ComponentListObject[]) => Promise<void>;
10
5
  export declare const writeComponentApi: (id: string, component: TransformComponentTokensResult, handoff: Handoff, preserveKeys?: string[]) => Promise<void>;
11
- /**
12
- * Update the main component summary API with the new component data
13
- * @param handoff
14
- * @param componentData
15
- */
16
- export declare const updateComponentSummaryApi: (handoff: Handoff, componentData: ComponentListObject[], isFullRebuild?: boolean) => Promise<void>;
17
6
  /**
18
7
  * Read the component API data
19
8
  * @param handoff
@@ -28,4 +17,4 @@ export declare const readComponentApi: (handoff: Handoff, id: string) => Promise
28
17
  * @returns The component summary or null if not found
29
18
  */
30
19
  export declare const readComponentMetadataApi: (handoff: Handoff, id: string) => Promise<ComponentListObject | null>;
31
- export default writeComponentSummaryAPI;
20
+ export default writeComponentApi;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.readComponentMetadataApi = exports.readComponentApi = exports.updateComponentSummaryApi = exports.writeComponentApi = exports.getAPIPath = void 0;
15
+ exports.readComponentMetadataApi = exports.readComponentApi = exports.writeComponentApi = exports.getAPIPath = exports.sanitizeComponentApiData = void 0;
16
16
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
17
  const path_1 = __importDefault(require("path"));
18
18
  const previews_1 = require("./previews");
@@ -23,6 +23,7 @@ const sanitizeComponentApiData = (component) => {
23
23
  delete sanitized.internalPatternPreviews;
24
24
  return sanitized;
25
25
  };
26
+ exports.sanitizeComponentApiData = sanitizeComponentApiData;
26
27
  /**
27
28
  * Merges values from a source object into a target object, returning a new object.
28
29
  * For each key present in either object:
@@ -62,28 +63,19 @@ const getAPIPath = (handoff) => {
62
63
  return apiPath;
63
64
  };
64
65
  exports.getAPIPath = getAPIPath;
65
- /**
66
- * Build the preview API from the component data
67
- * @param handoff
68
- * @param componentData
69
- */
70
- const writeComponentSummaryAPI = (handoff, componentData) => __awaiter(void 0, void 0, void 0, function* () {
71
- componentData.sort((a, b) => a.title.localeCompare(b.title));
72
- yield fs_extra_1.default.writeFile(path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json'), JSON.stringify(componentData, null, 2));
73
- });
74
66
  const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter(void 0, [id_1, component_1, handoff_1, ...args_1], void 0, function* (id, component, handoff, preserveKeys = []) {
75
67
  const outputDirPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'component');
76
68
  const outputFilePath = path_1.default.resolve(outputDirPath, `${id}.json`);
77
- const sanitizedComponent = sanitizeComponentApiData(component);
69
+ const sanitizedComponent = (0, exports.sanitizeComponentApiData)(component);
78
70
  if (fs_extra_1.default.existsSync(outputFilePath)) {
79
71
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
80
72
  if (existingJson) {
81
73
  try {
82
- const existingData = sanitizeComponentApiData(JSON.parse(existingJson));
74
+ const existingData = (0, exports.sanitizeComponentApiData)(JSON.parse(existingJson));
83
75
  // Special case: always allow page to be cleared when undefined
84
76
  // This handles the case where page slices are removed
85
77
  const finalPreserveKeys = sanitizedComponent.page === undefined ? preserveKeys.filter((key) => key !== 'page') : preserveKeys;
86
- const mergedData = sanitizeComponentApiData(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
78
+ const mergedData = (0, exports.sanitizeComponentApiData)(updateObject(existingData, sanitizedComponent, finalPreserveKeys));
87
79
  yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(mergedData, null, 2));
88
80
  return;
89
81
  }
@@ -98,37 +90,6 @@ const writeComponentApi = (id_1, component_1, handoff_1, ...args_1) => __awaiter
98
90
  yield fs_extra_1.default.writeFile(outputFilePath, JSON.stringify(sanitizedComponent, null, 2));
99
91
  });
100
92
  exports.writeComponentApi = writeComponentApi;
101
- /**
102
- * Update the main component summary API with the new component data
103
- * @param handoff
104
- * @param componentData
105
- */
106
- const updateComponentSummaryApi = (handoff_1, componentData_1, ...args_1) => __awaiter(void 0, [handoff_1, componentData_1, ...args_1], void 0, function* (handoff, componentData, isFullRebuild = false) {
107
- if (isFullRebuild) {
108
- // Full rebuild: replace the entire file
109
- yield writeComponentSummaryAPI(handoff, componentData);
110
- return;
111
- }
112
- // Partial update: merge with existing data
113
- const apiPath = path_1.default.resolve((0, exports.getAPIPath)(handoff), 'components.json');
114
- let existingData = [];
115
- if (fs_extra_1.default.existsSync(apiPath)) {
116
- try {
117
- const existing = yield fs_extra_1.default.readFile(apiPath, 'utf8');
118
- existingData = JSON.parse(existing).map((component) => (Object.assign(Object.assign({}, component), { previews: (0, previews_1.getDocumentedPreviews)(component.previews) })));
119
- }
120
- catch (_a) {
121
- // Corrupt or missing JSON — treat as empty
122
- existingData = [];
123
- }
124
- }
125
- // Replace existing entries with same ID
126
- const incomingIds = new Set(componentData.map((c) => c.id));
127
- const merged = [...componentData, ...existingData.filter((c) => !incomingIds.has(c.id))];
128
- // Always write the file (even if merged is empty)
129
- yield writeComponentSummaryAPI(handoff, merged);
130
- });
131
- exports.updateComponentSummaryApi = updateComponentSummaryApi;
132
93
  /**
133
94
  * Read the component API data
134
95
  * @param handoff
@@ -141,7 +102,7 @@ const readComponentApi = (handoff, id) => __awaiter(void 0, void 0, void 0, func
141
102
  try {
142
103
  const existingJson = yield fs_extra_1.default.readFile(outputFilePath, 'utf8');
143
104
  if (existingJson) {
144
- return sanitizeComponentApiData(JSON.parse(existingJson));
105
+ return (0, exports.sanitizeComponentApiData)(JSON.parse(existingJson));
145
106
  }
146
107
  }
147
108
  catch (_) {
@@ -180,4 +141,4 @@ const readComponentMetadataApi = (handoff, id) => __awaiter(void 0, void 0, void
180
141
  };
181
142
  });
182
143
  exports.readComponentMetadataApi = readComponentMetadataApi;
183
- exports.default = writeComponentSummaryAPI;
144
+ exports.default = exports.writeComponentApi;
@@ -0,0 +1,4 @@
1
+ import Handoff from '../../../index';
2
+ export declare const getComponentApiPath: (handoff: Handoff) => string;
3
+ export declare const removeComponentApi: (handoff: Handoff, id: string) => Promise<void>;
4
+ export declare const syncComponentArtifacts: (handoff: Handoff) => Promise<void>;