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.
Files changed (160) hide show
  1. package/config/config.template.js +21 -14
  2. package/config/config.template.ts +54 -0
  3. package/config/docs/system.md +2 -0
  4. package/dist/app-builder/build.js +31 -12
  5. package/dist/app-builder/config-diff/index.d.ts +4 -0
  6. package/dist/app-builder/config-diff/index.js +12 -0
  7. package/dist/app-builder/config-diff/registry.d.ts +9 -0
  8. package/dist/app-builder/config-diff/registry.js +30 -0
  9. package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
  10. package/dist/app-builder/config-diff/snapshot.js +43 -0
  11. package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
  12. package/dist/app-builder/config-diff/strategies/pattern.js +106 -0
  13. package/dist/app-builder/config-diff/types.d.ts +33 -0
  14. package/dist/app-builder/config-diff/types.js +2 -0
  15. package/dist/app-builder/watchers/component.d.ts +15 -0
  16. package/dist/app-builder/watchers/component.js +69 -0
  17. package/dist/app-builder/watchers/utils.d.ts +14 -0
  18. package/dist/app-builder/watchers/utils.js +44 -0
  19. package/dist/app-builder/watchers.d.ts +10 -19
  20. package/dist/app-builder/watchers.js +65 -79
  21. package/dist/app-builder/websocket.js +10 -4
  22. package/dist/cache/build-cache.js +14 -5
  23. package/dist/cli/create.js +67 -23
  24. package/dist/cli/eject.js +48 -7
  25. package/dist/cli/make.js +29 -4
  26. package/dist/cli/scaffold.js +60 -19
  27. package/dist/cli.js +1 -1
  28. package/dist/config/helpers.d.ts +7 -0
  29. package/dist/config/helpers.js +31 -0
  30. package/dist/config/index.d.ts +2 -1
  31. package/dist/config/index.js +5 -1
  32. package/dist/config/loader.d.ts +10 -1
  33. package/dist/config/loader.js +92 -28
  34. package/dist/config/normalizers/declaration.d.ts +11 -0
  35. package/dist/config/normalizers/declaration.js +119 -0
  36. package/dist/config/normalizers/pattern.d.ts +8 -0
  37. package/dist/config/normalizers/pattern.js +35 -0
  38. package/dist/config/runtime.d.ts +9 -3
  39. package/dist/config/runtime.js +260 -33
  40. package/dist/declarations/helpers.d.ts +6 -0
  41. package/dist/declarations/helpers.js +19 -0
  42. package/dist/declarations/index.d.ts +2 -0
  43. package/dist/declarations/index.js +9 -0
  44. package/dist/declarations/types.d.ts +60 -0
  45. package/dist/declarations/types.js +2 -0
  46. package/dist/index.d.ts +12 -1
  47. package/dist/index.js +34 -5
  48. package/dist/pipeline/index.d.ts +1 -0
  49. package/dist/pipeline/index.js +3 -1
  50. package/dist/pipeline/patterns.d.ts +8 -0
  51. package/dist/pipeline/patterns.js +21 -0
  52. package/dist/transformers/docgen/index.js +12 -2
  53. package/dist/transformers/plugins/csf-render.js +170 -34
  54. package/dist/transformers/plugins/ssr-render.js +18 -2
  55. package/dist/transformers/preview/component/api.js +16 -7
  56. package/dist/transformers/preview/component/builder.js +61 -8
  57. package/dist/transformers/preview/component/css.d.ts +8 -0
  58. package/dist/transformers/preview/component/css.js +6 -4
  59. package/dist/transformers/preview/component/html.js +14 -5
  60. package/dist/transformers/preview/component/javascript.d.ts +0 -1
  61. package/dist/transformers/preview/component/javascript.js +25 -14
  62. package/dist/transformers/preview/component/previews.d.ts +9 -0
  63. package/dist/transformers/preview/component/previews.js +10 -0
  64. package/dist/transformers/preview/pattern/api.d.ts +11 -0
  65. package/dist/transformers/preview/pattern/api.js +79 -0
  66. package/dist/transformers/preview/pattern/builder.d.ts +17 -0
  67. package/dist/transformers/preview/pattern/builder.js +167 -0
  68. package/dist/transformers/preview/pattern/html.d.ts +19 -0
  69. package/dist/transformers/preview/pattern/html.js +94 -0
  70. package/dist/transformers/preview/types.d.ts +54 -0
  71. package/dist/transformers/utils/csf.d.ts +16 -0
  72. package/dist/transformers/utils/csf.js +30 -0
  73. package/dist/transformers/utils/index.d.ts +1 -0
  74. package/dist/transformers/utils/index.js +2 -0
  75. package/dist/transformers/utils/module.d.ts +5 -0
  76. package/dist/transformers/utils/module.js +26 -13
  77. package/dist/transformers/utils/usage.d.ts +23 -0
  78. package/dist/transformers/utils/usage.js +294 -0
  79. package/dist/transformers/utils/vite-logger.js +4 -4
  80. package/dist/transformers/vite-config.js +2 -0
  81. package/dist/types/config.d.ts +32 -7
  82. package/dist/types/preview.d.ts +2 -0
  83. package/dist/utils/duration.d.ts +2 -0
  84. package/dist/utils/duration.js +6 -0
  85. package/dist/utils/logger.d.ts +42 -5
  86. package/dist/utils/logger.js +135 -15
  87. package/docs/cli.md +3 -3
  88. package/package.json +3 -1
  89. package/src/app/components/Component/PageSliceResolver.tsx +6 -6
  90. package/src/app/components/Component/Preview.tsx +17 -12
  91. package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
  92. package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
  93. package/src/app/components/util/index.ts +71 -2
  94. package/src/app/pages/system/pattern/[pattern]/index.tsx +346 -0
  95. package/src/app/pages/system/pattern/index.tsx +14 -0
  96. package/src/app-builder/README.md +37 -9
  97. package/src/app-builder/build.ts +34 -12
  98. package/src/app-builder/config-diff/index.ts +4 -0
  99. package/src/app-builder/config-diff/registry.ts +23 -0
  100. package/src/app-builder/config-diff/snapshot.ts +51 -0
  101. package/src/app-builder/config-diff/strategies/pattern.ts +80 -0
  102. package/src/app-builder/config-diff/types.ts +36 -0
  103. package/src/app-builder/watchers/component.ts +72 -0
  104. package/src/app-builder/watchers/utils.ts +44 -0
  105. package/src/app-builder/watchers.ts +74 -80
  106. package/src/app-builder/websocket.ts +16 -5
  107. package/src/cache/build-cache.ts +14 -5
  108. package/src/cli/create.ts +86 -25
  109. package/src/cli/eject.ts +29 -7
  110. package/src/cli/make.ts +32 -5
  111. package/src/cli/scaffold.ts +68 -21
  112. package/src/cli.ts +1 -1
  113. package/src/config/README.md +10 -1
  114. package/src/config/helpers.ts +43 -0
  115. package/src/config/index.ts +4 -1
  116. package/src/config/loader.ts +110 -31
  117. package/src/config/normalizers/declaration.ts +176 -0
  118. package/src/config/normalizers/pattern.ts +56 -0
  119. package/src/config/runtime.ts +317 -35
  120. package/src/declarations/helpers.ts +36 -0
  121. package/src/declarations/index.ts +19 -0
  122. package/src/declarations/types.ts +70 -0
  123. package/src/index.ts +47 -6
  124. package/src/pipeline/index.ts +1 -0
  125. package/src/pipeline/patterns.ts +12 -0
  126. package/src/transformers/README.md +2 -2
  127. package/src/transformers/docgen/index.ts +13 -2
  128. package/src/transformers/plugins/csf-render.ts +216 -49
  129. package/src/transformers/plugins/ssr-render.ts +23 -7
  130. package/src/transformers/preview/component/api.ts +27 -7
  131. package/src/transformers/preview/component/builder.ts +82 -9
  132. package/src/transformers/preview/component/css.ts +6 -5
  133. package/src/transformers/preview/component/html.ts +18 -5
  134. package/src/transformers/preview/component/javascript.ts +29 -19
  135. package/src/transformers/preview/component/previews.ts +12 -0
  136. package/src/transformers/preview/pattern/api.ts +83 -0
  137. package/src/transformers/preview/pattern/builder.ts +192 -0
  138. package/src/transformers/preview/pattern/html.ts +107 -0
  139. package/src/transformers/preview/types.ts +58 -1
  140. package/src/transformers/utils/csf.ts +48 -0
  141. package/src/transformers/utils/index.ts +3 -0
  142. package/src/transformers/utils/module.ts +33 -18
  143. package/src/transformers/utils/usage.ts +319 -0
  144. package/src/transformers/utils/vite-logger.ts +4 -4
  145. package/src/transformers/vite-config.ts +2 -0
  146. package/src/types/README.md +1 -1
  147. package/src/types/config.ts +33 -7
  148. package/src/types/preview.ts +2 -0
  149. package/src/utils/duration.ts +3 -0
  150. package/src/utils/logger.ts +143 -16
  151. package/templates/default/components/button/Button.jsx.tpl +64 -0
  152. package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
  153. package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
  154. package/templates/default/handoff.config.js.tpl +29 -16
  155. package/templates/default/handoff.config.ts.tpl +196 -0
  156. package/templates/default/handoff.config.with-examples.js.tpl +28 -17
  157. package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
  158. package/templates/default/package.json.tpl +1 -3
  159. package/handoff.config.js +0 -90
  160. package/templates/default/components/button/button.js.tpl +0 -62
