handoff-app 1.1.3 → 1.2.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/config.template.js +21 -14
- package/config/config.template.ts +54 -0
- package/config/docs/system.md +2 -0
- package/dist/app-builder/build.js +7 -2
- package/dist/app-builder/config-diff/index.d.ts +4 -0
- package/dist/app-builder/config-diff/index.js +12 -0
- package/dist/app-builder/config-diff/registry.d.ts +9 -0
- package/dist/app-builder/config-diff/registry.js +30 -0
- package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
- package/dist/app-builder/config-diff/snapshot.js +43 -0
- package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +93 -0
- package/dist/app-builder/config-diff/types.d.ts +19 -0
- package/dist/app-builder/config-diff/types.js +2 -0
- package/dist/app-builder/watchers/component.d.ts +15 -0
- package/dist/app-builder/watchers/component.js +69 -0
- package/dist/app-builder/watchers/utils.d.ts +14 -0
- package/dist/app-builder/watchers/utils.js +44 -0
- package/dist/app-builder/watchers.d.ts +10 -19
- package/dist/app-builder/watchers.js +56 -102
- package/dist/cache/build-cache.js +14 -5
- package/dist/cli/create.js +67 -23
- package/dist/cli/eject.js +48 -7
- package/dist/cli/make.js +29 -4
- package/dist/cli/scaffold.js +60 -19
- package/dist/cli.js +1 -1
- package/dist/config/helpers.d.ts +7 -0
- package/dist/config/helpers.js +31 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +5 -1
- package/dist/config/loader.d.ts +10 -1
- package/dist/config/loader.js +92 -28
- package/dist/config/normalizers/declaration.d.ts +11 -0
- package/dist/config/normalizers/declaration.js +119 -0
- package/dist/config/normalizers/pattern.d.ts +9 -0
- package/dist/config/normalizers/pattern.js +38 -0
- package/dist/config/runtime.d.ts +9 -3
- package/dist/config/runtime.js +254 -33
- package/dist/declarations/helpers.d.ts +6 -0
- package/dist/declarations/helpers.js +19 -0
- package/dist/declarations/index.d.ts +2 -0
- package/dist/declarations/index.js +9 -0
- package/dist/declarations/types.d.ts +60 -0
- package/dist/declarations/types.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +34 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline/patterns.d.ts +6 -0
- package/dist/pipeline/patterns.js +21 -0
- package/dist/transformers/docgen/index.js +12 -2
- package/dist/transformers/plugins/csf-render.js +170 -34
- package/dist/transformers/plugins/ssr-render.js +18 -2
- package/dist/transformers/preview/component/api.js +16 -7
- package/dist/transformers/preview/component/builder.js +9 -3
- package/dist/transformers/preview/component/css.d.ts +8 -0
- package/dist/transformers/preview/component/html.js +14 -5
- package/dist/transformers/preview/component/previews.d.ts +9 -0
- package/dist/transformers/preview/component/previews.js +10 -0
- package/dist/transformers/preview/pattern/api.d.ts +7 -0
- package/dist/transformers/preview/pattern/api.js +61 -0
- package/dist/transformers/preview/pattern/builder.d.ts +9 -0
- package/dist/transformers/preview/pattern/builder.js +86 -0
- package/dist/transformers/preview/pattern/html.d.ts +19 -0
- package/dist/transformers/preview/pattern/html.js +94 -0
- package/dist/transformers/preview/types.d.ts +54 -0
- package/dist/transformers/utils/csf.d.ts +16 -0
- package/dist/transformers/utils/csf.js +30 -0
- package/dist/transformers/utils/index.d.ts +1 -0
- package/dist/transformers/utils/index.js +2 -0
- package/dist/transformers/utils/module.d.ts +5 -0
- package/dist/transformers/utils/module.js +26 -13
- package/dist/transformers/utils/usage.d.ts +23 -0
- package/dist/transformers/utils/usage.js +294 -0
- package/dist/types/config.d.ts +32 -7
- package/dist/types/preview.d.ts +2 -0
- package/docs/cli.md +3 -3
- package/package.json +3 -1
- package/src/app/components/Component/PageSliceResolver.tsx +6 -6
- package/src/app/components/Component/Preview.tsx +17 -12
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
- package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
- package/src/app/components/util/index.ts +71 -2
- package/src/app/pages/system/pattern/[pattern]/index.tsx +223 -0
- package/src/app/pages/system/pattern/index.tsx +14 -0
- package/src/app-builder/README.md +37 -9
- package/src/app-builder/build.ts +8 -2
- package/src/app-builder/config-diff/index.ts +4 -0
- package/src/app-builder/config-diff/registry.ts +23 -0
- package/src/app-builder/config-diff/snapshot.ts +51 -0
- package/src/app-builder/config-diff/strategies/pattern.ts +65 -0
- package/src/app-builder/config-diff/types.ts +21 -0
- package/src/app-builder/watchers/component.ts +72 -0
- package/src/app-builder/watchers/utils.ts +44 -0
- package/src/app-builder/watchers.ts +62 -80
- package/src/cache/build-cache.ts +14 -5
- package/src/cli/create.ts +86 -25
- package/src/cli/eject.ts +29 -7
- package/src/cli/make.ts +32 -5
- package/src/cli/scaffold.ts +68 -21
- package/src/cli.ts +1 -1
- package/src/config/README.md +10 -1
- package/src/config/helpers.ts +43 -0
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +110 -31
- package/src/config/normalizers/declaration.ts +176 -0
- package/src/config/normalizers/pattern.ts +63 -0
- package/src/config/runtime.ts +310 -35
- package/src/declarations/helpers.ts +36 -0
- package/src/declarations/index.ts +19 -0
- package/src/declarations/types.ts +70 -0
- package/src/index.ts +47 -6
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/patterns.ts +10 -0
- package/src/transformers/README.md +2 -2
- package/src/transformers/docgen/index.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +216 -49
- package/src/transformers/plugins/ssr-render.ts +23 -7
- package/src/transformers/preview/component/api.ts +27 -7
- package/src/transformers/preview/component/builder.ts +21 -4
- package/src/transformers/preview/component/html.ts +18 -5
- package/src/transformers/preview/component/previews.ts +12 -0
- package/src/transformers/preview/pattern/api.ts +66 -0
- package/src/transformers/preview/pattern/builder.ts +94 -0
- package/src/transformers/preview/pattern/html.ts +107 -0
- package/src/transformers/preview/types.ts +58 -1
- package/src/transformers/utils/csf.ts +48 -0
- package/src/transformers/utils/index.ts +3 -0
- package/src/transformers/utils/module.ts +33 -18
- package/src/transformers/utils/usage.ts +319 -0
- package/src/types/README.md +1 -1
- package/src/types/config.ts +33 -7
- package/src/types/preview.ts +2 -0
- package/templates/default/components/button/Button.jsx.tpl +64 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
- package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
- package/templates/default/handoff.config.js.tpl +29 -16
- package/templates/default/handoff.config.ts.tpl +196 -0
- package/templates/default/handoff.config.with-examples.js.tpl +28 -17
- package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
- package/templates/default/package.json.tpl +1 -3
- package/handoff.config.js +0 -90
- package/templates/default/components/button/button.js.tpl +0 -62
package/src/cli/eject.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
1
2
|
import fs from 'fs-extra';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import Handoff from '../index';
|
|
@@ -8,14 +9,35 @@ import { Logger } from '../utils/logger';
|
|
|
8
9
|
* @param handoff
|
|
9
10
|
*/
|
|
10
11
|
export const ejectConfig = async (handoff: Handoff) => {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const existingConfigNames = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'];
|
|
13
|
+
const existingConfigPaths = existingConfigNames
|
|
14
|
+
.map((name) => path.resolve(path.join(handoff.workingPath, name)))
|
|
15
|
+
.filter((filePath) => fs.existsSync(filePath));
|
|
16
|
+
if (existingConfigPaths.length > 0 && !handoff.force) {
|
|
17
|
+
Logger.error(`Config file already exists. Use "--force" to overwrite.`);
|
|
18
|
+
return handoff;
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
const
|
|
20
|
+
|
|
21
|
+
const defaultFormat = fs.existsSync(path.resolve(handoff.workingPath, 'tsconfig.json')) ? 'typescript' : 'javascript';
|
|
22
|
+
const formatSelection = await p.select({
|
|
23
|
+
message: 'Which config format would you like to eject?',
|
|
24
|
+
options: [
|
|
25
|
+
{ value: 'typescript', label: 'TypeScript', hint: 'Generates handoff.config.ts' },
|
|
26
|
+
{ value: 'javascript', label: 'JavaScript', hint: 'Generates handoff.config.js' },
|
|
27
|
+
],
|
|
28
|
+
initialValue: defaultFormat,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (p.isCancel(formatSelection)) {
|
|
32
|
+
Logger.warn('Config eject cancelled.');
|
|
33
|
+
return handoff;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const useTypeScript = formatSelection === 'typescript';
|
|
37
|
+
const configPath = path.resolve(path.join(handoff.workingPath, useTypeScript ? 'handoff.config.ts' : 'handoff.config.js'));
|
|
38
|
+
const templatePath = path.resolve(handoff.modulePath, useTypeScript ? 'config/config.template.ts' : 'config/config.template.js');
|
|
39
|
+
|
|
40
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
19
41
|
fs.writeFileSync(configPath, template);
|
|
20
42
|
Logger.success(`Config ejected to ${configPath}`);
|
|
21
43
|
return handoff;
|
package/src/cli/make.ts
CHANGED
|
@@ -123,7 +123,7 @@ export const makeComponent = async (handoff: Handoff, name: string) => {
|
|
|
123
123
|
} else {
|
|
124
124
|
componentsRoot = path.resolve(handoff.workingPath, DEFAULT_COMPONENTS_DIR);
|
|
125
125
|
Logger.warn(
|
|
126
|
-
`No entries.components configured in handoff.config
|
|
126
|
+
`No entries.components configured in handoff.config.*. ` +
|
|
127
127
|
`Scaffolding into "${DEFAULT_COMPONENTS_DIR}/". ` +
|
|
128
128
|
`Add this path to entries.components in your config so the build picks it up.`
|
|
129
129
|
);
|
|
@@ -146,10 +146,6 @@ export const makeComponent = async (handoff: Handoff, name: string) => {
|
|
|
146
146
|
fs.writeFileSync(targetHtml, htmlTemplate);
|
|
147
147
|
Logger.success(`New component ${name}.hbs was created in ${workingPath}`);
|
|
148
148
|
|
|
149
|
-
const jsonpath = path.resolve(templatePath, 'template.json');
|
|
150
|
-
const jsonTemplate = fs.readFileSync(jsonpath, 'utf8');
|
|
151
|
-
fs.writeFileSync(path.resolve(workingPath, `${name}.json`), jsonTemplate);
|
|
152
|
-
|
|
153
149
|
const writeJSFile = await p.confirm({
|
|
154
150
|
message: `Generate a supporting javascript file ${name}.js?`,
|
|
155
151
|
initialValue: false,
|
|
@@ -172,5 +168,36 @@ export const makeComponent = async (handoff: Handoff, name: string) => {
|
|
|
172
168
|
fs.writeFileSync(path.resolve(workingPath, `${name}.scss`), scssTemplate);
|
|
173
169
|
}
|
|
174
170
|
|
|
171
|
+
const declarationEntries = [`template: './${name}.hbs'`];
|
|
172
|
+
if (writeJSFile === true) {
|
|
173
|
+
declarationEntries.push(`js: './${name}.js'`);
|
|
174
|
+
}
|
|
175
|
+
if (writeSassFile === true) {
|
|
176
|
+
declarationEntries.push(`scss: './${name}.scss'`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const declarationContent = `const { defineHandlebarsComponent } = require('handoff-app');
|
|
180
|
+
|
|
181
|
+
module.exports = defineHandlebarsComponent({
|
|
182
|
+
id: '${name}',
|
|
183
|
+
name: '',
|
|
184
|
+
description: '',
|
|
185
|
+
group: '',
|
|
186
|
+
type: 'element',
|
|
187
|
+
entries: {
|
|
188
|
+
${declarationEntries.join(',\n ')}
|
|
189
|
+
},
|
|
190
|
+
previews: {
|
|
191
|
+
default: {
|
|
192
|
+
title: 'Default',
|
|
193
|
+
args: {}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
fs.writeFileSync(path.resolve(workingPath, `${name}.handoff.js`), declarationContent);
|
|
200
|
+
Logger.success(`New component declaration ${name}.handoff.js was created in ${workingPath}`);
|
|
201
|
+
|
|
175
202
|
return handoff;
|
|
176
203
|
};
|
package/src/cli/scaffold.ts
CHANGED
|
@@ -5,9 +5,8 @@ import path from 'path';
|
|
|
5
5
|
import Handoff from '../index';
|
|
6
6
|
|
|
7
7
|
// Constants
|
|
8
|
-
const CONFIG_FILES = ['handoff.config.
|
|
8
|
+
const CONFIG_FILES = ['handoff.config.ts', 'handoff.config.js', 'handoff.config.cjs', 'handoff.config.json'] as const;
|
|
9
9
|
const COMPONENTS_DIR = 'components';
|
|
10
|
-
const DEFAULT_VERSION = '1.0.0';
|
|
11
10
|
const DEFAULT_GROUPS = [
|
|
12
11
|
{ value: 'Atomic Elements', label: 'Atomic Elements' },
|
|
13
12
|
{ value: 'Forms', label: 'Forms' },
|
|
@@ -47,27 +46,50 @@ const toTitleCase = (str: string): string => {
|
|
|
47
46
|
* Generate the component JS stub content
|
|
48
47
|
*/
|
|
49
48
|
const generateComponentStub = (config: ComponentConfig): string => {
|
|
50
|
-
// Build entries object with relative paths to generated files
|
|
51
49
|
const entriesLines: string[] = [];
|
|
52
50
|
if (config.generateTsx) {
|
|
53
|
-
entriesLines.push(`
|
|
51
|
+
entriesLines.push(` component: './${config.name}.tsx',`);
|
|
52
|
+
} else {
|
|
53
|
+
entriesLines.push(` template: './${config.name}.hbs',`);
|
|
54
54
|
}
|
|
55
55
|
if (config.generateScss) {
|
|
56
56
|
entriesLines.push(` scss: './${config.name}.scss',`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
: '';
|
|
62
|
-
|
|
63
|
-
return `/** @type {import('handoff-app').Component} */
|
|
64
|
-
module.exports = {
|
|
65
|
-
title: "${config.title}",
|
|
59
|
+
const commonMetadata = ` id: "${config.name}",
|
|
60
|
+
name: "${config.title}",
|
|
66
61
|
description: "${config.description}",
|
|
67
62
|
group: "${config.group}",
|
|
68
63
|
type: "element",
|
|
69
|
-
figmaComponentId: "${config.name}"
|
|
70
|
-
|
|
64
|
+
figmaComponentId: "${config.name}",`;
|
|
65
|
+
|
|
66
|
+
if (config.generateTsx) {
|
|
67
|
+
return `const { defineReactComponent } = require('handoff-app');
|
|
68
|
+
const ${toTitleCase(config.name).replace(/\s/g, '')} = require('./${config.name}').default;
|
|
69
|
+
|
|
70
|
+
module.exports = defineReactComponent(${toTitleCase(config.name).replace(/\s/g, '')}, {
|
|
71
|
+
${commonMetadata}
|
|
72
|
+
entries: {
|
|
73
|
+
${entriesLines.join('\n')}
|
|
74
|
+
},
|
|
75
|
+
previews: {
|
|
76
|
+
default: { title: "Default", args: {} }
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `const { defineHandlebarsComponent } = require('handoff-app');
|
|
83
|
+
|
|
84
|
+
module.exports = defineHandlebarsComponent({
|
|
85
|
+
${commonMetadata}
|
|
86
|
+
entries: {
|
|
87
|
+
${entriesLines.join('\n')}
|
|
88
|
+
},
|
|
89
|
+
previews: {
|
|
90
|
+
default: { title: "Default", args: {} }
|
|
91
|
+
}
|
|
92
|
+
});
|
|
71
93
|
`;
|
|
72
94
|
};
|
|
73
95
|
|
|
@@ -252,8 +274,8 @@ const updateConfigFile = async (
|
|
|
252
274
|
return { success: true, isJsConfig: false, configPath, skipped: false };
|
|
253
275
|
}
|
|
254
276
|
|
|
255
|
-
// For
|
|
256
|
-
if (configFile.endsWith('.js') || configFile.endsWith('.cjs')) {
|
|
277
|
+
// For code config files, try to update them programmatically
|
|
278
|
+
if (configFile.endsWith('.ts') || configFile.endsWith('.js') || configFile.endsWith('.cjs')) {
|
|
257
279
|
try {
|
|
258
280
|
let content = await fs.readFile(configPath, 'utf8');
|
|
259
281
|
|
|
@@ -309,7 +331,7 @@ const updateConfigFile = async (
|
|
|
309
331
|
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
310
332
|
}
|
|
311
333
|
|
|
312
|
-
// No entries object, try to add it before the closing of
|
|
334
|
+
// No entries object, try to add it before the closing of exported object
|
|
313
335
|
const formattedArray = formatComponentsArray(newComponentPaths);
|
|
314
336
|
const entriesBlock = ` entries: {\n components: ${formattedArray},\n },\n`;
|
|
315
337
|
|
|
@@ -322,6 +344,26 @@ const updateConfigFile = async (
|
|
|
322
344
|
await fs.writeFile(configPath, content, 'utf8');
|
|
323
345
|
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
324
346
|
}
|
|
347
|
+
|
|
348
|
+
// Try to insert after export default {
|
|
349
|
+
if (/export\s+default\s+\{/.test(content)) {
|
|
350
|
+
content = content.replace(
|
|
351
|
+
/(export\s+default\s+\{)/,
|
|
352
|
+
`$1\n${entriesBlock}`
|
|
353
|
+
);
|
|
354
|
+
await fs.writeFile(configPath, content, 'utf8');
|
|
355
|
+
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Try to insert into defineConfig({ ... })
|
|
359
|
+
if (/defineConfig\s*\(\s*\{/.test(content)) {
|
|
360
|
+
content = content.replace(
|
|
361
|
+
/(defineConfig\s*\(\s*\{)/,
|
|
362
|
+
`$1\n${entriesBlock}`
|
|
363
|
+
);
|
|
364
|
+
await fs.writeFile(configPath, content, 'utf8');
|
|
365
|
+
return { success: true, isJsConfig: true, configPath, skipped: false };
|
|
366
|
+
}
|
|
325
367
|
|
|
326
368
|
// Fallback: couldn't parse the JS config
|
|
327
369
|
return { success: false, isJsConfig: true, configPath, skipped: false };
|
|
@@ -344,15 +386,14 @@ const createComponentFiles = async (
|
|
|
344
386
|
const componentDir = path.resolve(
|
|
345
387
|
handoff.workingPath,
|
|
346
388
|
COMPONENTS_DIR,
|
|
347
|
-
config.name
|
|
348
|
-
DEFAULT_VERSION
|
|
389
|
+
config.name
|
|
349
390
|
);
|
|
350
391
|
|
|
351
392
|
// Ensure directory exists
|
|
352
393
|
await fs.ensureDir(componentDir);
|
|
353
394
|
|
|
354
|
-
// Create the main component
|
|
355
|
-
const jsPath = path.join(componentDir, `${config.name}.js`);
|
|
395
|
+
// Create the main component declaration file
|
|
396
|
+
const jsPath = path.join(componentDir, `${config.name}.handoff.js`);
|
|
356
397
|
await fs.writeFile(jsPath, generateComponentStub(config));
|
|
357
398
|
createdFiles.push(jsPath);
|
|
358
399
|
|
|
@@ -361,6 +402,12 @@ const createComponentFiles = async (
|
|
|
361
402
|
const tsxPath = path.join(componentDir, `${config.name}.tsx`);
|
|
362
403
|
await fs.writeFile(tsxPath, generateReactComponentStub(config.name, config.variantProps));
|
|
363
404
|
createdFiles.push(tsxPath);
|
|
405
|
+
} else {
|
|
406
|
+
const hbsTemplatePath = path.resolve(handoff.modulePath, 'config/templates/component/template.hbs');
|
|
407
|
+
const hbsTemplate = await fs.readFile(hbsTemplatePath, 'utf8');
|
|
408
|
+
const hbsPath = path.join(componentDir, `${config.name}.hbs`);
|
|
409
|
+
await fs.writeFile(hbsPath, hbsTemplate);
|
|
410
|
+
createdFiles.push(hbsPath);
|
|
364
411
|
}
|
|
365
412
|
|
|
366
413
|
// Optionally create SCSS file
|
|
@@ -380,7 +427,7 @@ const displayComponentPreview = (config: ComponentConfig): void => {
|
|
|
380
427
|
const stub = generateComponentStub(config);
|
|
381
428
|
const lines = stub.split('\n');
|
|
382
429
|
const preview = lines.slice(0, 12).join('\n') + '\n ...\n};';
|
|
383
|
-
p.note(preview, `Preview: ${config.name}.js`);
|
|
430
|
+
p.note(preview, `Preview: ${config.name}.handoff.js`);
|
|
384
431
|
};
|
|
385
432
|
|
|
386
433
|
/**
|
package/src/cli.ts
CHANGED
package/src/config/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Handles loading, validation, and resolution of handoff configuration.
|
|
|
7
7
|
| File | Purpose |
|
|
8
8
|
|------|---------|
|
|
9
9
|
| `defaults.ts` | Default config values and `getClientConfig()` for the Next.js app |
|
|
10
|
-
| `loader.ts` | `initConfig()` — reads `handoff.config.
|
|
10
|
+
| `loader.ts` | `initConfig()` — reads `handoff.config.ts/.js/.cjs/.json` (in that order), warns on conflicts, and merges overrides |
|
|
11
11
|
| `runtime.ts` | `initRuntimeConfig()` — resolves component entries, SCSS/JS paths, transformer options |
|
|
12
12
|
| `validator.ts` | `validateConfig()` — ensures required Figma credentials are present |
|
|
13
13
|
| `index.ts` | Barrel re-exports |
|
|
@@ -21,3 +21,12 @@ const config = initConfig(overrides);
|
|
|
21
21
|
const [runtimeConfig, configPaths] = initRuntimeConfig({ config, workingPath });
|
|
22
22
|
const validated = validateConfig(config);
|
|
23
23
|
```
|
|
24
|
+
|
|
25
|
+
## Main Config Precedence
|
|
26
|
+
|
|
27
|
+
When multiple main config files exist in the project root, Handoff picks the first match in this order and warns about ignored files:
|
|
28
|
+
|
|
29
|
+
1. `handoff.config.ts`
|
|
30
|
+
2. `handoff.config.js`
|
|
31
|
+
3. `handoff.config.cjs`
|
|
32
|
+
4. `handoff.config.json`
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Config } from '../types/config';
|
|
2
|
+
|
|
3
|
+
const normalizeAppConfig = (app?: Config['app']): Config['app'] => {
|
|
4
|
+
if (!app) return app;
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
googleTagManager,
|
|
8
|
+
typeCopy,
|
|
9
|
+
typeSort,
|
|
10
|
+
colorSort,
|
|
11
|
+
componentSort,
|
|
12
|
+
basePath,
|
|
13
|
+
...rest
|
|
14
|
+
} = app;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...rest,
|
|
18
|
+
google_tag_manager: googleTagManager ?? app.google_tag_manager,
|
|
19
|
+
type_copy: typeCopy ?? app.type_copy,
|
|
20
|
+
type_sort: typeSort ?? app.type_sort,
|
|
21
|
+
color_sort: colorSort ?? app.color_sort,
|
|
22
|
+
component_sort: componentSort ?? app.component_sort,
|
|
23
|
+
base_path: basePath ?? app.base_path,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const normalizeConfig = (config: Config): Config => {
|
|
28
|
+
const { devAccessToken, figmaProjectId, assetsZipLinks, app, ...rest } = config;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
...(rest as Config),
|
|
32
|
+
dev_access_token: devAccessToken ?? config.dev_access_token,
|
|
33
|
+
figma_project_id: figmaProjectId ?? config.figma_project_id,
|
|
34
|
+
assets_zip_links: assetsZipLinks ?? config.assets_zip_links,
|
|
35
|
+
app: normalizeAppConfig(app),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Optional helper for strongly-typed config authoring.
|
|
41
|
+
* It normalizes modern camelCase keys to legacy runtime keys.
|
|
42
|
+
*/
|
|
43
|
+
export const defineConfig = (config: Config): Config => normalizeConfig(config);
|
package/src/config/index.ts
CHANGED
|
@@ -3,7 +3,10 @@ export { defaultConfig, getClientConfig } from './defaults';
|
|
|
3
3
|
export type { ImageStyle } from './defaults';
|
|
4
4
|
|
|
5
5
|
// Config file loading
|
|
6
|
-
export { initConfig } from './loader';
|
|
6
|
+
export { initConfig, initConfigWithMetadata } from './loader';
|
|
7
|
+
|
|
8
|
+
// Config helpers
|
|
9
|
+
export { defineConfig } from './helpers';
|
|
7
10
|
|
|
8
11
|
// Runtime config resolution
|
|
9
12
|
export { initRuntimeConfig } from './runtime';
|
package/src/config/loader.ts
CHANGED
|
@@ -1,48 +1,127 @@
|
|
|
1
|
+
import esbuild from 'esbuild';
|
|
1
2
|
import fs from 'fs-extra';
|
|
3
|
+
import { createRequire } from 'module';
|
|
2
4
|
import path from 'path';
|
|
3
5
|
import { Config } from '../types/config';
|
|
6
|
+
import { Logger } from '../utils/logger';
|
|
4
7
|
import { defaultConfig } from './defaults';
|
|
5
8
|
|
|
9
|
+
const CONFIG_FILE_PREFERENCE = [
|
|
10
|
+
'handoff.config.ts',
|
|
11
|
+
'handoff.config.js',
|
|
12
|
+
'handoff.config.cjs',
|
|
13
|
+
'handoff.config.json',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
type ConfigLoadResult = {
|
|
17
|
+
config: Config;
|
|
18
|
+
configPath?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const evaluateTypeScriptConfig = (filePath: string, handoffModulePath: string): any => {
|
|
22
|
+
const buildResult = esbuild.buildSync({
|
|
23
|
+
entryPoints: [filePath],
|
|
24
|
+
bundle: true,
|
|
25
|
+
write: false,
|
|
26
|
+
platform: 'node',
|
|
27
|
+
format: 'cjs',
|
|
28
|
+
target: 'node16',
|
|
29
|
+
logLevel: 'silent',
|
|
30
|
+
external: ['handoff-app'],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const code = buildResult.outputFiles?.[0]?.text;
|
|
34
|
+
if (!code) {
|
|
35
|
+
throw new Error(`Unable to compile config file "${filePath}"`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mod: any = { exports: {} };
|
|
39
|
+
const localRequire = createRequire(filePath);
|
|
40
|
+
const handoffRequire = createRequire(path.resolve(handoffModulePath, 'package.json'));
|
|
41
|
+
const runtimeRequire = (id: string) => {
|
|
42
|
+
try {
|
|
43
|
+
return localRequire(id);
|
|
44
|
+
} catch {
|
|
45
|
+
return handoffRequire(id);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const evaluator = new Function('require', 'module', 'exports', '__filename', '__dirname', code);
|
|
49
|
+
evaluator(runtimeRequire, mod, mod.exports, filePath, path.dirname(filePath));
|
|
50
|
+
return mod.exports;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const loadConfigFile = (configPath: string): Config => {
|
|
54
|
+
if (configPath.endsWith('.json')) {
|
|
55
|
+
const buffer = fs.readFileSync(configPath);
|
|
56
|
+
return JSON.parse(buffer.toString()) as Config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (configPath.endsWith('.ts')) {
|
|
60
|
+
const handoffModulePath = path.resolve(__dirname, '../..');
|
|
61
|
+
const importedConfig = evaluateTypeScriptConfig(configPath, handoffModulePath);
|
|
62
|
+
return (importedConfig.default || importedConfig) as Config;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Invalidate require cache to ensure fresh read
|
|
66
|
+
delete require.cache[require.resolve(configPath)];
|
|
67
|
+
const importedConfig = require(configPath);
|
|
68
|
+
return (importedConfig.default || importedConfig) as Config;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resolveConfigFilePath = (): { selected?: string; ignored: string[] } => {
|
|
72
|
+
const existing = CONFIG_FILE_PREFERENCE
|
|
73
|
+
.map((fileName) => path.resolve(process.cwd(), fileName))
|
|
74
|
+
.filter((filePath) => fs.existsSync(filePath));
|
|
75
|
+
|
|
76
|
+
if (!existing.length) {
|
|
77
|
+
return { selected: undefined, ignored: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const [selected, ...ignored] = existing;
|
|
81
|
+
return { selected, ignored };
|
|
82
|
+
};
|
|
83
|
+
|
|
6
84
|
/**
|
|
7
|
-
* Loads the handoff configuration from the project root.
|
|
8
|
-
*
|
|
9
|
-
* Searches for config files in order: handoff.config.json, handoff.config.js, handoff.config.cjs.
|
|
10
|
-
* Merges file config with any provided overrides, then applies defaults for missing values.
|
|
11
|
-
*
|
|
12
|
-
* @param configOverride - Optional partial config to override file-loaded values.
|
|
13
|
-
* @returns The fully resolved Config object.
|
|
85
|
+
* Loads the handoff configuration from the project root and returns metadata.
|
|
14
86
|
*/
|
|
15
|
-
export const
|
|
16
|
-
let config = {};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Invalidate require cache to ensure fresh read
|
|
30
|
-
delete require.cache[require.resolve(configPath)];
|
|
31
|
-
const importedConfig = require(configPath);
|
|
32
|
-
config = importedConfig.default || importedConfig;
|
|
33
|
-
}
|
|
87
|
+
export const initConfigWithMetadata = (configOverride?: Partial<Config>): ConfigLoadResult => {
|
|
88
|
+
let config: Partial<Config> = {};
|
|
89
|
+
const { selected: configPath, ignored } = resolveConfigFilePath();
|
|
90
|
+
|
|
91
|
+
if (ignored.length > 0 && configPath) {
|
|
92
|
+
Logger.warn(
|
|
93
|
+
`Multiple config files found. Using "${path.basename(configPath)}" and ignoring: ${ignored
|
|
94
|
+
.map((filePath) => `"${path.basename(filePath)}"`)
|
|
95
|
+
.join(', ')}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (configPath) {
|
|
100
|
+
config = loadConfigFile(configPath);
|
|
34
101
|
}
|
|
35
102
|
|
|
36
103
|
// Apply overrides if provided
|
|
37
104
|
if (configOverride) {
|
|
38
|
-
Object.
|
|
39
|
-
const value = configOverride[key as keyof Config];
|
|
105
|
+
(Object.entries(configOverride) as [keyof Config, Config[keyof Config]][]).forEach(([key, value]) => {
|
|
40
106
|
if (value !== undefined) {
|
|
41
|
-
config[key as
|
|
107
|
+
(config as Record<string, unknown>)[key as string] = value;
|
|
42
108
|
}
|
|
43
109
|
});
|
|
44
110
|
}
|
|
45
111
|
|
|
46
|
-
const
|
|
47
|
-
return
|
|
112
|
+
const resolvedConfig = { ...defaultConfig(), ...config } as Config;
|
|
113
|
+
return { config: resolvedConfig, configPath };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Loads the handoff configuration from the project root.
|
|
118
|
+
*
|
|
119
|
+
* Searches for config files in order: handoff.config.ts, handoff.config.js, handoff.config.cjs, handoff.config.json.
|
|
120
|
+
* Merges file config with any provided overrides, then applies defaults for missing values.
|
|
121
|
+
*
|
|
122
|
+
* @param configOverride - Optional partial config to override file-loaded values.
|
|
123
|
+
* @returns The fully resolved Config object.
|
|
124
|
+
*/
|
|
125
|
+
export const initConfig = (configOverride?: Partial<Config>): Config => {
|
|
126
|
+
return initConfigWithMetadata(configOverride).config;
|
|
48
127
|
};
|