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
@@ -3,27 +3,140 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.toLowerCaseKeysAndValues = exports.getComponentsForPath = exports.initRuntimeConfig = void 0;
6
+ exports.toLowerCaseKeysAndValues = exports.getItemsForPath = exports.getComponentsForPath = exports.initRuntimeConfig = void 0;
7
+ const esbuild_1 = __importDefault(require("esbuild"));
7
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const module_1 = require("module");
8
10
  const path_1 = __importDefault(require("path"));
11
+ const csf_1 = require("../transformers/utils/csf");
12
+ const module_2 = require("../transformers/utils/module");
9
13
  const logger_1 = require("../utils/logger");
14
+ const path_2 = require("../utils/path");
15
+ const declaration_1 = require("./normalizers/declaration");
16
+ const pattern_1 = require("./normalizers/pattern");
17
+ const MODERN_EXTENSIONS = ['ts', 'js', 'cjs', 'json'];
18
+ const getLegacyDeclarationFiles = (componentBaseName) => [
19
+ `${componentBaseName}.json`,
20
+ `${componentBaseName}.js`,
21
+ `${componentBaseName}.cjs`,
22
+ ];
23
+ const getModernDeclarationFiles = (componentBaseName) => MODERN_EXTENSIONS.map((ext) => `${componentBaseName}.handoff.${ext}`);
24
+ const findPreferredModernDeclaration = (componentDir, componentBaseName) => {
25
+ const exactCandidates = getModernDeclarationFiles(componentBaseName);
26
+ const exactMatch = exactCandidates.find((candidate) => fs_extra_1.default.existsSync(path_1.default.resolve(componentDir, candidate)));
27
+ if (exactMatch)
28
+ return exactMatch;
29
+ const allFiles = fs_extra_1.default.existsSync(componentDir) ? fs_extra_1.default.readdirSync(componentDir) : [];
30
+ const modernFiles = allFiles
31
+ .filter((file) => /\.handoff\.(ts|js|cjs|json)$/.test(file))
32
+ .sort((a, b) => a.localeCompare(b));
33
+ if (!modernFiles.length)
34
+ return undefined;
35
+ for (const ext of MODERN_EXTENSIONS) {
36
+ const extMatch = modernFiles.find((file) => file.endsWith(`.handoff.${ext}`));
37
+ if (extMatch)
38
+ return extMatch;
39
+ }
40
+ return modernFiles[0];
41
+ };
42
+ const resolveComponentDeclaration = (componentDir, componentBaseName) => {
43
+ const modernMatch = findPreferredModernDeclaration(componentDir, componentBaseName);
44
+ const legacyFiles = getLegacyDeclarationFiles(componentBaseName);
45
+ const legacyMatch = legacyFiles.find((candidate) => fs_extra_1.default.existsSync(path_1.default.resolve(componentDir, candidate)));
46
+ if (modernMatch) {
47
+ if (legacyMatch) {
48
+ logger_1.Logger.warn(`Both modern and legacy declarations found in "${componentDir}". Using "${modernMatch}" and ignoring "${legacyMatch}".`);
49
+ }
50
+ return { fileName: modernMatch };
51
+ }
52
+ if (legacyMatch) {
53
+ return { fileName: legacyMatch };
54
+ }
55
+ return null;
56
+ };
57
+ const evaluateTypeScriptDeclaration = (filePath, handoffModulePath) => {
58
+ var _a, _b;
59
+ const buildResult = esbuild_1.default.buildSync({
60
+ entryPoints: [filePath],
61
+ bundle: true,
62
+ write: false,
63
+ platform: 'node',
64
+ format: 'cjs',
65
+ target: 'node16',
66
+ logLevel: 'silent',
67
+ jsx: 'automatic',
68
+ external: ['react', 'react-dom', 'handoff-app'],
69
+ });
70
+ const code = (_b = (_a = buildResult.outputFiles) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text;
71
+ if (!code) {
72
+ throw new Error(`Unable to compile declaration file "${filePath}"`);
73
+ }
74
+ const mod = { exports: {} };
75
+ const localRequire = (0, module_1.createRequire)(filePath);
76
+ const handoffRequire = (0, module_1.createRequire)(path_1.default.resolve(handoffModulePath, 'package.json'));
77
+ const runtimeRequire = (id) => {
78
+ try {
79
+ return localRequire(id);
80
+ }
81
+ catch (_a) {
82
+ return handoffRequire(id);
83
+ }
84
+ };
85
+ const evaluator = new Function('require', 'module', 'exports', '__filename', '__dirname', code);
86
+ evaluator(runtimeRequire, mod, mod.exports, filePath, path_1.default.dirname(filePath));
87
+ return mod.exports;
88
+ };
89
+ const loadDeclarationFile = (filePath, handoffModulePath) => {
90
+ if (filePath.endsWith('.json')) {
91
+ const componentJson = fs_extra_1.default.readFileSync(filePath, 'utf8');
92
+ return JSON.parse(componentJson);
93
+ }
94
+ if (filePath.endsWith('.ts')) {
95
+ return evaluateTypeScriptDeclaration(filePath, handoffModulePath);
96
+ }
97
+ // Invalidate require cache to ensure fresh read
98
+ delete require.cache[require.resolve(filePath)];
99
+ return require(filePath);
100
+ };
101
+ const discoverCsfPreviews = (component, handoff) => {
102
+ var _a;
103
+ if (component.renderer !== 'csf' || !((_a = component.entries) === null || _a === void 0 ? void 0 : _a.template)) {
104
+ return component.previews;
105
+ }
106
+ try {
107
+ const loaded = (0, module_2.buildAndEvaluateModuleSync)(component.entries.template, handoff);
108
+ const discoveredPreviews = (0, csf_1.createCsfStoryPreviews)(loaded.exports);
109
+ if (Object.keys(discoveredPreviews).length === 0) {
110
+ logger_1.Logger.warn(`No named stories found in CSF file for "${component.id}": ${component.entries.template}`);
111
+ return component.previews;
112
+ }
113
+ return discoveredPreviews;
114
+ }
115
+ catch (error) {
116
+ logger_1.Logger.warn(`Failed to discover CSF stories for "${component.id}" from "${component.entries.template}". Falling back to declared previews.`);
117
+ logger_1.Logger.debug('CSF discovery error', error);
118
+ return component.previews;
119
+ }
120
+ };
10
121
  /**
11
122
  * Initializes the runtime configuration by resolving component entries,
12
123
  * SCSS/JS paths, and transformer options from the handoff config.
13
124
  *
14
125
  * @param handoff - Object with config and workingPath.
15
- * @returns A tuple of [RuntimeConfig, configFilePaths].
126
+ * @returns A tuple of [RuntimeConfig, configFilePaths, configFileIndex].
16
127
  */
17
128
  const initRuntimeConfig = (handoff) => {
18
- var _a, _b, _c, _d, _e, _f, _g, _h;
19
- var _j;
129
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
130
+ var _l;
20
131
  const configFiles = [];
132
+ const configFileIndex = new Map();
21
133
  const result = {
22
134
  options: {},
23
135
  entries: {
24
136
  scss: undefined,
25
137
  js: undefined,
26
138
  components: {},
139
+ patterns: {},
27
140
  },
28
141
  };
29
142
  if (!!((_a = handoff.config.entries) === null || _a === void 0 ? void 0 : _a.scss)) {
@@ -37,46 +150,34 @@ const initRuntimeConfig = (handoff) => {
37
150
  for (const componentPath of componentPaths) {
38
151
  const resolvedComponentPath = path_1.default.resolve(handoff.workingPath, componentPath);
39
152
  const componentBaseName = path_1.default.basename(resolvedComponentPath);
40
- const possibleConfigFiles = [`${componentBaseName}.json`, `${componentBaseName}.js`, `${componentBaseName}.cjs`];
41
- const configFileName = possibleConfigFiles.find((file) => fs_extra_1.default.existsSync(path_1.default.resolve(resolvedComponentPath, file)));
42
- if (!configFileName) {
43
- logger_1.Logger.warn(`Missing config: ${path_1.default.resolve(resolvedComponentPath, possibleConfigFiles.join(' or '))}`);
153
+ const declaration = resolveComponentDeclaration(resolvedComponentPath, componentBaseName);
154
+ if (!declaration) {
155
+ const modernFiles = getModernDeclarationFiles(componentBaseName);
156
+ const legacyFiles = getLegacyDeclarationFiles(componentBaseName);
157
+ logger_1.Logger.warn(`Missing config: ${path_1.default.resolve(resolvedComponentPath, [...modernFiles, ...legacyFiles].join(' or '))}`);
44
158
  continue;
45
159
  }
46
- const resolvedComponentConfigPath = path_1.default.resolve(resolvedComponentPath, configFileName);
160
+ const resolvedComponentConfigPath = path_1.default.resolve(resolvedComponentPath, declaration.fileName);
47
161
  configFiles.push(resolvedComponentConfigPath);
48
162
  let component;
49
163
  try {
50
- if (configFileName.endsWith('.json')) {
51
- const componentJson = fs_extra_1.default.readFileSync(resolvedComponentConfigPath, 'utf8');
52
- component = JSON.parse(componentJson);
53
- }
54
- else {
55
- // Invalidate require cache to ensure fresh read
56
- delete require.cache[require.resolve(resolvedComponentConfigPath)];
57
- const importedComponent = require(resolvedComponentConfigPath);
58
- component = importedComponent.default || importedComponent;
59
- }
164
+ const importedDeclaration = loadDeclarationFile(resolvedComponentConfigPath, handoff.modulePath);
165
+ const rawComponent = importedDeclaration.default || importedDeclaration;
166
+ component = (0, declaration_1.normalizeComponentDeclaration)(rawComponent, {
167
+ declarationPath: resolvedComponentConfigPath,
168
+ fallbackId: componentBaseName,
169
+ warn: (message) => logger_1.Logger.warn(message),
170
+ });
60
171
  }
61
172
  catch (err) {
62
173
  logger_1.Logger.error(`Failed to read or parse config: ${resolvedComponentConfigPath}`, err);
63
174
  continue;
64
175
  }
65
- // Use component basename as the id
66
- component.id = componentBaseName;
67
- // Resolve entry paths relative to component directory
68
- if (component.entries) {
69
- for (const entryType in component.entries) {
70
- if (component.entries[entryType]) {
71
- component.entries[entryType] = path_1.default.resolve(resolvedComponentPath, component.entries[entryType]);
72
- }
73
- }
74
- }
75
176
  // Initialize options with safe defaults
76
177
  component.options || (component.options = {
77
178
  transformer: { defaults: {}, replace: {} },
78
179
  });
79
- (_j = component.options).transformer || (_j.transformer = { defaults: {}, replace: {} });
180
+ (_l = component.options).transformer || (_l.transformer = { defaults: {}, replace: {} });
80
181
  const transformer = component.options.transformer;
81
182
  (_g = transformer.cssRootClass) !== null && _g !== void 0 ? _g : (transformer.cssRootClass = null);
82
183
  (_h = transformer.tokenNameSegments) !== null && _h !== void 0 ? _h : (transformer.tokenNameSegments = null);
@@ -85,11 +186,52 @@ const initRuntimeConfig = (handoff) => {
85
186
  transformer.replace = (0, exports.toLowerCaseKeysAndValues)(Object.assign({}, transformer.replace));
86
187
  // Save transformer config
87
188
  result.options[component.id] = transformer;
189
+ // Discover CSF stories early so patterns can resolve stories like normal previews.
190
+ component.previews = discoverCsfPreviews(component, handoff);
88
191
  // Save full component entry
89
192
  result.entries.components[component.id] = component;
193
+ configFileIndex.set((0, path_2.normalizePathForCompare)(resolvedComponentConfigPath), { kind: 'component', entityId: component.id });
90
194
  }
91
195
  }
92
- return [result, Array.from(configFiles)];
196
+ // -------------------------------------------------------------------------
197
+ // Load pattern declarations
198
+ // -------------------------------------------------------------------------
199
+ if ((_k = (_j = handoff.config.entries) === null || _j === void 0 ? void 0 : _j.patterns) === null || _k === void 0 ? void 0 : _k.length) {
200
+ const patternPaths = handoff.config.entries.patterns.flatMap(exports.getItemsForPath);
201
+ for (const patternPath of patternPaths) {
202
+ const resolvedPatternPath = path_1.default.resolve(handoff.workingPath, patternPath);
203
+ const patternBaseName = path_1.default.basename(resolvedPatternPath);
204
+ const declaration = resolveComponentDeclaration(resolvedPatternPath, patternBaseName);
205
+ if (!declaration) {
206
+ const modernFiles = getModernDeclarationFiles(patternBaseName);
207
+ const legacyFiles = getLegacyDeclarationFiles(patternBaseName);
208
+ logger_1.Logger.warn(`Missing pattern config: ${path_1.default.resolve(resolvedPatternPath, [...modernFiles, ...legacyFiles].join(' or '))}`);
209
+ continue;
210
+ }
211
+ const resolvedPatternConfigPath = path_1.default.resolve(resolvedPatternPath, declaration.fileName);
212
+ configFiles.push(resolvedPatternConfigPath);
213
+ let pattern;
214
+ try {
215
+ const importedDeclaration = loadDeclarationFile(resolvedPatternConfigPath, handoff.modulePath);
216
+ const rawPattern = importedDeclaration.default || importedDeclaration;
217
+ pattern = (0, pattern_1.normalizePatternDeclaration)(rawPattern, {
218
+ declarationPath: resolvedPatternConfigPath,
219
+ fallbackId: patternBaseName,
220
+ });
221
+ }
222
+ catch (err) {
223
+ logger_1.Logger.error(`Failed to read or parse pattern config: ${resolvedPatternConfigPath}`, err);
224
+ continue;
225
+ }
226
+ configFileIndex.set((0, path_2.normalizePathForCompare)(resolvedPatternConfigPath), { kind: 'pattern', entityId: pattern.id });
227
+ result.entries.patterns[pattern.id] = pattern;
228
+ }
229
+ }
230
+ // -------------------------------------------------------------------------
231
+ // Inject synthetic previews from patterns onto components
232
+ // -------------------------------------------------------------------------
233
+ injectPatternPreviews(result);
234
+ return [result, Array.from(configFiles), configFileIndex];
93
235
  };
94
236
  exports.initRuntimeConfig = initRuntimeConfig;
95
237
  /**
@@ -107,9 +249,8 @@ exports.initRuntimeConfig = initRuntimeConfig;
107
249
  */
108
250
  const getComponentsForPath = (searchPath) => {
109
251
  const dirName = path_1.default.basename(searchPath);
110
- const possibleConfigFiles = [`${dirName}.json`, `${dirName}.js`, `${dirName}.cjs`];
252
+ const hasOwnConfig = !!resolveComponentDeclaration(searchPath, dirName);
111
253
  // Check if searchPath itself is a component directory (has a config file named after the directory)
112
- const hasOwnConfig = possibleConfigFiles.some((file) => fs_extra_1.default.existsSync(path_1.default.resolve(searchPath, file)));
113
254
  if (hasOwnConfig) {
114
255
  // This directory is a single component
115
256
  return [searchPath];
@@ -128,6 +269,92 @@ const getComponentsForPath = (searchPath) => {
128
269
  return [searchPath];
129
270
  };
130
271
  exports.getComponentsForPath = getComponentsForPath;
272
+ /**
273
+ * Generic directory discovery for both components and patterns.
274
+ * Reuses the same logic as getComponentsForPath.
275
+ */
276
+ const getItemsForPath = (searchPath) => {
277
+ return (0, exports.getComponentsForPath)(searchPath);
278
+ };
279
+ exports.getItemsForPath = getItemsForPath;
280
+ /**
281
+ * After both components and patterns are loaded, this function iterates all
282
+ * pattern component refs and auto-registers synthetic preview entries on the
283
+ * referenced components whenever custom args are specified.
284
+ *
285
+ * This ensures the component build renders every preview needed by patterns
286
+ * so that pattern composition is purely file I/O (no rendering).
287
+ */
288
+ const injectPatternPreviews = (result) => {
289
+ var _a, _b, _c, _d, _e, _f;
290
+ const patterns = (_b = (_a = result.entries) === null || _a === void 0 ? void 0 : _a.patterns) !== null && _b !== void 0 ? _b : {};
291
+ const components = (_d = (_c = result.entries) === null || _c === void 0 ? void 0 : _c.components) !== null && _d !== void 0 ? _d : {};
292
+ for (const [patternId, pattern] of Object.entries(patterns)) {
293
+ for (let i = 0; i < pattern.components.length; i++) {
294
+ const ref = pattern.components[i];
295
+ const component = components[ref.id];
296
+ if (!component) {
297
+ const error = `Pattern "${patternId}" references component "${ref.id}" which is not declared. This fragment will be skipped.`;
298
+ logger_1.Logger.warn(error);
299
+ ref.resolved = false;
300
+ continue;
301
+ }
302
+ // Case 1: only preview, no args -> use the existing preview as-is
303
+ if (ref.preview && !ref.args) {
304
+ if ((_e = component.previews) === null || _e === void 0 ? void 0 : _e[ref.preview]) {
305
+ ref.resolved = true;
306
+ }
307
+ else {
308
+ const error = `Pattern "${patternId}" references preview "${ref.preview}" on component "${ref.id}" which does not exist. This fragment may be skipped.`;
309
+ logger_1.Logger.warn(error);
310
+ ref.resolved = false;
311
+ }
312
+ ref.resolvedPreview = ref.preview;
313
+ continue;
314
+ }
315
+ // Case 2: no preview AND no args -> use the component's first existing preview (never assume "default")
316
+ if (!ref.preview && !ref.args) {
317
+ const previewKeys = Object.keys(component.previews || {});
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
+ }
327
+ continue;
328
+ }
329
+ // Case 3: args present (with or without preview base) -> create synthetic preview
330
+ let resolvedValues = {};
331
+ if (ref.preview) {
332
+ const basePreview = (_f = component.previews) === null || _f === void 0 ? void 0 : _f[ref.preview];
333
+ if (basePreview) {
334
+ resolvedValues = Object.assign({}, basePreview.values);
335
+ }
336
+ else {
337
+ const error = `Pattern "${patternId}" references preview "${ref.preview}" on component "${ref.id}" which does not exist. Using args only.`;
338
+ logger_1.Logger.warn(error);
339
+ ref.resolved = false;
340
+ }
341
+ }
342
+ resolvedValues = Object.assign(Object.assign({}, resolvedValues), ref.args);
343
+ const syntheticKey = `__pattern_${patternId}_${i}`;
344
+ component.internalPatternPreviews = component.internalPatternPreviews || {};
345
+ component.internalPatternPreviews[syntheticKey] = {
346
+ title: syntheticKey,
347
+ values: resolvedValues,
348
+ url: '',
349
+ sourcePreview: ref.preview,
350
+ };
351
+ ref.resolvedPreview = syntheticKey;
352
+ if (ref.resolved === undefined) {
353
+ ref.resolved = true;
354
+ }
355
+ }
356
+ }
357
+ };
131
358
  /**
132
359
  * Recursively converts all keys and string values in an object to lowercase.
133
360
  */
@@ -0,0 +1,6 @@
1
+ import { CsfDeclarationConfig, GenericDeclarationConfig, GenericPatternDeclarationConfig, HandlebarsDeclarationConfig, ReactComponentType, ReactDeclarationConfig } from './types';
2
+ export declare const defineReactComponent: <TProps>(_component: ReactComponentType<TProps>, config: ReactDeclarationConfig<TProps>) => GenericDeclarationConfig;
3
+ export declare const defineHandlebarsComponent: (config: HandlebarsDeclarationConfig) => GenericDeclarationConfig;
4
+ export declare const defineCsfComponent: (config: CsfDeclarationConfig) => GenericDeclarationConfig;
5
+ export declare const defineComponent: (config: GenericDeclarationConfig) => GenericDeclarationConfig;
6
+ export declare const definePattern: (config: GenericPatternDeclarationConfig) => GenericPatternDeclarationConfig;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.definePattern = exports.defineComponent = exports.defineCsfComponent = exports.defineHandlebarsComponent = exports.defineReactComponent = void 0;
4
+ const defineReactComponent = (_component, config) => {
5
+ return Object.assign(Object.assign({}, config), { renderer: 'react' });
6
+ };
7
+ exports.defineReactComponent = defineReactComponent;
8
+ const defineHandlebarsComponent = (config) => {
9
+ return Object.assign(Object.assign({}, config), { renderer: 'handlebars' });
10
+ };
11
+ exports.defineHandlebarsComponent = defineHandlebarsComponent;
12
+ const defineCsfComponent = (config) => {
13
+ return Object.assign(Object.assign({}, config), { renderer: 'csf' });
14
+ };
15
+ exports.defineCsfComponent = defineCsfComponent;
16
+ const defineComponent = (config) => config;
17
+ exports.defineComponent = defineComponent;
18
+ const definePattern = (config) => config;
19
+ exports.definePattern = definePattern;
@@ -0,0 +1,2 @@
1
+ export { defineComponent, defineCsfComponent, defineHandlebarsComponent, definePattern, defineReactComponent } from './helpers';
2
+ export type { CsfDeclarationConfig, DeclarationPreview, GenericDeclarationConfig, GenericPatternDeclarationConfig, HandlebarsDeclarationConfig, PatternComponentRef, ReactDeclarationConfig, RendererKind } from './types';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defineReactComponent = exports.definePattern = exports.defineHandlebarsComponent = exports.defineCsfComponent = exports.defineComponent = void 0;
4
+ var helpers_1 = require("./helpers");
5
+ Object.defineProperty(exports, "defineComponent", { enumerable: true, get: function () { return helpers_1.defineComponent; } });
6
+ Object.defineProperty(exports, "defineCsfComponent", { enumerable: true, get: function () { return helpers_1.defineCsfComponent; } });
7
+ Object.defineProperty(exports, "defineHandlebarsComponent", { enumerable: true, get: function () { return helpers_1.defineHandlebarsComponent; } });
8
+ Object.defineProperty(exports, "definePattern", { enumerable: true, get: function () { return helpers_1.definePattern; } });
9
+ Object.defineProperty(exports, "defineReactComponent", { enumerable: true, get: function () { return helpers_1.defineReactComponent; } });
@@ -0,0 +1,60 @@
1
+ import type React from 'react';
2
+ import type { ComponentObject } from '../transformers/preview/types';
3
+ export type RendererKind = 'react' | 'handlebars' | 'csf';
4
+ export type DeclarationPreview<TArgs = Record<string, any>> = {
5
+ title: string;
6
+ args?: TArgs;
7
+ values?: TArgs;
8
+ url?: string;
9
+ usage?: string;
10
+ };
11
+ type BaseDeclarationEntries = NonNullable<ComponentObject['entries']> & {
12
+ component?: string;
13
+ story?: string;
14
+ templates?: string;
15
+ };
16
+ type OptionalComponentMetadata = Partial<Omit<ComponentObject, 'previews' | 'entries' | 'title' | 'should_do' | 'should_not_do'>>;
17
+ export type BaseDeclarationConfig = OptionalComponentMetadata & {
18
+ id?: string;
19
+ name: string;
20
+ renderer?: RendererKind;
21
+ entries?: BaseDeclarationEntries;
22
+ previews?: Record<string, DeclarationPreview>;
23
+ shouldDo?: string[];
24
+ shouldNotDo?: string[];
25
+ };
26
+ export type ReactDeclarationConfig<TProps> = Omit<BaseDeclarationConfig, 'renderer' | 'entries' | 'previews'> & {
27
+ entries: BaseDeclarationEntries & {
28
+ component: string;
29
+ };
30
+ previews: Record<string, DeclarationPreview<Partial<TProps>>>;
31
+ };
32
+ export type HandlebarsDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer' | 'entries'> & {
33
+ entries: BaseDeclarationEntries & {
34
+ template: string;
35
+ };
36
+ };
37
+ export type CsfDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer' | 'entries'> & {
38
+ entries: BaseDeclarationEntries & {
39
+ story: string;
40
+ };
41
+ };
42
+ export type GenericDeclarationConfig = Omit<BaseDeclarationConfig, 'renderer'> & {
43
+ renderer: RendererKind;
44
+ };
45
+ export type ReactComponentType<TProps = any> = React.ComponentType<TProps>;
46
+ export type PatternComponentRef = {
47
+ id: string;
48
+ preview?: string;
49
+ args?: Record<string, any>;
50
+ };
51
+ export type BasePatternDeclarationConfig = {
52
+ id?: string;
53
+ name: string;
54
+ description?: string;
55
+ group?: string;
56
+ tags?: string[];
57
+ components: PatternComponentRef[];
58
+ };
59
+ export type GenericPatternDeclarationConfig = BasePatternDeclarationConfig;
60
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { Types as CoreTypes, Handoff as HandoffRunner } from 'handoff-core';
3
- import { Config, RuntimeConfig } from './types/config';
3
+ import { Config, ConfigFileEntry, RuntimeConfig } from './types/config';
4
4
  declare class Handoff {
5
5
  config: Config | null;
6
6
  debug: boolean;
@@ -17,6 +17,8 @@ declare class Handoff {
17
17
  };
18
18
  private _initialArgs;
19
19
  private _configFilePaths;
20
+ private _configFileIndex;
21
+ private _mainConfigFilePath?;
20
22
  private _documentationObjectCache?;
21
23
  private _handoffRunner?;
22
24
  constructor(debug?: boolean, force?: boolean, config?: Partial<Config>);
@@ -26,6 +28,7 @@ declare class Handoff {
26
28
  preRunner(validate?: boolean): Handoff;
27
29
  fetch(): Promise<Handoff>;
28
30
  component(name: string | null): Promise<Handoff>;
31
+ pattern(): Promise<Handoff>;
29
32
  build(skipComponents?: boolean): Promise<Handoff>;
30
33
  ejectConfig(): Promise<Handoff>;
31
34
  ejectPages(): Promise<Handoff>;
@@ -82,6 +85,11 @@ declare class Handoff {
82
85
  * @returns {string[]} Array of absolute paths to config files
83
86
  */
84
87
  getConfigFilePaths(): string[];
88
+ /**
89
+ * Gets the selected main config file path if one exists.
90
+ */
91
+ getMainConfigFilePath(): string | undefined;
92
+ getConfigFileEntry(configPath: string): ConfigFileEntry | undefined;
85
93
  /**
86
94
  * Clears all cached data
87
95
  * @returns {void}
@@ -96,5 +104,8 @@ declare class Handoff {
96
104
  }
97
105
  export type { ComponentObject as Component } from './transformers/preview/types';
98
106
  export type { Config, RegisterHandlebarsHelpersContext } from './types/config';
107
+ export { defineConfig } from './config';
108
+ export { defineComponent, defineCsfComponent, defineHandlebarsComponent, definePattern, defineReactComponent, } from './declarations';
109
+ export type { CsfDeclarationConfig, DeclarationPreview, GenericDeclarationConfig, GenericPatternDeclarationConfig, HandlebarsDeclarationConfig, PatternComponentRef, ReactDeclarationConfig, RendererKind, } from './declarations';
99
110
  export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
100
111
  export default Handoff;
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ 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.CoreTypes = exports.CoreTransformerUtils = exports.CoreTransformers = void 0;
38
+ exports.CoreTypes = exports.CoreTransformerUtils = exports.CoreTransformers = exports.defineReactComponent = exports.definePattern = exports.defineHandlebarsComponent = exports.defineCsfComponent = exports.defineComponent = exports.defineConfig = void 0;
39
39
  require("dotenv/config");
40
40
  const fs_extra_1 = __importDefault(require("fs-extra"));
41
41
  const handoff_core_1 = require("handoff-core");
@@ -48,6 +48,7 @@ const pipeline_1 = __importStar(require("./pipeline"));
48
48
  const builder_1 = __importStar(require("./transformers/preview/component/builder"));
49
49
  const logger_1 = require("./utils/logger");
50
50
  const path_2 = require("./utils/path");
51
+ const path_3 = require("./utils/path");
51
52
  class Handoff {
52
53
  constructor(debug, force, config) {
53
54
  this.debug = false;
@@ -58,6 +59,7 @@ class Handoff {
58
59
  this.sitesDirectory = 'out';
59
60
  this._initialArgs = {};
60
61
  this._configFilePaths = [];
62
+ this._configFileIndex = new Map();
61
63
  this._initialArgs = { debug, force, config };
62
64
  this.construct(debug, force, config);
63
65
  }
@@ -71,11 +73,13 @@ class Handoff {
71
73
  }
72
74
  init(configOverride) {
73
75
  var _a, _b, _c;
74
- const config = (0, config_1.initConfig)(configOverride !== null && configOverride !== void 0 ? configOverride : {});
76
+ const configResult = (0, config_1.initConfigWithMetadata)(configOverride !== null && configOverride !== void 0 ? configOverride : {});
77
+ const config = configResult.config;
75
78
  this.config = config;
79
+ this._mainConfigFilePath = configResult.configPath;
76
80
  this.exportsDirectory = (_a = config.exportsOutputDirectory) !== null && _a !== void 0 ? _a : this.exportsDirectory;
77
81
  this.sitesDirectory = (_b = config.sitesOutputDirectory) !== null && _b !== void 0 ? _b : this.exportsDirectory;
78
- [this.runtimeConfig, this._configFilePaths] = (0, config_1.initRuntimeConfig)(this);
82
+ [this.runtimeConfig, this._configFilePaths, this._configFileIndex] = (0, config_1.initRuntimeConfig)(this);
79
83
  if (this.config.app.base_path && !process.env.HANDOFF_APP_BASE_PATH) {
80
84
  process.env.HANDOFF_APP_BASE_PATH = (_c = this.config.app.base_path) !== null && _c !== void 0 ? _c : '';
81
85
  }
@@ -114,6 +118,13 @@ class Handoff {
114
118
  return this;
115
119
  });
116
120
  }
121
+ pattern() {
122
+ return __awaiter(this, void 0, void 0, function* () {
123
+ this.preRunner();
124
+ yield (0, pipeline_1.buildPatterns)(this);
125
+ return this;
126
+ });
127
+ }
117
128
  build(skipComponents) {
118
129
  return __awaiter(this, void 0, void 0, function* () {
119
130
  this.preRunner();
@@ -242,7 +253,7 @@ class Handoff {
242
253
  return this.config.figma_project_id;
243
254
  }
244
255
  // Fallback to filesystem-safe transformation of working path
245
- return (0, path_2.generateFilesystemSafeId)(this.workingPath);
256
+ return (0, path_3.generateFilesystemSafeId)(this.workingPath);
246
257
  }
247
258
  /**
248
259
  * Gets the output path for the current project
@@ -291,7 +302,17 @@ class Handoff {
291
302
  * @returns {string[]} Array of absolute paths to config files
292
303
  */
293
304
  getConfigFilePaths() {
294
- return this._configFilePaths;
305
+ const combined = this._mainConfigFilePath ? [this._mainConfigFilePath, ...this._configFilePaths] : this._configFilePaths;
306
+ return Array.from(new Set(combined));
307
+ }
308
+ /**
309
+ * Gets the selected main config file path if one exists.
310
+ */
311
+ getMainConfigFilePath() {
312
+ return this._mainConfigFilePath;
313
+ }
314
+ getConfigFileEntry(configPath) {
315
+ return this._configFileIndex.get((0, path_2.normalizePathForCompare)(configPath));
295
316
  }
296
317
  /**
297
318
  * Clears all cached data
@@ -316,6 +337,14 @@ class Handoff {
316
337
  });
317
338
  }
318
339
  }
340
+ var config_2 = require("./config");
341
+ Object.defineProperty(exports, "defineConfig", { enumerable: true, get: function () { return config_2.defineConfig; } });
342
+ var declarations_1 = require("./declarations");
343
+ Object.defineProperty(exports, "defineComponent", { enumerable: true, get: function () { return declarations_1.defineComponent; } });
344
+ Object.defineProperty(exports, "defineCsfComponent", { enumerable: true, get: function () { return declarations_1.defineCsfComponent; } });
345
+ Object.defineProperty(exports, "defineHandlebarsComponent", { enumerable: true, get: function () { return declarations_1.defineHandlebarsComponent; } });
346
+ Object.defineProperty(exports, "definePattern", { enumerable: true, get: function () { return declarations_1.definePattern; } });
347
+ Object.defineProperty(exports, "defineReactComponent", { enumerable: true, get: function () { return declarations_1.defineReactComponent; } });
319
348
  // Export transformers and types from handoff-core
320
349
  var handoff_core_2 = require("handoff-core");
321
350
  Object.defineProperty(exports, "CoreTransformers", { enumerable: true, get: function () { return handoff_core_2.Transformers; } });
@@ -2,6 +2,7 @@ import 'dotenv/config';
2
2
  import Handoff from '..';
3
3
  export { readPrevJSONFile, zip, zipAssets } from './archive';
4
4
  export { buildComponents } from './components';
5
+ export { buildPatterns } from './patterns';
5
6
  /**
6
7
  * Run the entire Figma data pipeline:
7
8
  * 1. Validate runtime requirements
@@ -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.buildComponents = exports.zipAssets = exports.zip = exports.readPrevJSONFile = void 0;
15
+ exports.buildPatterns = exports.buildComponents = exports.zipAssets = exports.zip = exports.readPrevJSONFile = void 0;
16
16
  require("dotenv/config");
17
17
  const app_builder_1 = __importDefault(require("../app-builder"));
18
18
  const logger_1 = require("../utils/logger");
@@ -26,6 +26,8 @@ Object.defineProperty(exports, "zip", { enumerable: true, get: function () { ret
26
26
  Object.defineProperty(exports, "zipAssets", { enumerable: true, get: function () { return archive_1.zipAssets; } });
27
27
  var components_1 = require("./components");
28
28
  Object.defineProperty(exports, "buildComponents", { enumerable: true, get: function () { return components_1.buildComponents; } });
29
+ var patterns_1 = require("./patterns");
30
+ Object.defineProperty(exports, "buildPatterns", { enumerable: true, get: function () { return patterns_1.buildPatterns; } });
29
31
  /**
30
32
  * Run the entire Figma data pipeline:
31
33
  * 1. Validate runtime requirements
@@ -0,0 +1,8 @@
1
+ import Handoff from '..';
2
+ import { ProcessPatternsOptions } from '../transformers/preview/pattern/builder';
3
+ export type { ProcessPatternsOptions };
4
+ /**
5
+ * Builds pattern previews by composing pre-built component preview HTML.
6
+ * Must be called AFTER buildComponents.
7
+ */
8
+ export declare const buildPatterns: (handoff: Handoff, options?: ProcessPatternsOptions) => Promise<void>;