@@ -1,13 +1,9 @@
1
1
  import chokidar from 'chokidar';
2
2
  import Handoff from '..';
3
- import { ComponentSegment } from '../transformers/preview/component/builder';
4
- import { ComponentListObject } from '../transformers/preview/types';
5
- export interface WatcherState {
6
- debounce: boolean;
7
- runtimeComponentsWatcher: chokidar.FSWatcher | null;
8
- runtimeConfigurationWatcher: chokidar.FSWatcher | null;
9
- componentDirectoriesWatcher: chokidar.FSWatcher | null;
10
- }
3
+ import { RuntimeComponentEntryType } from './watchers/component';
4
+ import { WatcherState } from './watchers/utils';
5
+ export { getRuntimeComponentsPathsToWatch, mapEntryTypeToSegment } from './watchers/component';
6
+ export type { WatcherState } from './watchers/utils';
11
7
  /**
12
8
  * Watches the working public directory for changes and updates the app.
13
9
  */
@@ -26,22 +22,17 @@ export declare const watchPages: (handoff: Handoff, chokidarConfig: chokidar.Wat
26
22
  * since per-component builds may depend on these shared entries.
27
23
  */
28
24
  export declare const watchGlobalEntries: (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => void;
29
- /**
30
- * Maps configuration entry types to component segments.
31
- */
32
- export declare const mapEntryTypeToSegment: (type: keyof ComponentListObject["entries"]) => ComponentSegment | undefined;
33
- /**
34
- * Gets the paths of runtime components to watch.
35
- *
36
- * @returns A Map of paths to watch and their entry types
37
- */
38
- export declare const getRuntimeComponentsPathsToWatch: (handoff: Handoff) => Map<string, "template" | "js" | "scss">;
39
25
  /**
40
26
  * Watches runtime components for changes.
41
27
  */
42
- export declare const watchRuntimeComponents: (handoff: Handoff, state: WatcherState, runtimeComponentPathsToWatch: Map<string, keyof ComponentListObject["entries"]>) => void;
28
+ export declare const watchRuntimeComponents: (handoff: Handoff, state: WatcherState, runtimeComponentPathsToWatch: Map<string, RuntimeComponentEntryType>) => void;
43
29
  /**
44
30
  * Watches the runtime configuration for changes.
31
+ *
32
+ * Uses the generic {@link ConfigDiffStrategy} registry so that each entity
33
+ * kind (patterns today, potentially themes / slots later) supplies its own
34
+ * snapshot → diff → selective-rebuild logic without the watcher needing to
35
+ * know the details.
45
36
  */
46
37
  export declare const watchRuntimeConfiguration: (handoff: Handoff, state: WatcherState) => void;
47
38
  /**
@@ -35,17 +35,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  return (mod && mod.__esModule) ? mod : { "default": mod };
36
36
  };
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
- exports.watchComponentDirectories = exports.watchRuntimeConfiguration = exports.watchRuntimeComponents = exports.getRuntimeComponentsPathsToWatch = exports.mapEntryTypeToSegment = exports.watchGlobalEntries = exports.watchPages = exports.watchAppSource = exports.watchPublicDirectory = void 0;
38
+ exports.watchComponentDirectories = exports.watchRuntimeConfiguration = exports.watchRuntimeComponents = exports.watchGlobalEntries = exports.watchPages = exports.watchAppSource = exports.watchPublicDirectory = exports.mapEntryTypeToSegment = exports.getRuntimeComponentsPathsToWatch = void 0;
39
39
  const chokidar_1 = __importDefault(require("chokidar"));
40
40
  const fs_extra_1 = __importDefault(require("fs-extra"));
41
41
  const path_1 = __importDefault(require("path"));
42
+ const builder_1 = __importStar(require("../transformers/preview/component/builder"));
42
43
  const css_1 = require("../transformers/preview/component/css");
43
44
  const javascript_1 = require("../transformers/preview/component/javascript");
44
- const builder_1 = __importStar(require("../transformers/preview/component/builder"));
45
45
  const logger_1 = require("../utils/logger");
46
46
  const path_2 = require("../utils/path");
47
47
  const client_config_1 = require("./client-config");
48
+ const config_diff_1 = require("./config-diff");
48
49
  const paths_1 = require("./paths");
50
+ const component_1 = require("./watchers/component");
51
+ const utils_1 = require("./watchers/utils");
52
+ var component_2 = require("./watchers/component");
53
+ Object.defineProperty(exports, "getRuntimeComponentsPathsToWatch", { enumerable: true, get: function () { return component_2.getRuntimeComponentsPathsToWatch; } });
54
+ Object.defineProperty(exports, "mapEntryTypeToSegment", { enumerable: true, get: function () { return component_2.mapEntryTypeToSegment; } });
49
55
  /**
50
56
  * Watches the working public directory for changes and updates the app.
51
57
  */
@@ -56,8 +62,7 @@ const watchPublicDirectory = (handoff, wss, state, chokidarConfig) => {
56
62
  case 'add':
57
63
  case 'change':
58
64
  case 'unlink':
59
- if (!state.debounce) {
60
- state.debounce = true;
65
+ yield (0, utils_1.scheduleHandler)(state, 'publicDirectory', () => __awaiter(void 0, void 0, void 0, function* () {
61
66
  try {
62
67
  logger_1.Logger.warn('Public directory changed. Handoff will ingest the new data...');
63
68
  yield (0, paths_1.syncPublicFiles)(handoff);
@@ -66,10 +71,7 @@ const watchPublicDirectory = (handoff, wss, state, chokidarConfig) => {
66
71
  catch (e) {
67
72
  logger_1.Logger.error('Error syncing public directory:', e);
68
73
  }
69
- finally {
70
- state.debounce = false;
71
- }
72
- }
74
+ }));
73
75
  break;
74
76
  }
75
77
  }));
@@ -151,8 +153,7 @@ const watchGlobalEntries = (handoff, state, chokidarConfig) => {
151
153
  case 'add':
152
154
  case 'change':
153
155
  case 'unlink':
154
- if (!state.debounce) {
155
- state.debounce = true;
156
+ yield (0, utils_1.scheduleHandler)(state, 'globalEntries', () => __awaiter(void 0, void 0, void 0, function* () {
156
157
  try {
157
158
  logger_1.Logger.warn('Global entry changed. Rebuilding bundles and components...');
158
159
  const shouldRebuildMainCss = !!scssEntry && scssPathsToWatch.some((watchPath) => (0, path_2.isPathInside)(file, watchPath));
@@ -164,57 +165,17 @@ const watchGlobalEntries = (handoff, state, chokidarConfig) => {
164
165
  yield (0, javascript_1.buildMainJS)(handoff);
165
166
  }
166
167
  yield (0, builder_1.default)(handoff);
168
+ yield (0, config_diff_1.runAllFinalizers)(handoff);
167
169
  }
168
170
  catch (e) {
169
171
  logger_1.Logger.error('Error rebuilding after global entry change:', e);
170
172
  }
171
- finally {
172
- state.debounce = false;
173
- }
174
- }
173
+ }));
175
174
  break;
176
175
  }
177
176
  }));
