handoff-app 1.1.0 → 1.1.2-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/Changelog.md +58 -0
- package/README.md +7 -0
- package/dist/app-builder/build.js +20 -11
- package/dist/app-builder/watchers.js +8 -9
- package/dist/cache/build-cache.js +25 -10
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/transformers/plugins/handlebars-previews.js +17 -5
- package/dist/transformers/preview/component/builder.js +16 -1
- package/dist/transformers/utils/handlebars.d.ts +4 -2
- package/dist/transformers/utils/handlebars.js +10 -5
- package/dist/transformers/utils/html.js +15 -3
- package/dist/types/config.d.ts +25 -0
- package/dist/utils/path.d.ts +15 -0
- package/dist/utils/path.js +31 -0
- package/docs/api.md +65 -71
- package/package.json +1 -1
- package/src/app/components/Component/ComponentLists.tsx +5 -4
- package/src/app/components/Navigation/MainNav.tsx +3 -5
- package/src/app/components/Navigation/SideNav.tsx +4 -7
- package/src/app/components/SideNav/Custom.tsx +5 -3
- package/src/app/components/ui/mobile-nav.tsx +10 -7
- package/src/app/components/util/index.ts +2 -2
- package/src/app/lib/utils.ts +13 -0
- package/src/app/next.config.mjs +21 -13
- package/src/app/pages/system/component/[component]/index.tsx +1 -1
- package/src/app-builder/build.ts +21 -11
- package/src/app-builder/watchers.ts +9 -8
- package/src/cache/build-cache.ts +29 -11
- package/src/cli.ts +1 -1
- package/src/index.ts +1 -1
- package/src/transformers/plugins/handlebars-previews.ts +22 -6
- package/src/transformers/preview/component/builder.ts +17 -1
- package/src/transformers/utils/handlebars.ts +17 -9
- package/src/transformers/utils/html.ts +16 -3
- package/src/types/config.ts +25 -0
- package/src/utils/path.ts +29 -0
- package/templates/default/handoff.config.js.tpl +16 -0
- package/templates/default/handoff.config.with-examples.js.tpl +16 -0
package/Changelog.md
CHANGED
|
@@ -6,6 +6,61 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
6
6
|
and this project adheres to
|
|
7
7
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
8
8
|
|
|
9
|
+
## \[1.1.2] - 2026-03-26
|
|
10
|
+
|
|
11
|
+
This release improves integration with downstream platforms (for example HubSpot) by letting
|
|
12
|
+
projects register their own Handlebars helpers during preview generation, instead of relying
|
|
13
|
+
only on Handoff’s built-in `field` and `eq` helpers.
|
|
14
|
+
|
|
15
|
+
### Feature
|
|
16
|
+
|
|
17
|
+
**`hooks.registerHandlebarsHelpers`** — New optional hook in `handoff.config.js`. After Handoff
|
|
18
|
+
registers its default preview helpers, your callback runs with the Handlebars runtime, the
|
|
19
|
+
current `componentId`, the component `properties` (slot metadata), and a flag for inspect-mode
|
|
20
|
+
renders (`injectFieldWrappers`). Use `handlebars.registerHelper` (and block helpers as needed)
|
|
21
|
+
to add helpers that are only available while building component preview HTML—so templates can
|
|
22
|
+
stay platform-specific without forking Handoff.
|
|
23
|
+
|
|
24
|
+
Typical uses: block helpers that wrap regions meant for a host page’s header or footer, or a
|
|
25
|
+
helper that serializes a value as JSON for inline scripts. Those behaviors are **not** shipped
|
|
26
|
+
as core helpers; you define them in config so each project controls naming and semantics.
|
|
27
|
+
|
|
28
|
+
See `docs/api.md` (Hooks → `registerHandlebarsHelpers`) for the full signature and README
|
|
29
|
+
“Configuration hooks” for a quick pointer.
|
|
30
|
+
|
|
31
|
+
### Example: JSON in templates
|
|
32
|
+
|
|
33
|
+
If you register a `json` helper in `hooks.registerHandlebarsHelpers`, like this
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
hooks: {
|
|
37
|
+
registerHandlebarsHelpers: ({ handlebars, componentId }) => {
|
|
38
|
+
// Register 'json' as a simple (non-block) helper
|
|
39
|
+
handlebars.registerHelper('json', (value, data) => {
|
|
40
|
+
return new handlebars.SafeString(JSON.stringify(value, null, 2));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then your templates can do:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
<script type="text/javascript">
|
|
50
|
+
window.barChartData = {{{json properties.data}}};
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
with output shaped by your helper implementation (for example serialized JSON for chart data).
|
|
55
|
+
|
|
56
|
+
## \[1.1.1] - 2026-03-12
|
|
57
|
+
|
|
58
|
+
This patch release focuses on Windows stabilization and consistency across environments.
|
|
59
|
+
|
|
60
|
+
It includes targeted fixes to reduce platform-specific issues in local development and documentation rendering, especially around path handling, routing/navigation behavior, and runtime update detection. These changes aim to make behavior on Windows align more closely with Mac and Linux, with no intended breaking changes.
|
|
61
|
+
|
|
62
|
+
Also includes a fallback for components with empty previews definitions, preventing preview generation failures during builds.
|
|
63
|
+
|
|
9
64
|
## \[1.1.0] - 2026-03-05
|
|
10
65
|
|
|
11
66
|
This release introduces two major new features. First, handoff now can do automatic
|
|
@@ -1822,3 +1877,6 @@ package version was set to ^0.1.0 rather than ^0.2.0. This release fixes that.
|
|
|
1822
1877
|
|
|
1823
1878
|
```
|
|
1824
1879
|
```
|
|
1880
|
+
|
|
1881
|
+
```
|
|
1882
|
+
```
|
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ transform tokens into dev ready artifacts.
|
|
|
16
16
|
* [How does it work](#how-does-it-work)
|
|
17
17
|
* [Handoff is in Beta!](#handoff-is-in-beta)
|
|
18
18
|
* [Requirements](#requirements)
|
|
19
|
+
* [Configuration hooks](#configuration-hooks)
|
|
19
20
|
* [Get Started](#get-started)
|
|
20
21
|
* [Figma](#figma)
|
|
21
22
|
* [Create a project](#create-a-project)
|
|
@@ -168,6 +169,12 @@ handoff-app start
|
|
|
168
169
|
|
|
169
170
|
Once that runs, your browser should update with the new colors.
|
|
170
171
|
|
|
172
|
+
## Configuration hooks
|
|
173
|
+
|
|
174
|
+
Pipeline customization is done in `handoff.config.js` under `hooks` (camelCase names: `validateComponent`, `jsBuildConfig`, `registerHandlebarsHelpers`, etc.). For example, `registerHandlebarsHelpers` runs after Handoff registers the built-in `field` and `eq` helpers so you can call `handlebars.registerHelper` for your `.hbs` preview templates.
|
|
175
|
+
|
|
176
|
+
See [docs/api.md](docs/api.md#hooks) for hook arguments and examples.
|
|
177
|
+
|
|
171
178
|
## Further Reading
|
|
172
179
|
|
|
173
180
|
* [Configure your project](https://www.handoff.com/docs/customization)
|
|
@@ -25,6 +25,7 @@ const client_config_1 = require("./client-config");
|
|
|
25
25
|
const paths_1 = require("./paths");
|
|
26
26
|
const watchers_1 = require("./watchers");
|
|
27
27
|
const websocket_1 = require("./websocket");
|
|
28
|
+
const escapeForSingleQuotedJsString = (value) => value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
28
29
|
/**
|
|
29
30
|
* Performs cleanup of the application directory by removing the existing app directory if it exists.
|
|
30
31
|
*/
|
|
@@ -57,8 +58,7 @@ const initializeProjectApp = (handoff) => __awaiter(void 0, void 0, void 0, func
|
|
|
57
58
|
yield fs_extra_1.default.copy(customThemePath, destPath, { overwrite: true });
|
|
58
59
|
logger_1.Logger.success(`Custom theme.css loaded`);
|
|
59
60
|
}
|
|
60
|
-
// Prepare project app configuration
|
|
61
|
-
// Warning: Regex replacement is fragile and depends on exact formatting in next.config.mjs
|
|
61
|
+
// Prepare project app configuration using stable placeholder replacement.
|
|
62
62
|
const handoffProjectId = handoff.getProjectId();
|
|
63
63
|
const handoffAppBasePath = (_a = handoff.config.app.base_path) !== null && _a !== void 0 ? _a : '';
|
|
64
64
|
const handoffWorkingPath = path_1.default.resolve(handoff.workingPath);
|
|
@@ -67,15 +67,24 @@ const initializeProjectApp = (handoff) => __awaiter(void 0, void 0, void 0, func
|
|
|
67
67
|
const nextConfigPath = path_1.default.resolve(srcPath, 'next.config.mjs');
|
|
68
68
|
const targetPath = path_1.default.resolve(appPath, 'next.config.mjs');
|
|
69
69
|
const handoffWebsocketPort = (_c = (_b = handoff.config.app.ports) === null || _b === void 0 ? void 0 : _b.websocket) !== null && _c !== void 0 ? _c : 3001;
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
const escapedAppBasePath = escapeForSingleQuotedJsString(handoffAppBasePath);
|
|
71
|
+
const escapedProjectId = escapeForSingleQuotedJsString(handoffProjectId);
|
|
72
|
+
const escapedWorkingPath = escapeForSingleQuotedJsString(handoffWorkingPath);
|
|
73
|
+
const escapedModulePath = escapeForSingleQuotedJsString(handoffModulePath);
|
|
74
|
+
const escapedExportPath = escapeForSingleQuotedJsString(handoffExportPath);
|
|
75
|
+
const escapedWebsocketPort = escapeForSingleQuotedJsString(String(handoffWebsocketPort));
|
|
76
|
+
const placeholderValues = {
|
|
77
|
+
'%HANDOFF_PROJECT_ID%': escapedProjectId,
|
|
78
|
+
'%HANDOFF_APP_BASE_PATH%': escapedAppBasePath,
|
|
79
|
+
'%HANDOFF_WORKING_PATH%': escapedWorkingPath,
|
|
80
|
+
'%HANDOFF_MODULE_PATH%': escapedModulePath,
|
|
81
|
+
'%HANDOFF_EXPORT_PATH%': escapedExportPath,
|
|
82
|
+
'%HANDOFF_WEBSOCKET_PORT%': escapedWebsocketPort,
|
|
83
|
+
};
|
|
84
|
+
let nextConfigContent = yield fs_extra_1.default.readFile(nextConfigPath, 'utf-8');
|
|
85
|
+
for (const [placeholder, value] of Object.entries(placeholderValues)) {
|
|
86
|
+
nextConfigContent = nextConfigContent.split(placeholder).join(value);
|
|
87
|
+
}
|
|
79
88
|
yield fs_extra_1.default.writeFile(targetPath, nextConfigContent);
|
|
80
89
|
return appPath;
|
|
81
90
|
});
|
|
@@ -43,6 +43,7 @@ const css_1 = require("../transformers/preview/component/css");
|
|
|
43
43
|
const javascript_1 = require("../transformers/preview/component/javascript");
|
|
44
44
|
const builder_1 = __importStar(require("../transformers/preview/component/builder"));
|
|
45
45
|
const logger_1 = require("../utils/logger");
|
|
46
|
+
const path_2 = require("../utils/path");
|
|
46
47
|
const client_config_1 = require("./client-config");
|
|
47
48
|
const paths_1 = require("./paths");
|
|
48
49
|
/**
|
|
@@ -145,11 +146,6 @@ const watchGlobalEntries = (handoff, state, chokidarConfig) => {
|
|
|
145
146
|
const pathsToWatch = [...scssPathsToWatch, ...jsPathsToWatch];
|
|
146
147
|
if (pathsToWatch.length === 0)
|
|
147
148
|
return;
|
|
148
|
-
const isPathInside = (filePath, parentPath) => {
|
|
149
|
-
const normalizedFilePath = path_1.default.resolve(filePath);
|
|
150
|
-
const normalizedParentPath = path_1.default.resolve(parentPath);
|
|
151
|
-
return normalizedFilePath === normalizedParentPath || normalizedFilePath.startsWith(`${normalizedParentPath}${path_1.default.sep}`);
|
|
152
|
-
};
|
|
153
149
|
chokidar_1.default.watch(pathsToWatch, chokidarConfig).on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
154
150
|
switch (event) {
|
|
155
151
|
case 'add':
|
|
@@ -159,8 +155,8 @@ const watchGlobalEntries = (handoff, state, chokidarConfig) => {
|
|
|
159
155
|
state.debounce = true;
|
|
160
156
|
try {
|
|
161
157
|
logger_1.Logger.warn('Global entry changed. Rebuilding bundles and components...');
|
|
162
|
-
const shouldRebuildMainCss = !!scssEntry && scssPathsToWatch.some((watchPath) => isPathInside(file, watchPath));
|
|
163
|
-
const shouldRebuildMainJs = !!jsEntry && jsPathsToWatch.some((watchPath) => isPathInside(file, watchPath));
|
|
158
|
+
const shouldRebuildMainCss = !!scssEntry && scssPathsToWatch.some((watchPath) => (0, path_2.isPathInside)(file, watchPath));
|
|
159
|
+
const shouldRebuildMainJs = !!jsEntry && jsPathsToWatch.some((watchPath) => (0, path_2.isPathInside)(file, watchPath));
|
|
164
160
|
if (shouldRebuildMainCss) {
|
|
165
161
|
yield (0, css_1.buildMainCss)(handoff);
|
|
166
162
|
}
|
|
@@ -228,11 +224,14 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
|
|
|
228
224
|
}
|
|
229
225
|
if (runtimeComponentPathsToWatch.size > 0) {
|
|
230
226
|
const pathsToWatch = Array.from(runtimeComponentPathsToWatch.keys());
|
|
227
|
+
const runtimeComponentEntryTypeByPath = new Map(Array.from(runtimeComponentPathsToWatch.entries()).map(([watchPath, entryType]) => [(0, path_2.normalizePathForCompare)(watchPath), entryType]));
|
|
228
|
+
const configFileSet = new Set(handoff.getConfigFilePaths().map((configPath) => (0, path_2.normalizePathForCompare)(configPath)));
|
|
231
229
|
state.runtimeComponentsWatcher = chokidar_1.default.watch(pathsToWatch, {
|
|
232
230
|
ignoreInitial: true,
|
|
233
231
|
});
|
|
234
232
|
state.runtimeComponentsWatcher.on('all', (event, file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
235
|
-
|
|
233
|
+
const normalizedFile = (0, path_2.normalizePathForCompare)(file);
|
|
234
|
+
if (configFileSet.has(normalizedFile)) {
|
|
236
235
|
return;
|
|
237
236
|
}
|
|
238
237
|
switch (event) {
|
|
@@ -242,7 +241,7 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
|
|
|
242
241
|
if (!state.debounce) {
|
|
243
242
|
state.debounce = true;
|
|
244
243
|
try {
|
|
245
|
-
const entryType =
|
|
244
|
+
const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
|
|
246
245
|
const segmentToUpdate = entryType ? (0, exports.mapEntryTypeToSegment)(entryType) : undefined;
|
|
247
246
|
const componentDir = path_1.default.basename(path_1.default.dirname(file));
|
|
248
247
|
yield (0, builder_1.default)(handoff, componentDir, segmentToUpdate);
|
|
@@ -27,6 +27,7 @@ exports.pruneRemovedComponents = pruneRemovedComponents;
|
|
|
27
27
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
28
28
|
const path_1 = __importDefault(require("path"));
|
|
29
29
|
const logger_1 = require("../utils/logger");
|
|
30
|
+
const path_2 = require("../utils/path");
|
|
30
31
|
const file_state_1 = require("./file-state");
|
|
31
32
|
/** Current cache format version - bump when structure changes */
|
|
32
33
|
const CACHE_VERSION = '1.0.0';
|
|
@@ -142,23 +143,17 @@ function getComponentFilePaths(handoff, componentId) {
|
|
|
142
143
|
}
|
|
143
144
|
const files = [];
|
|
144
145
|
let templateDir;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (const configPath of configPaths) {
|
|
148
|
-
// Check if this config path belongs to this component
|
|
149
|
-
if (configPath.includes(componentId)) {
|
|
150
|
-
files.push(configPath);
|
|
151
|
-
break;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// Add entry files
|
|
146
|
+
const componentDirs = new Set();
|
|
147
|
+
// Add entry files and infer component directories from resolved entry paths
|
|
155
148
|
const entries = runtimeComponent.entries;
|
|
156
149
|
if (entries) {
|
|
157
150
|
if (entries.js) {
|
|
158
151
|
files.push(entries.js);
|
|
152
|
+
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(entries.js)));
|
|
159
153
|
}
|
|
160
154
|
if (entries.scss) {
|
|
161
155
|
files.push(entries.scss);
|
|
156
|
+
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(entries.scss)));
|
|
162
157
|
}
|
|
163
158
|
// Handle both 'template' (singular) and 'templates' (plural) entry types
|
|
164
159
|
const templatePath = entries.template || entries.templates;
|
|
@@ -167,17 +162,37 @@ function getComponentFilePaths(handoff, componentId) {
|
|
|
167
162
|
const stat = fs_extra_1.default.statSync(templatePath);
|
|
168
163
|
if (stat.isDirectory()) {
|
|
169
164
|
templateDir = templatePath;
|
|
165
|
+
componentDirs.add((0, path_2.normalizePathForCompare)(templatePath));
|
|
170
166
|
}
|
|
171
167
|
else {
|
|
172
168
|
files.push(templatePath);
|
|
169
|
+
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(templatePath)));
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
catch (_d) {
|
|
176
173
|
// File doesn't exist, still add to track
|
|
177
174
|
files.push(templatePath);
|
|
175
|
+
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(templatePath)));
|
|
178
176
|
}
|
|
179
177
|
}
|
|
180
178
|
}
|
|
179
|
+
// Find the config file path for this component using exact config filename + directory matching.
|
|
180
|
+
const configPaths = handoff.getConfigFilePaths();
|
|
181
|
+
const expectedConfigFileNames = new Set([`${componentId}.json`, `${componentId}.js`, `${componentId}.cjs`]);
|
|
182
|
+
const matchingConfigPath = configPaths.find((configPath) => {
|
|
183
|
+
const configFileName = path_1.default.basename(configPath);
|
|
184
|
+
if (!expectedConfigFileNames.has(configFileName)) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (componentDirs.size === 0) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
const configDir = (0, path_2.normalizePathForCompare)(path_1.default.dirname(configPath));
|
|
191
|
+
return componentDirs.has(configDir);
|
|
192
|
+
});
|
|
193
|
+
if (matchingConfigPath) {
|
|
194
|
+
files.push(matchingConfigPath);
|
|
195
|
+
}
|
|
181
196
|
return { files, templateDir };
|
|
182
197
|
}
|
|
183
198
|
/**
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -95,6 +95,6 @@ declare class Handoff {
|
|
|
95
95
|
private readJsonFile;
|
|
96
96
|
}
|
|
97
97
|
export type { ComponentObject as Component } from './transformers/preview/types';
|
|
98
|
-
export type { Config } from './types/config';
|
|
98
|
+
export type { Config, RegisterHandlebarsHelpersContext } from './types/config';
|
|
99
99
|
export { Transformers as CoreTransformers, TransformerUtils as CoreTransformerUtils, Types as CoreTypes } from 'handoff-core';
|
|
100
100
|
export default Handoff;
|
|
@@ -61,10 +61,11 @@ function processComponentInstances(componentData, documentationComponents) {
|
|
|
61
61
|
* @param injectFieldWrappers - Whether to inject field wrappers for inspection
|
|
62
62
|
* @returns Rendered HTML string
|
|
63
63
|
*/
|
|
64
|
-
function renderHandlebarsTemplate(template, componentData, previewData, injectFieldWrappers) {
|
|
64
|
+
function renderHandlebarsTemplate(template, componentData, previewData, injectFieldWrappers, handoff) {
|
|
65
65
|
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
var _a, _b;
|
|
66
67
|
// Register Handlebars helpers with current injection state
|
|
67
|
-
(0, handlebars_2.registerHandlebarsHelpers)({ id: componentData.id, properties: componentData.properties || {} }, injectFieldWrappers);
|
|
68
|
+
(0, handlebars_2.registerHandlebarsHelpers)({ id: componentData.id, properties: componentData.properties || {} }, injectFieldWrappers, (_b = (_a = handoff.config) === null || _a === void 0 ? void 0 : _a.hooks) === null || _b === void 0 ? void 0 : _b.registerHandlebarsHelpers);
|
|
68
69
|
const context = (0, handlebars_2.createHandlebarsContext)({
|
|
69
70
|
id: componentData.id,
|
|
70
71
|
properties: componentData.properties || {},
|
|
@@ -131,14 +132,24 @@ function handlebarsPreviewsPlugin(componentData, documentationComponents, handof
|
|
|
131
132
|
}
|
|
132
133
|
// Process component instances from documentation
|
|
133
134
|
processComponentInstances(componentData, documentationComponents);
|
|
135
|
+
if (!componentData.previews || Object.keys(componentData.previews).length === 0) {
|
|
136
|
+
logger_1.Logger.warn(`No previews defined for ${componentId}; using default preview values.`);
|
|
137
|
+
componentData.previews = {
|
|
138
|
+
default: {
|
|
139
|
+
title: 'Default',
|
|
140
|
+
values: {},
|
|
141
|
+
url: '',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
134
145
|
const generatedPreviews = {};
|
|
135
146
|
// Generate previews for each variation
|
|
136
147
|
for (const previewKey in componentData.previews) {
|
|
137
148
|
try {
|
|
138
149
|
const previewData = componentData.previews[previewKey];
|
|
139
150
|
// Render both normal and inspect modes
|
|
140
|
-
const normalModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, false);
|
|
141
|
-
const inspectModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, true);
|
|
151
|
+
const normalModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, false, handoff);
|
|
152
|
+
const inspectModeHtml = yield renderHandlebarsTemplate(templateContent, componentData, previewData, true, handoff);
|
|
142
153
|
// Emit preview files
|
|
143
154
|
emitPreviewFiles(componentId, previewKey, normalModeHtml, inspectModeHtml, (file) => this.emitFile(file));
|
|
144
155
|
generatedPreviews[previewKey] = normalModeHtml;
|
|
@@ -153,7 +164,8 @@ function handlebarsPreviewsPlugin(componentData, documentationComponents, handof
|
|
|
153
164
|
componentData.format = PLUGIN_CONSTANTS.OUTPUT_FORMAT;
|
|
154
165
|
componentData.preview = '';
|
|
155
166
|
componentData.code = (0, html_1.trimPreview)(templateContent);
|
|
156
|
-
|
|
167
|
+
const firstGeneratedPreview = Object.values(generatedPreviews)[0];
|
|
168
|
+
componentData.html = (0, html_1.trimPreview)(firstGeneratedPreview !== null && firstGeneratedPreview !== void 0 ? firstGeneratedPreview : templateContent);
|
|
157
169
|
});
|
|
158
170
|
},
|
|
159
171
|
};
|
|
@@ -66,6 +66,19 @@ var ComponentSegment;
|
|
|
66
66
|
ComponentSegment["Previews"] = "previews";
|
|
67
67
|
ComponentSegment["Validation"] = "validation";
|
|
68
68
|
})(ComponentSegment || (exports.ComponentSegment = ComponentSegment = {}));
|
|
69
|
+
const ensureDefaultPreview = (data) => {
|
|
70
|
+
if (!data)
|
|
71
|
+
return;
|
|
72
|
+
if (!data.previews || Object.keys(data.previews).length === 0) {
|
|
73
|
+
data.previews = {
|
|
74
|
+
default: {
|
|
75
|
+
title: 'Default',
|
|
76
|
+
values: {},
|
|
77
|
+
url: '',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
69
82
|
/**
|
|
70
83
|
* Returns a normalized build plan describing which component segments need rebuilding.
|
|
71
84
|
*
|
|
@@ -200,7 +213,7 @@ function processComponents(handoff, id, segmentToProcess, options) {
|
|
|
200
213
|
// those will override this default (standard "config overrides defaults" behavior).
|
|
201
214
|
if (!restMetadata.figmaComponentId) {
|
|
202
215
|
componentDefaults.previews = {
|
|
203
|
-
|
|
216
|
+
default: {
|
|
204
217
|
title: 'Default',
|
|
205
218
|
values: {},
|
|
206
219
|
url: '',
|
|
@@ -246,6 +259,8 @@ function processComponents(handoff, id, segmentToProcess, options) {
|
|
|
246
259
|
data.properties = existingData.properties;
|
|
247
260
|
}
|
|
248
261
|
}
|
|
262
|
+
// Components should always have at least one preview variation.
|
|
263
|
+
ensureDefaultPreview(data);
|
|
249
264
|
// Build JS if needed (new build, validation missing, or explicit segment request).
|
|
250
265
|
if (buildPlan.js) {
|
|
251
266
|
data = yield (0, javascript_1.default)(data, handoff);
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { RegisterHandlebarsHelpersContext } from '../../types/config';
|
|
1
2
|
import { SlotMetadata } from '../preview/component';
|
|
2
3
|
import { HandlebarsContext } from '../types';
|
|
3
4
|
/**
|
|
4
|
-
* Registers common Handlebars helpers
|
|
5
|
+
* Registers common Handlebars helpers, then runs optional `hooks.registerHandlebarsHelpers`.
|
|
5
6
|
* @param data - Component data containing properties
|
|
6
7
|
* @param injectFieldWrappers - Whether to inject field wrappers for inspection
|
|
8
|
+
* @param extend - Optional callback from config to register additional helpers
|
|
7
9
|
*/
|
|
8
10
|
export declare const registerHandlebarsHelpers: (data: {
|
|
9
11
|
properties: {
|
|
10
12
|
[key: string]: SlotMetadata;
|
|
11
13
|
};
|
|
12
14
|
id: string;
|
|
13
|
-
}, injectFieldWrappers: boolean) => void;
|
|
15
|
+
}, injectFieldWrappers: boolean, extend?: (context: RegisterHandlebarsHelpersContext) => void) => void;
|
|
14
16
|
/**
|
|
15
17
|
* Creates Handlebars template context
|
|
16
18
|
* @param data - Component data
|
|
@@ -7,11 +7,12 @@ exports.createHandlebarsContext = exports.registerHandlebarsHelpers = void 0;
|
|
|
7
7
|
const handlebars_1 = __importDefault(require("handlebars"));
|
|
8
8
|
const logger_1 = require("../../utils/logger");
|
|
9
9
|
/**
|
|
10
|
-
* Registers common Handlebars helpers
|
|
10
|
+
* Registers common Handlebars helpers, then runs optional `hooks.registerHandlebarsHelpers`.
|
|
11
11
|
* @param data - Component data containing properties
|
|
12
12
|
* @param injectFieldWrappers - Whether to inject field wrappers for inspection
|
|
13
|
+
* @param extend - Optional callback from config to register additional helpers
|
|
13
14
|
*/
|
|
14
|
-
const registerHandlebarsHelpers = (data, injectFieldWrappers) => {
|
|
15
|
+
const registerHandlebarsHelpers = (data, injectFieldWrappers, extend) => {
|
|
15
16
|
// Field helper for property binding
|
|
16
17
|
handlebars_1.default.registerHelper('field', function (field, options) {
|
|
17
18
|
if (injectFieldWrappers) {
|
|
@@ -43,6 +44,12 @@ const registerHandlebarsHelpers = (data, injectFieldWrappers) => {
|
|
|
43
44
|
handlebars_1.default.registerHelper('eq', function (a, b) {
|
|
44
45
|
return a === b;
|
|
45
46
|
});
|
|
47
|
+
extend === null || extend === void 0 ? void 0 : extend({
|
|
48
|
+
handlebars: handlebars_1.default,
|
|
49
|
+
componentId: data.id,
|
|
50
|
+
properties: data.properties,
|
|
51
|
+
injectFieldWrappers,
|
|
52
|
+
});
|
|
46
53
|
};
|
|
47
54
|
exports.registerHandlebarsHelpers = registerHandlebarsHelpers;
|
|
48
55
|
/**
|
|
@@ -54,9 +61,7 @@ exports.registerHandlebarsHelpers = registerHandlebarsHelpers;
|
|
|
54
61
|
const createHandlebarsContext = (data, previewData, options) => {
|
|
55
62
|
var _a;
|
|
56
63
|
const basePath = (_a = process.env.HANDOFF_APP_BASE_PATH) !== null && _a !== void 0 ? _a : '';
|
|
57
|
-
const sharedStylesLink = (options === null || options === void 0 ? void 0 : options.includeSharedStyles)
|
|
58
|
-
? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">`
|
|
59
|
-
: '';
|
|
64
|
+
const sharedStylesLink = (options === null || options === void 0 ? void 0 : options.includeSharedStyles) ? `<link rel="stylesheet" href="${basePath}/api/component/shared.css">` : '';
|
|
60
65
|
return {
|
|
61
66
|
style: `${sharedStylesLink}<link rel="stylesheet" href="${basePath}/api/component/main.css">` +
|
|
62
67
|
`<link rel="stylesheet" href="${basePath}/api/component/${data.id}.css">\n` +
|
|
@@ -21,9 +21,21 @@ const prettier_1 = __importDefault(require("prettier"));
|
|
|
21
21
|
* @returns Trimmed HTML content
|
|
22
22
|
*/
|
|
23
23
|
const trimPreview = (preview) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (typeof preview !== 'string') {
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
const normalizedPreview = preview.trim();
|
|
28
|
+
if (!normalizedPreview) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const bodyEl = (0, node_html_parser_1.parse)(normalizedPreview).querySelector('body');
|
|
33
|
+
return bodyEl ? bodyEl.innerHTML.trim() : normalizedPreview;
|
|
34
|
+
}
|
|
35
|
+
catch (_a) {
|
|
36
|
+
// Keep build resilient if parser fails on malformed HTML.
|
|
37
|
+
return normalizedPreview;
|
|
38
|
+
}
|
|
27
39
|
};
|
|
28
40
|
exports.trimPreview = trimPreview;
|
|
29
41
|
/**
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BuildOptions } from 'esbuild';
|
|
2
|
+
import type Handlebars from 'handlebars';
|
|
2
3
|
import { Types as HandoffTypes } from 'handoff-core';
|
|
3
4
|
import { InlineConfig } from 'vite';
|
|
4
5
|
import { SlotMetadata } from '../transformers/preview/component';
|
|
@@ -51,6 +52,15 @@ export interface Breakpoints {
|
|
|
51
52
|
name: string;
|
|
52
53
|
};
|
|
53
54
|
}
|
|
55
|
+
/** Context passed to the `registerHandlebarsHelpers` config hook after built-in helpers (`field`, `eq`) are registered for a preview render. */
|
|
56
|
+
export type RegisterHandlebarsHelpersContext = {
|
|
57
|
+
handlebars: typeof Handlebars;
|
|
58
|
+
componentId: string;
|
|
59
|
+
properties: {
|
|
60
|
+
[key: string]: SlotMetadata;
|
|
61
|
+
};
|
|
62
|
+
injectFieldWrappers: boolean;
|
|
63
|
+
};
|
|
54
64
|
export interface NextAppConfig {
|
|
55
65
|
theme?: string;
|
|
56
66
|
title: string;
|
|
@@ -238,6 +248,21 @@ export interface Config {
|
|
|
238
248
|
* ```
|
|
239
249
|
*/
|
|
240
250
|
htmlBuildConfig?: (config: InlineConfig) => InlineConfig;
|
|
251
|
+
/**
|
|
252
|
+
* Optional hook invoked after Handoff registers built-in Handlebars helpers for
|
|
253
|
+
* component preview HTML. Use `context.handlebars.registerHelper` to add or
|
|
254
|
+
* replace helpers. Called once per preview render (per variation and inspect mode).
|
|
255
|
+
*
|
|
256
|
+
* @param context - Handlebars runtime, component id/properties, and whether
|
|
257
|
+
* inspect field wrappers are enabled for this render.
|
|
258
|
+
* @example
|
|
259
|
+
* ```typescript
|
|
260
|
+
* registerHandlebarsHelpers: ({ handlebars, componentId }) => {
|
|
261
|
+
* handlebars.registerHelper('upperId', () => componentId.toUpperCase());
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
registerHandlebarsHelpers?: (context: RegisterHandlebarsHelpersContext) => void;
|
|
241
266
|
};
|
|
242
267
|
}
|
|
243
268
|
export type ClientConfig = Pick<Config, 'app' | 'exportsOutputDirectory' | 'sitesOutputDirectory' | 'assets_zip_links' | 'useVariables'>;
|
package/dist/utils/path.d.ts
CHANGED
|
@@ -4,3 +4,18 @@
|
|
|
4
4
|
* @returns A filesystem-safe string suitable for use as a directory name
|
|
5
5
|
*/
|
|
6
6
|
export declare function generateFilesystemSafeId(workingPath: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a filesystem path for cross-platform string comparison.
|
|
9
|
+
* - Resolves to absolute path
|
|
10
|
+
* - Normalizes separators to forward slashes
|
|
11
|
+
* - Lowercases on Windows where paths are case-insensitive
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizePathForCompare(inputPath: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* True when both paths resolve to the same normalized filesystem path.
|
|
16
|
+
*/
|
|
17
|
+
export declare function arePathsEqual(pathA: string, pathB: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* True when childPath is equal to or within parentPath.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isPathInside(childPath: string, parentPath: string): boolean;
|
package/dist/utils/path.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.generateFilesystemSafeId = generateFilesystemSafeId;
|
|
7
|
+
exports.normalizePathForCompare = normalizePathForCompare;
|
|
8
|
+
exports.arePathsEqual = arePathsEqual;
|
|
9
|
+
exports.isPathInside = isPathInside;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
4
11
|
/**
|
|
5
12
|
* Generates a filesystem-safe directory name from an absolute path
|
|
6
13
|
* @param workingPath - The absolute working path to transform
|
|
@@ -26,3 +33,27 @@ function generateFilesystemSafeId(workingPath) {
|
|
|
26
33
|
}
|
|
27
34
|
return safeId;
|
|
28
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a filesystem path for cross-platform string comparison.
|
|
38
|
+
* - Resolves to absolute path
|
|
39
|
+
* - Normalizes separators to forward slashes
|
|
40
|
+
* - Lowercases on Windows where paths are case-insensitive
|
|
41
|
+
*/
|
|
42
|
+
function normalizePathForCompare(inputPath) {
|
|
43
|
+
const normalized = path_1.default.resolve(inputPath).replace(/\\/g, '/');
|
|
44
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when both paths resolve to the same normalized filesystem path.
|
|
48
|
+
*/
|
|
49
|
+
function arePathsEqual(pathA, pathB) {
|
|
50
|
+
return normalizePathForCompare(pathA) === normalizePathForCompare(pathB);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* True when childPath is equal to or within parentPath.
|
|
54
|
+
*/
|
|
55
|
+
function isPathInside(childPath, parentPath) {
|
|
56
|
+
const normalizedChild = normalizePathForCompare(childPath);
|
|
57
|
+
const normalizedParent = normalizePathForCompare(parentPath);
|
|
58
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}/`);
|
|
59
|
+
}
|