178
177
  };
179
178
  exports.watchGlobalEntries = watchGlobalEntries;
180
- /**
181
- * Maps configuration entry types to component segments.
182
- */
183
- const mapEntryTypeToSegment = (type) => {
184
- return {
185
- js: builder_1.ComponentSegment.JavaScript,
186
- scss: builder_1.ComponentSegment.Style,
187
- template: builder_1.ComponentSegment.Previews,
188
- templates: builder_1.ComponentSegment.Previews,
189
- }[type];
190
- };
191
- exports.mapEntryTypeToSegment = mapEntryTypeToSegment;
192
- /**
193
- * Gets the paths of runtime components to watch.
194
- *
195
- * @returns A Map of paths to watch and their entry types
196
- */
197
- const getRuntimeComponentsPathsToWatch = (handoff) => {
198
- var _a, _b, _c;
199
- const result = new Map();
200
- for (const runtimeComponentId of Object.keys((_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries.components) !== null && _b !== void 0 ? _b : {})) {
201
- const runtimeComponent = handoff.runtimeConfig.entries.components[runtimeComponentId];
202
- for (const [runtimeComponentEntryType, runtimeComponentEntryPath] of Object.entries((_c = runtimeComponent.entries) !== null && _c !== void 0 ? _c : {})) {
203
- const normalizedComponentEntryPath = runtimeComponentEntryPath;
204
- if (fs_extra_1.default.existsSync(normalizedComponentEntryPath)) {
205
- const entryType = runtimeComponentEntryType;
206
- if (fs_extra_1.default.statSync(normalizedComponentEntryPath).isFile()) {
207
- result.set(path_1.default.resolve(normalizedComponentEntryPath), entryType);
208
- }
209
- else {
210
- result.set(normalizedComponentEntryPath, entryType);
211
- }
212
- }
213
- }
214
- }
215
- return result;
216
- };
217
- exports.getRuntimeComponentsPathsToWatch = getRuntimeComponentsPathsToWatch;
218
179
  /**
219
180
  * Watches runtime components for changes.
220
181
  */
@@ -238,21 +199,27 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
238
199
  case 'add':
239
200
  case 'change':
240
201
  case 'unlink':
241
- if (!state.debounce) {
242
- state.debounce = true;
202
+ yield (0, utils_1.scheduleHandler)(state, `runtimeComponent:${normalizedFile}`, () => __awaiter(void 0, void 0, void 0, function* () {
243
203
  try {
244
204
  const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
245
- const segmentToUpdate = entryType ? (0, exports.mapEntryTypeToSegment)(entryType) : undefined;
246
- const componentDir = path_1.default.basename(path_1.default.dirname(file));
247
- yield (0, builder_1.default)(handoff, componentDir, segmentToUpdate);
205
+ const segmentToUpdate = entryType ? (0, component_1.mapEntryTypeToSegment)(entryType) : undefined;
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
+ }
216
+ yield (0, builder_1.default)(handoff, componentId, segmentToUpdate);
217
+ yield (0, config_diff_1.runAllFinalizers)(handoff, finalizeContext);
248
218
  }
249
219
  catch (e) {
250
220
  logger_1.Logger.error('Error processing component:', e);
251
221
  }
252
- finally {
253
- state.debounce = false;
254
- }
255
- }
222
+ }));
256
223
  break;
257
224
  }
258
225
  }));
@@ -261,6 +228,11 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
261
228
  exports.watchRuntimeComponents = watchRuntimeComponents;
262
229
  /**
263
230
  * Watches the runtime configuration for changes.
231
+ *
232
+ * Uses the generic {@link ConfigDiffStrategy} registry so that each entity
233
+ * kind (patterns today, potentially themes / slots later) supplies its own
234
+ * snapshot → diff → selective-rebuild logic without the watcher needing to
235
+ * know the details.
264
236
  */
265
237
  const watchRuntimeConfiguration = (handoff, state) => {
266
238
  if (state.runtimeConfigurationWatcher) {
@@ -269,26 +241,44 @@ const watchRuntimeConfiguration = (handoff, state) => {
269
241
  if (handoff.getConfigFilePaths().length > 0) {
270
242
  state.runtimeConfigurationWatcher = chokidar_1.default.watch(handoff.getConfigFilePaths(), { ignoreInitial: true });
271
243
  state.runtimeConfigurationWatcher.on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
244
+ const normalizedFile = (0, path_2.normalizePathForCompare)(file);
272
245
  switch (event) {
273
246
  case 'add':
274
247
  case 'change':
275
248
  case 'unlink':
276
- if (!state.debounce) {
277
- state.debounce = true;
249
+ yield (0, utils_1.scheduleHandler)(state, `runtimeConfig:${normalizedFile}`, () => __awaiter(void 0, void 0, void 0, function* () {
250
+ var _a;
278
251
  try {
279
- file = path_1.default.dirname(file);
252
+ const changedFile = file;
253
+ // Look up the entity this config file belongs to BEFORE reload.
254
+ const entryBefore = handoff.getConfigFileEntry(changedFile);
255
+ const strategy = entryBefore ? (0, config_diff_1.getStrategy)(entryBefore.kind) : undefined;
256
+ const handle = entryBefore && strategy ? strategy.capture(handoff, entryBefore.entityId) : undefined;
280
257
  handoff.reload();
281
258
  yield (0, client_config_1.persistClientConfig)(handoff);
282
- (0, exports.watchRuntimeComponents)(handoff, state, (0, exports.getRuntimeComponentsPathsToWatch)(handoff));
283
- yield (0, builder_1.default)(handoff, path_1.default.basename(file));
259
+ (0, exports.watchRuntimeComponents)(handoff, state, (0, component_1.getRuntimeComponentsPathsToWatch)(handoff));
260
+ (0, exports.watchRuntimeConfiguration)(handoff, state);
261
+ const entryAfter = handoff.getConfigFileEntry(changedFile);
262
+ const normalizedChanged = (0, path_2.normalizePathForCompare)(changedFile);
263
+ const normalizedMainConfig = handoff.getMainConfigFilePath()
264
+ ? (0, path_2.normalizePathForCompare)(handoff.getMainConfigFilePath())
265
+ : undefined;
266
+ if (normalizedMainConfig && normalizedChanged === normalizedMainConfig) {
267
+ yield (0, builder_1.default)(handoff);
268
+ }
269
+ else if (handle) {
270
+ yield handle.apply(handoff, entryAfter === null || entryAfter === void 0 ? void 0 : entryAfter.entityId);
271
+ }
272
+ else {
273
+ const effectiveId = (_a = entryAfter === null || entryAfter === void 0 ? void 0 : entryAfter.entityId) !== null && _a !== void 0 ? _a : path_1.default.basename(path_1.default.dirname(changedFile));
274
+ yield (0, builder_1.default)(handoff, effectiveId);
275
+ }
276
+ yield (0, config_diff_1.runAllFinalizers)(handoff);
284
277
  }
285
278
  catch (e) {
286
279
  logger_1.Logger.error('Error reloading runtime configuration:', e);
287
280
  }
288
- finally {
289
- state.debounce = false;
290
- }
291
- }
281
+ }));
292
282
  break;
293
283
  }
294
284
  }));
@@ -321,35 +311,31 @@ const watchComponentDirectories = (handoff, state, chokidarConfig) => {
321
311
  const knownComponents = new Set(Object.keys((_e = (_d = (_c = handoff.runtimeConfig) === null || _c === void 0 ? void 0 : _c.entries) === null || _d === void 0 ? void 0 : _d.components) !== null && _e !== void 0 ? _e : {}));
322
312
  state.componentDirectoriesWatcher = chokidar_1.default.watch(dirsToWatch, Object.assign(Object.assign({}, chokidarConfig), { depth: 1 }));
323
313
  state.componentDirectoriesWatcher.on('add', (file) => __awaiter(void 0, void 0, void 0, function* () {
324
- var _a, _b, _c;
325
314
  const basename = path_1.default.basename(file);
326
315
  const dirName = path_1.default.basename(path_1.default.dirname(file));
327
316
  const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs');
328
317
  const isNewComponent = isConfigFile && basename.startsWith(dirName) && !knownComponents.has(dirName);
329
318
  if (!isNewComponent)
330
319
  return;
331
- if (!state.debounce) {
332
- state.debounce = true;
320
+ yield (0, utils_1.scheduleHandler)(state, `newComponent:${dirName}`, () => __awaiter(void 0, void 0, void 0, function* () {
321
+ var _a, _b, _c;
333
322
  try {
334
323
  logger_1.Logger.warn(`New component detected: ${dirName}. Reloading configuration...`);
335
324
  handoff.reload();
336
325
  knownComponents.add(dirName);
337
- // Update known set with all current components
338
326
  for (const id of Object.keys((_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {})) {
339
327
  knownComponents.add(id);
340
328
  }
341
329
  yield (0, client_config_1.persistClientConfig)(handoff);
342
- (0, exports.watchRuntimeComponents)(handoff, state, (0, exports.getRuntimeComponentsPathsToWatch)(handoff));
330
+ (0, exports.watchRuntimeComponents)(handoff, state, (0, component_1.getRuntimeComponentsPathsToWatch)(handoff));
343
331
  (0, exports.watchRuntimeConfiguration)(handoff, state);
344
332
  yield (0, builder_1.default)(handoff, dirName);
333
+ yield (0, config_diff_1.runAllFinalizers)(handoff, { patternRebuildComponentIds: [dirName] });
345
334
  }
346
335
  catch (e) {
347
336
  logger_1.Logger.error('Error processing new component:', e);
348
337
  }
349
- finally {
350
- state.debounce = false;
351
- }
352
- }
338
+ }));
353
339
  }));
354
340
  };
355
341
  exports.watchComponentDirectories = watchComponentDirectories;
@@ -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);
@@ -155,8 +155,8 @@ function getComponentFilePaths(handoff, componentId) {
155
155
  files.push(entries.scss);
156
156
  componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(entries.scss)));
157
157
  }
158
- // Handle both 'template' (singular) and 'templates' (plural) entry types
159
- const templatePath = entries.template || entries.templates;
158
+ // Handle template-style entries across renderer families
159
+ const templatePath = entries.template || entries.templates || entries.component || entries.story;
160
160
  if (templatePath) {
161
161
  try {
162
162
  const stat = fs_extra_1.default.statSync(templatePath);
@@ -178,10 +178,19 @@ function getComponentFilePaths(handoff, componentId) {
178
178
  }
179
179
  // Find the config file path for this component using exact config filename + directory matching.
180
180
  const configPaths = handoff.getConfigFilePaths();
181
- const expectedConfigFileNames = new Set([`${componentId}.json`, `${componentId}.js`, `${componentId}.cjs`]);
181
+ const expectedConfigFileNames = new Set([
182
+ `${componentId}.handoff.ts`,
183
+ `${componentId}.handoff.js`,
184
+ `${componentId}.handoff.cjs`,
185
+ `${componentId}.handoff.json`,
186
+ `${componentId}.json`,
187
+ `${componentId}.js`,
188
+ `${componentId}.cjs`,
189
+ ]);
182
190
  const matchingConfigPath = configPaths.find((configPath) => {
183
191
  const configFileName = path_1.default.basename(configPath);
184
- if (!expectedConfigFileNames.has(configFileName)) {
192
+ const isModernDeclaration = /\.handoff\.(ts|js|cjs|json)$/.test(configFileName);
193
+ if (!expectedConfigFileNames.has(configFileName) && !isModernDeclaration) {
185
194
  return false;
186
195
  }
187
196
  if (componentDirs.size === 0) {
@@ -210,7 +219,7 @@ function computeComponentFileStates(handoff, componentId) {
210
219
  }
211
220
  let templateDirFiles;
212
221
  if (templateDir) {
213
- templateDirFiles = yield (0, file_state_1.computeDirectoryState)(templateDir, ['.hbs', '.html', '.tsx', '.ts']);
222
+ templateDirFiles = yield (0, file_state_1.computeDirectoryState)(templateDir, ['.hbs', '.html', '.tsx', '.ts', '.jsx', '.js']);
214
223
  }
215
224
  return { files, templateDirFiles };
216
225
  });
@@ -85,22 +85,35 @@ const createDirectory = (dirPath) => {
85
85
  * Get mandatory templates based on whether examples are included
86
86
  * Uses different handoff config templates for blank vs. with-examples projects
87
87
  */
88
- const getMandatoryTemplates = (includeExamples) => [
89
- { source: 'package.json.tpl', destination: 'package.json' },
90
- {
91
- source: includeExamples ? 'handoff.config.with-examples.js.tpl' : 'handoff.config.js.tpl',
92
- destination: 'handoff.config.js'
93
- },
94
- { source: 'tsconfig.json.tpl', destination: 'tsconfig.json' },
95
- { source: 'gitignore.tpl', destination: '.gitignore' },
96
- { source: 'env.tpl', destination: '.env' },
97
- { source: 'README.md.tpl', destination: 'README.md' },
98
- ];
99
- // Example component files (only included when user opts for examples)
100
- const exampleTemplates = [
101
- { source: 'components/button/Button.tsx.tpl', destination: 'components/button/Button.tsx', isExample: true },
102
- { source: 'components/button/button.js.tpl', destination: 'components/button/button.js', isExample: true },
103
- ];
88
+ const getMandatoryTemplates = (includeExamples, languagePreference) => {
89
+ const configFormat = languagePreference;
90
+ const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
91
+ const configTemplateSource = `${configTemplateBaseName}.${configFormat === 'typescript' ? 'ts' : 'js'}.tpl`;
92
+ const configDestination = `handoff.config.${configFormat === 'typescript' ? 'ts' : 'js'}`;
93
+ const templates = [
94
+ { source: 'package.json.tpl', destination: 'package.json' },
95
+ {
96
+ source: configTemplateSource,
97
+ destination: configDestination,
98
+ },
99
+ { source: 'gitignore.tpl', destination: '.gitignore' },
100
+ { source: 'env.tpl', destination: '.env' },
101
+ { source: 'README.md.tpl', destination: 'README.md' },
102
+ ];
103
+ if (languagePreference === 'typescript') {
104
+ templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
105
+ }
106
+ return templates;
107
+ };
108
+ const getExampleTemplates = (languagePreference) => {
109
+ const declarationSuffix = languagePreference === 'typescript' ? 'ts' : 'js';
110
+ const declarationFile = `components/button/button.react.handoff.${declarationSuffix}.tpl`;
111
+ const componentFile = languagePreference === 'typescript' ? 'components/button/Button.tsx.tpl' : 'components/button/Button.jsx.tpl';
112
+ return [
113
+ { source: declarationFile, destination: `components/button/button.handoff.${languagePreference === 'typescript' ? 'ts' : 'js'}`, isExample: true },
114
+ { source: componentFile, destination: `components/button/Button.${languagePreference === 'typescript' ? 'tsx' : 'jsx'}`, isExample: true },
115
+ ];
116
+ };
104
117
  /**
105
118
  * Render a template string by replacing placeholders with values
106
119
  */
@@ -109,18 +122,34 @@ const renderTemplate = (content, data) => {
109
122
  if (!(key in data)) {
110
123
  console.warn(chalk_1.default.yellow(`Warning: Unknown template placeholder '{{${key}}}'`));
111
124
  }
112
- return data[key] || '';
125
+ const value = data[key];
126
+ return typeof value === 'string' ? value : '';
113
127
  });
114
128
  };
129
+ const getDependenciesToInstall = (options) => {
130
+ const dependencies = ['handoff-app@latest'];
131
+ const devDependencies = [];
132
+ if (options.includeExamples) {
133
+ dependencies.push('react', 'react-dom');
134
+ if (options.languagePreference === 'typescript') {
135
+ devDependencies.push('@types/react', '@types/react-dom');
136
+ }
137
+ }
138
+ return { dependencies, devDependencies };
139
+ };
115
140
  /**
116
- * Execute npm install
141
+ * Execute npm install for required dependencies.
117
142
  */
118
- const runNpmInstall = (projectPath) => __awaiter(void 0, void 0, void 0, function* () {
143
+ const installDependencies = (projectPath, options) => __awaiter(void 0, void 0, void 0, function* () {
144
+ const { dependencies, devDependencies } = getDependenciesToInstall(options);
119
145
  const spinner = p.spinner();
120
146
  spinner.start('Installing dependencies...');
121
147
  return new Promise((resolve, reject) => {
122
148
  var _a;
123
- const npmProcess = (0, child_process_1.spawn)('npm', ['install'], {
149
+ const installCommand = devDependencies.length
150
+ ? `npm install --save-prefix="^" ${dependencies.join(' ')} && npm install --save-dev --save-prefix="^" ${devDependencies.join(' ')}`
151
+ : `npm install --save-prefix="^" ${dependencies.join(' ')}`;
152
+ const npmProcess = (0, child_process_1.spawn)(installCommand, {
124
153
  cwd: projectPath,
125
154
  stdio: 'pipe',
126
155
  shell: true,
@@ -208,6 +237,19 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
208
237
  process.exit(0);
209
238
  }
210
239
  const includeExamples = projectType === 'with-examples';
240
+ const languagePreferenceSelection = yield p.select({
241
+ message: 'Preferred project language (declarations, examples, and handoff.config)?',
242
+ options: [
243
+ { value: 'typescript', label: 'TypeScript', hint: 'Uses .handoff.ts and handoff.config.ts' },
244
+ { value: 'javascript', label: 'JavaScript', hint: 'Uses .handoff.js and handoff.config.js' },
245
+ ],
246
+ initialValue: 'typescript',
247
+ });
248
+ if (p.isCancel(languagePreferenceSelection)) {
249
+ p.cancel('Project creation cancelled.');
250
+ process.exit(0);
251
+ }
252
+ const languagePreference = languagePreferenceSelection;
211
253
  // Get Figma project ID
212
254
  p.log.step(chalk_1.default.blue('Figma Configuration'));
213
255
  const figmaProjectId = yield p.text({
@@ -233,6 +275,8 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
233
275
  projectName: projectName,
234
276
  figmaProjectId: (figmaProjectId || '').trim(),
235
277
  figmaAccessToken: (figmaAccessToken || '').trim(),
278
+ languagePreference,
279
+ includeExamples,
236
280
  };
237
281
  // Create files
238
282
  const spinner = p.spinner();
@@ -243,8 +287,8 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
243
287
  }
244
288
  // Combine mandatory templates with example templates based on user choice
245
289
  const templates = [
246
- ...getMandatoryTemplates(includeExamples),
247
- ...(includeExamples ? exampleTemplates : []),
290
+ ...getMandatoryTemplates(includeExamples, languagePreference),
291
+ ...(includeExamples ? getExampleTemplates(languagePreference) : []),
248
292
  ];
249
293
  try {
250
294
  for (const template of templates) {
@@ -267,7 +311,7 @@ const create = () => __awaiter(void 0, void 0, void 0, function* () {
267
311
  throw err;
268
312
  }
269
313
  // Install dependencies
270
- yield runNpmInstall(projectPath);
314
+ yield installDependencies(projectPath, options);
271
315
  // Success message
272
316
  const nextSteps = [`cd ${projectName}`];
273
317
  if (figmaProjectId && figmaAccessToken) {
package/dist/cli/eject.js CHANGED
@@ -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) {
@@ -13,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
36
  };
14
37
  Object.defineProperty(exports, "__esModule", { value: true });
15
38
  exports.ejectTheme = exports.ejectPages = exports.ejectConfig = void 0;
39
+ const p = __importStar(require("@clack/prompts"));
16
40
  const fs_extra_1 = __importDefault(require("fs-extra"));
17
41
  const path_1 = __importDefault(require("path"));
18
42
  const logger_1 = require("../utils/logger");
@@ -21,14 +45,31 @@ const logger_1 = require("../utils/logger");
21
45
  * @param handoff
22
46
  */
23
47
  const ejectConfig = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
24
- const configPath = path_1.default.resolve(path_1.default.join(handoff.workingPath, 'handoff.config.js'));
25
- if (fs_extra_1.default.existsSync(configPath)) {
26
- if (!handoff.force) {
27
- logger_1.Logger.error(`Config file already exists. Use "--force" to overwrite.`);
28
- }
48
+ const existingConfigNames = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'];
49
+ const existingConfigPaths = existingConfigNames
50
+ .map((name) => path_1.default.resolve(path_1.default.join(handoff.workingPath, name)))
51
+ .filter((filePath) => fs_extra_1.default.existsSync(filePath));
52
+ if (existingConfigPaths.length > 0 && !handoff.force) {
53
+ logger_1.Logger.error(`Config file already exists. Use "--force" to overwrite.`);
54
+ return handoff;
55
+ }
56
+ const defaultFormat = fs_extra_1.default.existsSync(path_1.default.resolve(handoff.workingPath, 'tsconfig.json')) ? 'typescript' : 'javascript';
57
+ const formatSelection = yield p.select({
58
+ message: 'Which config format would you like to eject?',
59
+ options: [
60
+ { value: 'typescript', label: 'TypeScript', hint: 'Generates handoff.config.ts' },
61
+ { value: 'javascript', label: 'JavaScript', hint: 'Generates handoff.config.js' },
62
+ ],
63
+ initialValue: defaultFormat,
64
+ });
65
+ if (p.isCancel(formatSelection)) {
66
+ logger_1.Logger.warn('Config eject cancelled.');
67
+ return handoff;
29
68
  }
30
- // load the template as a string
31
- const template = fs_extra_1.default.readFileSync(path_1.default.resolve(handoff.modulePath, 'config/config.template.js'), 'utf8');
69
+ const useTypeScript = formatSelection === 'typescript';
70
+ const configPath = path_1.default.resolve(path_1.default.join(handoff.workingPath, useTypeScript ? 'handoff.config.ts' : 'handoff.config.js'));
71
+ const templatePath = path_1.default.resolve(handoff.modulePath, useTypeScript ? 'config/config.template.ts' : 'config/config.template.js');
72
+ const template = fs_extra_1.default.readFileSync(templatePath, 'utf8');
32
73
  fs_extra_1.default.writeFileSync(configPath, template);
33
74
  logger_1.Logger.success(`Config ejected to ${configPath}`);
34
75
  return handoff;