handoff-app 1.2.2-6 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +75 -0
- package/Changelog.md +37 -15
- package/dist/app-builder/build.js +2 -0
- package/dist/app-builder/watchers/utils.d.ts +2 -0
- package/dist/app-builder/watchers.d.ts +14 -4
- package/dist/app-builder/watchers.js +123 -28
- package/dist/cache/build-cache.js +20 -2
- package/dist/config/runtime.js +6 -1
- package/package.json +1 -1
- package/src/app/components/Component/ComponentLists.tsx +102 -102
- package/src/app/components/NotFound.tsx +24 -0
- package/src/app/pages/[...slug]/index.tsx +2 -16
- package/src/app/pages/system/pattern/[pattern]/index.tsx +31 -2
- package/src/app-builder/build.ts +3 -0
- package/src/app-builder/watchers/utils.ts +2 -0
- package/src/app-builder/watchers.ts +149 -25
- package/src/cache/build-cache.ts +17 -0
- package/src/config/runtime.ts +6 -1
- package/handoff.config.js +0 -90
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read(//Users/domagojgojak/.claude/skills/to-prd/**)",
|
|
5
|
+
"Read(//Users/domagojgojak/.claude/skills/**)",
|
|
6
|
+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('name:',d.get\\('name'\\)\\); print\\('version:',d.get\\('version'\\)\\); print\\('bin:',json.dumps\\(d.get\\('bin'\\),indent=2\\)\\); print\\('main:',d.get\\('main'\\)\\); print\\('scripts:',json.dumps\\(d.get\\('scripts'\\),indent=2\\)\\)\")",
|
|
7
|
+
"Bash(python3 -c \"import json,sys;d=json.load\\(sys.stdin\\);dd={**d.get\\('dependencies',{}\\),**d.get\\('devDependencies',{}\\)};print\\([k for k in dd if any\\(t in k for t in ['jest','vitest','playwright','mocha','supertest','@testing']\\)] or 'none'\\)\")",
|
|
8
|
+
"Bash(python3 -c \"import json,sys;d=json.load\\(sys.stdin\\);print\\(d.get\\('files','\\(no files field\\)'\\)\\)\")",
|
|
9
|
+
"Bash(npx tsc *)",
|
|
10
|
+
"Bash(npx next *)",
|
|
11
|
+
"Bash(node *)",
|
|
12
|
+
"Read(//Users/domagojgojak/source/repos/convertiv/handoff-demo/**)",
|
|
13
|
+
"Bash(npm run *)",
|
|
14
|
+
"Bash(git -C /Users/domagojgojak/source/repos/convertiv/handoff-app status --short)",
|
|
15
|
+
"Bash(git -C /Users/domagojgojak/source/repos/convertiv/handoff-app diff --stat)",
|
|
16
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/badge.css)",
|
|
17
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/badge-primary.html)",
|
|
18
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/badge.json)",
|
|
19
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/main.js)",
|
|
20
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/pattern/demo.html)",
|
|
21
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/assets/script-l0sNRNKZ.js)",
|
|
22
|
+
"Bash(curl -s -w ' <-HTTP %{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component/../components.json)",
|
|
23
|
+
"Bash(curl -s -w ' <-HTTP %{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component/%2E%2E/secret)",
|
|
24
|
+
"Bash(curl -s -w ' <-HTTP %{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component/nope.css)",
|
|
25
|
+
"Bash(curl -s -o /dev/null -w 'HTTP %{http_code} CT %{content_type}\\\\n' http://localhost:3987/api/docs/artifacts/component/probe.txt)",
|
|
26
|
+
"Bash(curl -s --path-as-is -o /dev/null -w '%{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/%2E%2E/%2E%2E/secret)",
|
|
27
|
+
"Bash(curl -s --path-as-is -o /dev/null -w '%{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component/%2E%2E)",
|
|
28
|
+
"Bash(curl -s --path-as-is -o /dev/null -w '%{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component%2F..%2Fcomponents.json)",
|
|
29
|
+
"Bash(curl -s --path-as-is http://localhost:3987/api/docs/artifacts/component/nope.css)",
|
|
30
|
+
"Bash(curl -s --path-as-is -o /dev/null -w '%{http_code}\\\\n' http://localhost:3987/api/docs/artifacts/component/../../components.json)",
|
|
31
|
+
"Bash(curl -s -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/previews/primary)",
|
|
32
|
+
"Bash(curl -s -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/inspect/primary)",
|
|
33
|
+
"Bash(curl -s -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/assets/style)",
|
|
34
|
+
"Bash(curl -s -w ' %{http_code}\\\\n' http://localhost:3987/api/docs/components/badge/assets/client)",
|
|
35
|
+
"Bash(curl -s -w ' %{http_code}\\\\n' http://localhost:3987/api/docs/components/badge/assets/bogus)",
|
|
36
|
+
"Bash(curl -sL -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/previews/primary/)",
|
|
37
|
+
"Bash(curl -sL -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/inspect/primary/)",
|
|
38
|
+
"Bash(curl -sL -o /dev/null -w '%{http_code} %{content_type}\\\\n' http://localhost:3987/api/docs/components/badge/assets/style/)",
|
|
39
|
+
"Bash(curl -sL -w ' %{http_code}\\\\n' http://localhost:3987/api/docs/components/badge/assets/client/)",
|
|
40
|
+
"Bash(curl -sL -w ' %{http_code}\\\\n' http://localhost:3987/api/docs/components/badge/assets/bogus/)",
|
|
41
|
+
"Bash(curl -s http://localhost:3987/api/docs/components.json)",
|
|
42
|
+
"Bash(grep -v \"deprecated\\\\|create-next-app\\\\|migrate\\\\|codemod\\\\|^$\\\\|Disabling\\\\|disable some\\\\|Learn more\\\\|ESLint CLI\")",
|
|
43
|
+
"Bash(grep -rn \"['\\\\\"\\\\`].*api/\\\\\\(component\\\\|pattern\\\\|components\\\\|patterns\\\\\\)\" src/app src/templates)",
|
|
44
|
+
"Bash(echo \"EXIT:$?\")",
|
|
45
|
+
"Bash(git log *)",
|
|
46
|
+
"Bash(echo \"EXIT: $?\")",
|
|
47
|
+
"WebSearch",
|
|
48
|
+
"Bash(python3 -m json.tool)",
|
|
49
|
+
"Bash(find . -maxdepth 3 -name 'next-server.js')",
|
|
50
|
+
"Bash(find . -maxdepth 2 -name 'node_modules' -type d)",
|
|
51
|
+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('basePath:',d.get\\('basePath'\\)\\); print\\('dynamicRoutes:',[r['page'] for r in d.get\\('dynamicRoutes',[]\\)]\\); print\\('staticRoutes:',[r['page'] for r in d.get\\('staticRoutes',[]\\)][:20]\\)\")",
|
|
52
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
53
|
+
"Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\('runtime:', json.dumps\\(d.get\\('runtime'\\)\\)\\)\")",
|
|
54
|
+
"Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\('appDir/relativeAppDir:', d.get\\('relativeAppDir'\\)\\); print\\('config.distDir:', d['config'].get\\('distDir'\\)\\); print\\('outputFileTracingRoot:', d['config'].get\\('outputFileTracingRoot'\\)\\)\")",
|
|
55
|
+
"Bash(HOME=/tmp node -e \"try { require\\('./index.js'\\); console.log\\('LOADED OK'\\); } catch\\(e\\){ console.log\\('COLD-START CRASH:', e.code||'', e.message.split\\('\\\\n'\\)[0]\\); }\")",
|
|
56
|
+
"Bash(cp tsconfig.json /tmp/_tsconfig.bak)",
|
|
57
|
+
"Bash(python3 -c ' *)",
|
|
58
|
+
"Bash(npm pack *)",
|
|
59
|
+
"Bash(python3 -c \"import json; print\\(json.dumps\\(json.load\\(open\\('package.json'\\)\\).get\\('scripts',{}\\),indent=2\\)\\)\")",
|
|
60
|
+
"Bash(python3 -)",
|
|
61
|
+
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/)",
|
|
62
|
+
"Bash(pkill -f \"handoff-app dev\")",
|
|
63
|
+
"Bash(pkill -f \"next dev\")",
|
|
64
|
+
"Bash(rm -f /tmp/demo_*.html /tmp/handoff-demo-dev.log)",
|
|
65
|
+
"Bash(grep -vE \"postman|package-lock|package.json$\")",
|
|
66
|
+
"Bash(mv ./handoff-app-2.0.0.tgz ../handoff-demo/packages/)",
|
|
67
|
+
"Bash(rm -rf node_modules/handoff-app node_modules/.cache)",
|
|
68
|
+
"Bash(npm install *)",
|
|
69
|
+
"Bash(echo \"exit: $?\")",
|
|
70
|
+
"Bash(echo \"---exit $?---\")",
|
|
71
|
+
"Bash(git config *)",
|
|
72
|
+
"Bash(echo \"---exit: $?\")"
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
package/Changelog.md
CHANGED
|
@@ -6,19 +6,42 @@ 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.2.2] -
|
|
9
|
+
## \[1.2.2] - TBD
|
|
10
10
|
|
|
11
|
-
This patch release
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
This patch release improves watch-mode reliability for patterns, broadens slot
|
|
12
|
+
default typing, clears downstream lint/build compatibility issues, and fixes a
|
|
13
|
+
few release-branch UI and dependency issues.
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### Added
|
|
16
16
|
|
|
17
|
-
*
|
|
18
|
-
|
|
17
|
+
* Added `watchPatternDirectories` so newly created pattern subdirectories are
|
|
18
|
+
detected during `start` without restarting the dev server.
|
|
19
|
+
* Added a shared `NotFound` component and reused it for stale pattern URLs and
|
|
20
|
+
missing markdown pages.
|
|
19
21
|
|
|
20
|
-
###
|
|
22
|
+
### Changed
|
|
21
23
|
|
|
24
|
+
* Broadened `SlotMetadata.default` so slot defaults can be strings, numbers,
|
|
25
|
+
booleans, objects, arrays, or `null`.
|
|
26
|
+
* Refactored component and pattern directory watching through a shared
|
|
27
|
+
`watchEntityDirectories` helper.
|
|
28
|
+
* Downgraded incomplete or mid-edit pattern declarations from hard parse errors
|
|
29
|
+
to warnings, allowing watch mode to retry automatically after the file is
|
|
30
|
+
saved with valid content.
|
|
31
|
+
* Removed unneeded theme declarations.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
* Fixed watch-mode rebuilds for newly added patterns by rebuilding referenced
|
|
36
|
+
component previews before composing the new pattern.
|
|
37
|
+
* Fixed pattern watcher edge cases where declaration IDs differ from directory
|
|
38
|
+
names, and where an initially incomplete pattern config later becomes valid.
|
|
39
|
+
* Fixed `.handoff.ts` declaration files not being detected by the new-entity
|
|
40
|
+
config-file gate.
|
|
41
|
+
* Extended component build-cache inputs to include pattern config files that
|
|
42
|
+
reference the component, preventing stale preview output after pattern changes.
|
|
43
|
+
* Fixed select/list handling when a component group or category value is empty.
|
|
44
|
+
* Fixed CSS typing issues and app build issues found during the release branch.
|
|
22
45
|
* Resolved `no-else-return`, `no-unused-vars`, `camelcase`, `prefer-const`,
|
|
23
46
|
`eqeqeq`, `no-nested-ternary`, `no-lonely-if`, and `no-use-before-define`
|
|
24
47
|
across static pages (catch-all docs, assets, foundations, home, system, and
|
|
@@ -26,17 +49,16 @@ enabling `--downlevelIteration`.
|
|
|
26
49
|
* Removed or refactored unused `getStaticProps` context parameters, props, and
|
|
27
50
|
locals; normalized naming (e.g. font machine keys); simplified icon detail
|
|
28
51
|
routing query handling.
|
|
29
|
-
* Reordered helper components
|
|
30
|
-
|
|
52
|
+
* Reordered helper components ahead of page components where required for
|
|
53
|
+
declaration order.
|
|
31
54
|
* Converted `getComponentPreviews` to a hoisted `function` declaration and
|
|
32
55
|
refactored the component token table for clearer control flow.
|
|
56
|
+
* Avoided iterating `Map`, `Set`, `Iterable`, and `matchAll` results directly in
|
|
57
|
+
`for...of` where the compile target requires `--downlevelIteration`.
|
|
33
58
|
|
|
34
|
-
###
|
|
59
|
+
### Security
|
|
35
60
|
|
|
36
|
-
*
|
|
37
|
-
`for...of` where the compile target requires it: use `Array.from(...)` or
|
|
38
|
-
equivalent helpers in the config-diff registry, snapshot diff, CSF import
|
|
39
|
-
parsing, and component artifact sync.
|
|
61
|
+
* Updated Vite to address a security issue.
|
|
40
62
|
|
|
41
63
|
## \[1.2.1] - 2026-04-08
|
|
42
64
|
|
|
@@ -196,11 +196,13 @@ const watchApp = (handoff) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
196
196
|
runtimeComponentsWatcher: null,
|
|
197
197
|
runtimeConfigurationWatcher: null,
|
|
198
198
|
componentDirectoriesWatcher: null,
|
|
199
|
+
patternDirectoriesWatcher: null,
|
|
199
200
|
};
|
|
200
201
|
(0, watchers_1.watchPublicDirectory)(handoff, wss, state, chokidarConfig);
|
|
201
202
|
(0, watchers_1.watchRuntimeComponents)(handoff, state, (0, watchers_1.getRuntimeComponentsPathsToWatch)(handoff));
|
|
202
203
|
(0, watchers_1.watchRuntimeConfiguration)(handoff, state);
|
|
203
204
|
(0, watchers_1.watchComponentDirectories)(handoff, state, chokidarConfig);
|
|
205
|
+
(0, watchers_1.watchPatternDirectories)(handoff, state, chokidarConfig);
|
|
204
206
|
(0, watchers_1.watchGlobalEntries)(handoff, state, chokidarConfig);
|
|
205
207
|
(0, watchers_1.watchPages)(handoff, chokidarConfig);
|
|
206
208
|
});
|
|
@@ -5,6 +5,8 @@ export interface WatcherState {
|
|
|
5
5
|
runtimeComponentsWatcher: chokidar.FSWatcher | null;
|
|
6
6
|
runtimeConfigurationWatcher: chokidar.FSWatcher | null;
|
|
7
7
|
componentDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
8
|
+
/** Watches entries.patterns parent directories for newly created pattern subdirectories. */
|
|
9
|
+
patternDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Schedules a handler for execution with mutual exclusion and per-source
|
|
@@ -36,9 +36,19 @@ export declare const watchRuntimeComponents: (handoff: Handoff, state: WatcherSt
|
|
|
36
36
|
*/
|
|
37
37
|
export declare const watchRuntimeConfiguration: (handoff: Handoff, state: WatcherState) => void;
|
|
38
38
|
/**
|
|
39
|
-
* Watches the parent component directories from config.entries.components
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Watches the parent component directories from config.entries.components for
|
|
40
|
+
* new components being added. When a new config file appears in a new
|
|
41
|
+
* subdirectory, reloads the runtime config, restarts the component and
|
|
42
|
+
* configuration watchers, then builds the new component and any patterns that
|
|
43
|
+
* reference it.
|
|
43
44
|
*/
|
|
44
45
|
export declare const watchComponentDirectories: (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Watches the parent pattern directories from config.entries.patterns for new
|
|
48
|
+
* patterns being added. The rebuild sequence intentionally differs from
|
|
49
|
+
* watchComponentDirectories because the dependency arrow is reversed: a new
|
|
50
|
+
* pattern references existing components, so we rebuild only the preview
|
|
51
|
+
* segment of those components (to produce the __pattern_* HTML files that
|
|
52
|
+
* injectPatternPreviews registered) before composing the pattern itself.
|
|
53
|
+
*/
|
|
54
|
+
export declare const watchPatternDirectories: (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => void;
|
|
@@ -35,10 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
36
36
|
};
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.watchComponentDirectories = exports.watchRuntimeConfiguration = exports.watchRuntimeComponents = exports.watchGlobalEntries = exports.watchPages = exports.watchAppSource = exports.watchPublicDirectory = exports.mapEntryTypeToSegment = exports.getRuntimeComponentsPathsToWatch = void 0;
|
|
38
|
+
exports.watchPatternDirectories = exports.watchComponentDirectories = exports.watchRuntimeConfiguration = exports.watchRuntimeComponents = exports.watchGlobalEntries = exports.watchPages = exports.watchAppSource = exports.watchPublicDirectory = exports.mapEntryTypeToSegment = exports.getRuntimeComponentsPathsToWatch = void 0;
|
|
39
39
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
40
40
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
41
41
|
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const patterns_1 = require("../pipeline/patterns");
|
|
42
43
|
const builder_1 = __importStar(require("../transformers/preview/component/builder"));
|
|
43
44
|
const css_1 = require("../transformers/preview/component/css");
|
|
44
45
|
const javascript_1 = require("../transformers/preview/component/javascript");
|
|
@@ -226,6 +227,15 @@ const watchRuntimeComponents = (handoff, state, runtimeComponentPathsToWatch) =>
|
|
|
226
227
|
}
|
|
227
228
|
};
|
|
228
229
|
exports.watchRuntimeComponents = watchRuntimeComponents;
|
|
230
|
+
const rebuildPatternComponentPreviews = (handoff, patternId) => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
var _a, _b, _c, _d;
|
|
232
|
+
const pattern = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.patterns) === null || _c === void 0 ? void 0 : _c[patternId];
|
|
233
|
+
if (!((_d = pattern === null || pattern === void 0 ? void 0 : pattern.components) === null || _d === void 0 ? void 0 : _d.length))
|
|
234
|
+
return;
|
|
235
|
+
for (const ref of pattern.components) {
|
|
236
|
+
yield (0, builder_1.default)(handoff, ref.id, builder_1.ComponentSegment.Previews);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
229
239
|
/**
|
|
230
240
|
* Watches the runtime configuration for changes.
|
|
231
241
|
*
|
|
@@ -269,6 +279,9 @@ const watchRuntimeConfiguration = (handoff, state) => {
|
|
|
269
279
|
else if (handle) {
|
|
270
280
|
yield handle.apply(handoff, entryAfter === null || entryAfter === void 0 ? void 0 : entryAfter.entityId);
|
|
271
281
|
}
|
|
282
|
+
else if ((entryAfter === null || entryAfter === void 0 ? void 0 : entryAfter.kind) === 'pattern') {
|
|
283
|
+
yield rebuildPatternComponentPreviews(handoff, entryAfter.entityId);
|
|
284
|
+
}
|
|
272
285
|
else {
|
|
273
286
|
const effectiveId = (_a = entryAfter === null || entryAfter === void 0 ? void 0 : entryAfter.entityId) !== null && _a !== void 0 ? _a : path_1.default.basename(path_1.default.dirname(changedFile));
|
|
274
287
|
yield (0, builder_1.default)(handoff, effectiveId);
|
|
@@ -286,56 +299,138 @@ const watchRuntimeConfiguration = (handoff, state) => {
|
|
|
286
299
|
};
|
|
287
300
|
exports.watchRuntimeConfiguration = watchRuntimeConfiguration;
|
|
288
301
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
302
|
+
* Shared factory for watching a parent directory for newly created entity
|
|
303
|
+
* subdirectories (components or patterns). Handles the common scaffolding so
|
|
304
|
+
* that callers only describe what differs for their entity kind:
|
|
305
|
+
*
|
|
306
|
+
* - which config-entry paths to watch
|
|
307
|
+
* - how to read the current known ids after a reload
|
|
308
|
+
* - which WatcherState slot to use
|
|
309
|
+
* - a log label for the entity kind
|
|
310
|
+
* - the rebuild work to run once the runtime config is fresh
|
|
293
311
|
*/
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
312
|
+
const watchEntityDirectories = (handoff, state, chokidarConfig, options) => {
|
|
313
|
+
const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options;
|
|
314
|
+
const existingWatcher = getWatcher(state);
|
|
315
|
+
if (existingWatcher) {
|
|
316
|
+
existingWatcher.close();
|
|
298
317
|
}
|
|
299
|
-
const
|
|
300
|
-
if (
|
|
318
|
+
const configPaths = getConfigPaths(handoff);
|
|
319
|
+
if (configPaths.length === 0)
|
|
301
320
|
return;
|
|
302
321
|
const dirsToWatch = [];
|
|
303
|
-
for (const
|
|
304
|
-
const resolved = path_1.default.resolve(handoff.workingPath,
|
|
322
|
+
for (const configPath of configPaths) {
|
|
323
|
+
const resolved = path_1.default.resolve(handoff.workingPath, configPath);
|
|
305
324
|
if (fs_extra_1.default.existsSync(resolved) && fs_extra_1.default.statSync(resolved).isDirectory()) {
|
|
306
325
|
dirsToWatch.push(resolved);
|
|
307
326
|
}
|
|
308
327
|
}
|
|
309
328
|
if (dirsToWatch.length === 0)
|
|
310
329
|
return;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
330
|
+
// Snapshot known ids at watch startup so we only react to genuinely new
|
|
331
|
+
// entities, not to file changes inside already-known directories.
|
|
332
|
+
const knownIds = new Set(getKnownIds(handoff));
|
|
333
|
+
const watcher = chokidar_1.default.watch(dirsToWatch, Object.assign(Object.assign({}, chokidarConfig), { depth: 1 }));
|
|
334
|
+
setWatcher(state, watcher);
|
|
335
|
+
watcher.on('add', (file) => __awaiter(void 0, void 0, void 0, function* () {
|
|
314
336
|
const basename = path_1.default.basename(file);
|
|
315
337
|
const dirName = path_1.default.basename(path_1.default.dirname(file));
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
338
|
+
// Only react to the primary declaration file for the directory (e.g.
|
|
339
|
+
// button.json, button.js, or button.handoff.ts inside a button/ subdir).
|
|
340
|
+
// The basename.startsWith(dirName) guard below ensures .ts here only
|
|
341
|
+
// ever matches files named after their parent directory, not arbitrary
|
|
342
|
+
// TypeScript source files.
|
|
343
|
+
const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs') || basename.endsWith('.ts');
|
|
344
|
+
const isNewEntity = isConfigFile && basename.startsWith(dirName) && !knownIds.has(dirName);
|
|
345
|
+
if (!isNewEntity)
|
|
319
346
|
return;
|
|
320
|
-
yield (0, utils_1.scheduleHandler)(state,
|
|
321
|
-
var _a, _b, _c;
|
|
347
|
+
yield (0, utils_1.scheduleHandler)(state, `${scheduleKeyPrefix}:${dirName}`, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
322
348
|
try {
|
|
323
|
-
logger_1.Logger.warn(`New
|
|
349
|
+
logger_1.Logger.warn(`New ${entityLabel} detected: ${dirName}. Reloading configuration...`);
|
|
350
|
+
// Snapshot ids before reload so we can tell whether the new entity
|
|
351
|
+
// was successfully registered afterwards.
|
|
352
|
+
const idsBefore = new Set(getKnownIds(handoff));
|
|
324
353
|
handoff.reload();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
354
|
+
// Refresh from the post-reload runtime config so any ids that were
|
|
355
|
+
// discovered alongside the new entity are also marked as known.
|
|
356
|
+
const idsAfter = getKnownIds(handoff);
|
|
357
|
+
const addedIds = idsAfter.filter((id) => !idsBefore.has(id));
|
|
358
|
+
for (const id of idsAfter) {
|
|
359
|
+
knownIds.add(id);
|
|
328
360
|
}
|
|
329
361
|
yield (0, client_config_1.persistClientConfig)(handoff);
|
|
362
|
+
// Re-register file watchers so the new config file and any runtime
|
|
363
|
+
// files it introduces are covered going forward. This must happen even
|
|
364
|
+
// when the file failed to parse so that watchRuntimeConfiguration can
|
|
365
|
+
// trigger a retry the moment the file is saved with valid content.
|
|
330
366
|
(0, exports.watchRuntimeComponents)(handoff, state, (0, component_1.getRuntimeComponentsPathsToWatch)(handoff));
|
|
331
367
|
(0, exports.watchRuntimeConfiguration)(handoff, state);
|
|
332
|
-
|
|
333
|
-
|
|
368
|
+
// If no new ids appeared after reload the file was empty or invalid.
|
|
369
|
+
// Skip the build — watchRuntimeConfiguration will fire when it is saved.
|
|
370
|
+
if (addedIds.length === 0) {
|
|
371
|
+
logger_1.Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
yield onDetected(handoff, { dirName, addedIds });
|
|
334
375
|
}
|
|
335
376
|
catch (e) {
|
|
336
|
-
logger_1.Logger.error(
|
|
377
|
+
logger_1.Logger.error(`Error processing new ${entityLabel}:`, e);
|
|
337
378
|
}
|
|
338
379
|
}));
|
|
339
380
|
}));
|
|
340
381
|
};
|
|
382
|
+
/**
|
|
383
|
+
* Watches the parent component directories from config.entries.components for
|
|
384
|
+
* new components being added. When a new config file appears in a new
|
|
385
|
+
* subdirectory, reloads the runtime config, restarts the component and
|
|
386
|
+
* configuration watchers, then builds the new component and any patterns that
|
|
387
|
+
* reference it.
|
|
388
|
+
*/
|
|
389
|
+
const watchComponentDirectories = (handoff, state, chokidarConfig) => {
|
|
390
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
391
|
+
getConfigPaths: (h) => { var _a, _b; return (_b = (_a = h.config.entries) === null || _a === void 0 ? void 0 : _a.components) !== null && _b !== void 0 ? _b : []; },
|
|
392
|
+
getKnownIds: (h) => { var _a, _b, _c; return Object.keys((_c = (_b = (_a = h.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) !== null && _c !== void 0 ? _c : {}); },
|
|
393
|
+
getWatcher: (s) => s.componentDirectoriesWatcher,
|
|
394
|
+
setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; },
|
|
395
|
+
scheduleKeyPrefix: 'newComponent',
|
|
396
|
+
entityLabel: 'component',
|
|
397
|
+
onDetected: (handoff_1, _a) => __awaiter(void 0, [handoff_1, _a], void 0, function* (handoff, { addedIds }) {
|
|
398
|
+
for (const componentId of addedIds) {
|
|
399
|
+
yield (0, builder_1.default)(handoff, componentId);
|
|
400
|
+
yield (0, config_diff_1.runAllFinalizers)(handoff, { patternRebuildComponentIds: [componentId] });
|
|
401
|
+
}
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
};
|
|
341
405
|
exports.watchComponentDirectories = watchComponentDirectories;
|
|
406
|
+
/**
|
|
407
|
+
* Watches the parent pattern directories from config.entries.patterns for new
|
|
408
|
+
* patterns being added. The rebuild sequence intentionally differs from
|
|
409
|
+
* watchComponentDirectories because the dependency arrow is reversed: a new
|
|
410
|
+
* pattern references existing components, so we rebuild only the preview
|
|
411
|
+
* segment of those components (to produce the __pattern_* HTML files that
|
|
412
|
+
* injectPatternPreviews registered) before composing the pattern itself.
|
|
413
|
+
*/
|
|
414
|
+
const watchPatternDirectories = (handoff, state, chokidarConfig) => {
|
|
415
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
416
|
+
getConfigPaths: (h) => { var _a, _b; return (_b = (_a = h.config.entries) === null || _a === void 0 ? void 0 : _a.patterns) !== null && _b !== void 0 ? _b : []; },
|
|
417
|
+
getKnownIds: (h) => { var _a, _b, _c; return Object.keys((_c = (_b = (_a = h.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.patterns) !== null && _c !== void 0 ? _c : {}); },
|
|
418
|
+
getWatcher: (s) => s.patternDirectoriesWatcher,
|
|
419
|
+
setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; },
|
|
420
|
+
scheduleKeyPrefix: 'newPattern',
|
|
421
|
+
entityLabel: 'pattern',
|
|
422
|
+
onDetected: (handoff_1, _a) => __awaiter(void 0, [handoff_1, _a], void 0, function* (handoff, { addedIds }) {
|
|
423
|
+
// Rebuild only the previews segment for each component the new pattern
|
|
424
|
+
// references. Their JS/CSS/structure are unchanged — only the new
|
|
425
|
+
// synthetic preview HTML files need to be generated so that buildPatterns
|
|
426
|
+
// can assemble the pattern from them.
|
|
427
|
+
for (const patternId of addedIds) {
|
|
428
|
+
yield rebuildPatternComponentPreviews(handoff, patternId);
|
|
429
|
+
}
|
|
430
|
+
// Build only the new pattern directly rather than going through
|
|
431
|
+
// runAllFinalizers, which would rebuild all patterns unnecessarily.
|
|
432
|
+
yield (0, patterns_1.buildPatterns)(handoff, { onlyPatternIds: new Set(addedIds) });
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
};
|
|
436
|
+
exports.watchPatternDirectories = watchPatternDirectories;
|
|
@@ -136,7 +136,7 @@ function haveGlobalDepsChanged(cached, current) {
|
|
|
136
136
|
* Gets all file paths that should be tracked for a component
|
|
137
137
|
*/
|
|
138
138
|
function getComponentFilePaths(handoff, componentId) {
|
|
139
|
-
var _a, _b, _c;
|
|
139
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
140
140
|
const runtimeComponent = (_c = (_b = (_a = handoff.runtimeConfig) === null || _a === void 0 ? void 0 : _a.entries) === null || _b === void 0 ? void 0 : _b.components) === null || _c === void 0 ? void 0 : _c[componentId];
|
|
141
141
|
if (!runtimeComponent) {
|
|
142
142
|
return { files: [] };
|
|
@@ -169,7 +169,7 @@ function getComponentFilePaths(handoff, componentId) {
|
|
|
169
169
|
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(templatePath)));
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
-
catch (
|
|
172
|
+
catch (_h) {
|
|
173
173
|
// File doesn't exist, still add to track
|
|
174
174
|
files.push(templatePath);
|
|
175
175
|
componentDirs.add((0, path_2.normalizePathForCompare)(path_1.default.dirname(templatePath)));
|
|
@@ -202,6 +202,24 @@ function getComponentFilePaths(handoff, componentId) {
|
|
|
202
202
|
if (matchingConfigPath) {
|
|
203
203
|
files.push(matchingConfigPath);
|
|
204
204
|
}
|
|
205
|
+
// Include every pattern config file whose declaration references this component.
|
|
206
|
+
// When a pattern is added or modified, injectPatternPreviews injects new
|
|
207
|
+
// __pattern_* synthetic preview keys onto the referenced component, changing
|
|
208
|
+
// what processComponents must render. Without tracking these files here, the
|
|
209
|
+
// cache would incorrectly consider the component unchanged and skip the preview
|
|
210
|
+
// rebuild, leaving buildPatterns unable to find the required HTML fragments.
|
|
211
|
+
const runtimePatterns = (_f = (_e = (_d = handoff.runtimeConfig) === null || _d === void 0 ? void 0 : _d.entries) === null || _e === void 0 ? void 0 : _e.patterns) !== null && _f !== void 0 ? _f : {};
|
|
212
|
+
for (const configPath of configPaths) {
|
|
213
|
+
const entry = handoff.getConfigFileEntry(configPath);
|
|
214
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.kind) !== 'pattern')
|
|
215
|
+
continue;
|
|
216
|
+
const pattern = runtimePatterns[entry.entityId];
|
|
217
|
+
if (!pattern)
|
|
218
|
+
continue;
|
|
219
|
+
if ((_g = pattern.components) === null || _g === void 0 ? void 0 : _g.some((ref) => ref.id === componentId)) {
|
|
220
|
+
files.push(configPath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
205
223
|
return { files, templateDir };
|
|
206
224
|
}
|
|
207
225
|
/**
|
package/dist/config/runtime.js
CHANGED
|
@@ -220,7 +220,12 @@ const initRuntimeConfig = (handoff) => {
|
|
|
220
220
|
});
|
|
221
221
|
}
|
|
222
222
|
catch (err) {
|
|
223
|
-
|
|
223
|
+
// Treat as a warning rather than an error: the file may be empty or
|
|
224
|
+
// mid-edit. It is already tracked in configFiles above, so
|
|
225
|
+
// watchRuntimeConfiguration will pick it up once the file is saved
|
|
226
|
+
// with valid content.
|
|
227
|
+
logger_1.Logger.warn(`Pattern config skipped (incomplete or invalid) — will retry on next save: ${resolvedPatternConfigPath}`);
|
|
228
|
+
logger_1.Logger.debug(`Pattern parse detail:`, err);
|
|
224
229
|
continue;
|
|
225
230
|
}
|
|
226
231
|
configFileIndex.set((0, path_2.normalizePathForCompare)(resolvedPatternConfigPath), { kind: 'pattern', entityId: pattern.id });
|
package/package.json
CHANGED
|
@@ -76,114 +76,114 @@ export const ComponentList = ({
|
|
|
76
76
|
return (
|
|
77
77
|
<TooltipProvider delayDuration={0}>
|
|
78
78
|
<div className="mx-auto w-full mb-4">
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
<div className="flex justify-between bg-accent rounded-xl mb-8">
|
|
80
|
+
<div className="mr-auto flex items-center gap-3 bg-accent p-2.5 rounded-md">
|
|
81
|
+
<div className="relative">
|
|
82
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-400" />
|
|
83
|
+
<Input
|
|
84
|
+
type="text"
|
|
85
|
+
placeholder="Search components..."
|
|
86
|
+
className="rounded-md pl-9 pr-3 py-1 text-sm border-none shadow-none"
|
|
87
|
+
onChange={e => setSearch(e.currentTarget.value)}
|
|
88
|
+
aria-label="Search components"
|
|
89
|
+
style={{ minWidth: 200 }}
|
|
90
|
+
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
93
|
|
|
94
|
-
</div>
|
|
95
|
-
<div className="flex justify-end p-2.5 gap-2.5">
|
|
96
|
-
<div className="flex items-center gap-0.5 border border-input bg-background shadow-xs hover:text-accent-foreground p-0.5 rounded-md">
|
|
97
|
-
<Select
|
|
98
|
-
value={category}
|
|
99
|
-
onValueChange={val => {
|
|
100
|
-
setCategory(val);
|
|
101
|
-
}}
|
|
102
|
-
aria-label="Filter by category"
|
|
103
|
-
>
|
|
104
|
-
<SelectTrigger className="rounded-md [&_span]:text-xs [&_svg]:ml-2.5 [&_svg]:size-3 py-1 px-2 pl-3 text-sm border-none shadow-none bg-transparent focus:border-blue-400 focus:outline-none">
|
|
105
|
-
<SelectValue placeholder="All Categories" />
|
|
106
|
-
</SelectTrigger>
|
|
107
|
-
<SelectContent>
|
|
108
|
-
<SelectItem value="all">All Categories</SelectItem>
|
|
109
|
-
{categories.map((category) => (
|
|
110
|
-
<SelectItem key={category} value={category}>
|
|
111
|
-
{category}
|
|
112
|
-
</SelectItem>
|
|
113
|
-
))}
|
|
114
|
-
</SelectContent>
|
|
115
|
-
</Select>
|
|
116
|
-
|
|
117
|
-
<hr className="h-7 w-px bg-input" />
|
|
118
|
-
|
|
119
|
-
<Tooltip>
|
|
120
|
-
<TooltipTrigger asChild>
|
|
121
|
-
<Button variant="ghost" className={cn("w-9", groupBy && 'bg-accent text-accent-foreground')} onClick={() => setGroupBy(!groupBy)}>
|
|
122
|
-
<Rows className="size-3.5" strokeWidth={1.5} />
|
|
123
|
-
<span className="sr-only">Group by category</span>
|
|
124
|
-
</Button>
|
|
125
|
-
</TooltipTrigger>
|
|
126
|
-
<TooltipContent>
|
|
127
|
-
Group by category
|
|
128
|
-
</TooltipContent>
|
|
129
|
-
</Tooltip>
|
|
130
94
|
</div>
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
95
|
+
<div className="flex justify-end p-2.5 gap-2.5">
|
|
96
|
+
<div className="flex items-center gap-0.5 border border-input bg-background shadow-xs hover:text-accent-foreground p-0.5 rounded-md">
|
|
97
|
+
<Select
|
|
98
|
+
value={category}
|
|
99
|
+
onValueChange={val => {
|
|
100
|
+
setCategory(val);
|
|
101
|
+
}}
|
|
102
|
+
aria-label="Filter by category"
|
|
103
|
+
>
|
|
104
|
+
<SelectTrigger className="rounded-md [&_span]:text-xs [&_svg]:ml-2.5 [&_svg]:size-3 py-1 px-2 pl-3 text-sm border-none shadow-none bg-transparent focus:border-blue-400 focus:outline-none">
|
|
105
|
+
<SelectValue placeholder="All Categories" />
|
|
106
|
+
</SelectTrigger>
|
|
107
|
+
<SelectContent>
|
|
108
|
+
<SelectItem value="all">All Categories</SelectItem>
|
|
109
|
+
{categories.filter((category) => category !== '').map((category) => (
|
|
110
|
+
<SelectItem key={category} value={category}>
|
|
111
|
+
{category}
|
|
112
|
+
</SelectItem>
|
|
113
|
+
))}
|
|
114
|
+
</SelectContent>
|
|
115
|
+
</Select>
|
|
116
|
+
|
|
117
|
+
<hr className="h-7 w-px bg-input" />
|
|
118
|
+
|
|
119
|
+
<Tooltip>
|
|
120
|
+
<TooltipTrigger asChild>
|
|
121
|
+
<Button variant="ghost" className={cn("w-9", groupBy && 'bg-accent text-accent-foreground')} onClick={() => setGroupBy(!groupBy)}>
|
|
122
|
+
<Rows className="size-3.5" strokeWidth={1.5} />
|
|
123
|
+
<span className="sr-only">Group by category</span>
|
|
124
|
+
</Button>
|
|
125
|
+
</TooltipTrigger>
|
|
126
|
+
<TooltipContent>
|
|
127
|
+
Group by category
|
|
128
|
+
</TooltipContent>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
</div>
|
|
131
|
+
<ToggleGroup className='flex gap-0.5 border border-input bg-background shadow-xs hover:text-accent-foreground p-0.5 rounded-md' type="single" value={layout} onValueChange={(value) => storeLayout(value)}>
|
|
132
|
+
<Tooltip>
|
|
133
|
+
<TooltipTrigger asChild>
|
|
134
|
+
<ToggleGroupItem className={cn("size-9 py-2 px-2 flex items-center justify-center hover:bg-accent rounded-md", layout === 'grid' && 'bg-accent text-accent-foreground')} value="grid" aria-label="Grid layout" >
|
|
135
|
+
<LayoutGrid className="size-3.5" strokeWidth={1.5} />
|
|
136
|
+
</ToggleGroupItem>
|
|
137
|
+
</TooltipTrigger>
|
|
138
|
+
<TooltipContent>
|
|
139
|
+
Grid layout
|
|
140
|
+
</TooltipContent>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
<Tooltip>
|
|
143
|
+
<TooltipTrigger asChild>
|
|
144
|
+
<ToggleGroupItem className={cn("size-9 py-2 px-2 flex items-center justify-center hover:bg-accent rounded-md", layout === 'single' && 'bg-accent text-accent-foreground')} value="single" aria-label="Single column layout" >
|
|
145
|
+
<AlignJustify className="size-3.5" strokeWidth={1.5} />
|
|
146
|
+
</ToggleGroupItem>
|
|
147
|
+
</TooltipTrigger>
|
|
148
|
+
<TooltipContent>
|
|
149
|
+
Column layout
|
|
150
|
+
</TooltipContent>
|
|
151
|
+
</Tooltip>
|
|
152
|
+
</ToggleGroup>
|
|
153
|
+
</div>
|
|
153
154
|
</div>
|
|
154
|
-
</div>
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
156
|
+
{groupBy ? (
|
|
157
|
+
<>
|
|
158
|
+
{Object.keys(groupedList).map((group) => {
|
|
159
|
+
return (
|
|
160
|
+
<div key={`group-${group}`} className="mb-8">
|
|
161
|
+
<HeadersType.H3 className="mb-4 text-lg font-medium border-b border-gray-200 pb-2">{group}</HeadersType.H3>
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
'grid',
|
|
165
|
+
layout === 'grid' ? 'grid-cols-1 gap-10 min-[800px]:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4' : 'grid-cols-1 gap-2'
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
{groupedList[group].map((component) => {
|
|
169
|
+
return <ComponentsPageCard key={`component-${component.id}`} component={component} layout={layout} />;
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
171
172
|
</div>
|
|
172
|
-
|
|
173
|
-
)
|
|
173
|
+
);
|
|
174
|
+
})}
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<div
|
|
178
|
+
className={cn(
|
|
179
|
+
'grid',
|
|
180
|
+
layout === 'grid' ? 'grid-cols-1 gap-10 min-[800px]:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4' : 'grid-cols-1 gap-2'
|
|
181
|
+
)}
|
|
182
|
+
> {list.map((component) => {
|
|
183
|
+
return <ComponentsPageCard key={`component-${component.id}`} component={component} layout={layout} />;
|
|
174
184
|
})}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
<div
|
|
178
|
-
className={cn(
|
|
179
|
-
'grid',
|
|
180
|
-
layout === 'grid' ? 'grid-cols-1 gap-10 min-[800px]:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4' : 'grid-cols-1 gap-2'
|
|
181
|
-
)}
|
|
182
|
-
> {list.map((component) => {
|
|
183
|
-
return <ComponentsPageCard key={`component-${component.id}`} component={component} layout={layout} />;
|
|
184
|
-
})}
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
187
|
</div>
|
|
188
188
|
</TooltipProvider>
|
|
189
189
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface NotFoundProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: React.ReactNode;
|
|
6
|
+
linkHref?: string;
|
|
7
|
+
linkLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const NotFound: React.FC<NotFoundProps> = () => {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex flex-col items-center">
|
|
13
|
+
<div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
|
|
14
|
+
<h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
|
|
15
|
+
<p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
|
|
16
|
+
This page doesn’t exist or has been moved.
|
|
17
|
+
<br />
|
|
18
|
+
Please check the URL or return to the homepage.
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default NotFound;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { GetStaticProps } from 'next';
|
|
2
2
|
import Head from 'next/head';
|
|
3
|
-
import Link from 'next/link';
|
|
4
3
|
import { useRef } from 'react';
|
|
4
|
+
import NotFound from '../../components/NotFound';
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
7
|
import remarkGfm from 'remark-gfm';
|
|
@@ -48,21 +48,7 @@ export default function DocCatchAllPage({ content, menu, metadata, current, conf
|
|
|
48
48
|
<title>404 - Page Not Found</title>
|
|
49
49
|
<meta name="description" content="Page Not Found" />
|
|
50
50
|
</Head>
|
|
51
|
-
<
|
|
52
|
-
<div className="mb-2 text-7xl font-bold text-gray-800 dark:text-white">404</div>
|
|
53
|
-
<h1 className="mb-4 text-2xl font-semibold text-gray-700 dark:text-gray-300">Oops! Page not found.</h1>
|
|
54
|
-
<p className="mb-6 max-w-md text-center text-gray-500 dark:text-gray-400">
|
|
55
|
-
Sorry, the page you are looking for does not exist or has been moved.
|
|
56
|
-
<br />
|
|
57
|
-
Please check the URL or return to the homepage.
|
|
58
|
-
</p>
|
|
59
|
-
<Link
|
|
60
|
-
href="/"
|
|
61
|
-
className="rounded-md bg-blue-600 px-6 py-2 font-medium text-white shadow-md transition-colors duration-200 hover:bg-blue-700"
|
|
62
|
-
>
|
|
63
|
-
Go Home
|
|
64
|
-
</Link>
|
|
65
|
-
</div>
|
|
51
|
+
<NotFound />
|
|
66
52
|
</div>
|
|
67
53
|
);
|
|
68
54
|
}
|
|
@@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|
|
5
5
|
import ReactMarkdown from 'react-markdown';
|
|
6
6
|
import rehypeRaw from 'rehype-raw';
|
|
7
7
|
import Layout from '../../../../components/Layout/Main';
|
|
8
|
+
import NotFound from '../../../../components/NotFound';
|
|
8
9
|
import { MarkdownComponents } from '../../../../components/Markdown/MarkdownComponents';
|
|
9
10
|
import PrevNextNav from '../../../../components/Navigation/PrevNextNav';
|
|
10
11
|
import HeadersType from '../../../../components/Typography/Headers';
|
|
@@ -130,6 +131,7 @@ export const getStaticProps = async (context: { params: IParams }) => {
|
|
|
130
131
|
const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nextPattern }: PatternPageProps) => {
|
|
131
132
|
const [pattern, setPattern] = useState<PatternListObject | undefined>(undefined);
|
|
132
133
|
const [fetchError, setFetchError] = useState<string | undefined>(undefined);
|
|
134
|
+
const [patternNotFound, setPatternNotFound] = useState(false);
|
|
133
135
|
const [iframeHeight, setIframeHeight] = useState('400px');
|
|
134
136
|
const iframeRef = React.useRef<HTMLIFrameElement>(null);
|
|
135
137
|
|
|
@@ -145,13 +147,20 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
145
147
|
const controller = new AbortController();
|
|
146
148
|
setPattern(undefined);
|
|
147
149
|
setFetchError(undefined);
|
|
150
|
+
setPatternNotFound(false);
|
|
148
151
|
|
|
149
152
|
fetch(`${normalizedBasePath}/api/pattern/${id}.json`, { signal: controller.signal })
|
|
150
153
|
.then((res) => {
|
|
154
|
+
if (res.status === 404) {
|
|
155
|
+
setPatternNotFound(true);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
151
158
|
if (!res.ok) throw new Error(`Failed to load pattern: ${res.status} ${res.statusText}`);
|
|
152
159
|
return res.json();
|
|
153
160
|
})
|
|
154
|
-
.then((data) =>
|
|
161
|
+
.then((data) => {
|
|
162
|
+
if (data) setPattern(data as PatternListObject);
|
|
163
|
+
})
|
|
155
164
|
.catch((err) => {
|
|
156
165
|
if (err.name !== 'AbortError') {
|
|
157
166
|
setFetchError(err instanceof Error ? err.message : 'Failed to load pattern data.');
|
|
@@ -167,7 +176,27 @@ const PatternPage = ({ menu, metadata, current, id, config, previousPattern, nex
|
|
|
167
176
|
}
|
|
168
177
|
}, []);
|
|
169
178
|
|
|
170
|
-
if (
|
|
179
|
+
if (patternNotFound) {
|
|
180
|
+
return (
|
|
181
|
+
<Layout config={config} menu={menu} current={current} metadata={{ ...metadata, title: '404 - Page Not Found', metaTitle: '404 - Page Not Found' }}>
|
|
182
|
+
<div className="flex min-h-[60vh] flex-col items-center justify-center">
|
|
183
|
+
<NotFound />
|
|
184
|
+
</div>
|
|
185
|
+
</Layout>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (fetchError) {
|
|
190
|
+
return (
|
|
191
|
+
<Layout config={config} menu={menu} current={current} metadata={metadata}>
|
|
192
|
+
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-2">
|
|
193
|
+
<p className="text-lg font-semibold text-gray-700 dark:text-gray-300">Something went wrong</p>
|
|
194
|
+
<p className="text-sm text-red-500">{fetchError}</p>
|
|
195
|
+
</div>
|
|
196
|
+
</Layout>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
171
200
|
if (!pattern) return <p>Loading...</p>;
|
|
172
201
|
|
|
173
202
|
return (
|
package/src/app-builder/build.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
watchComponentDirectories,
|
|
18
18
|
watchGlobalEntries,
|
|
19
19
|
watchPages,
|
|
20
|
+
watchPatternDirectories,
|
|
20
21
|
watchPublicDirectory,
|
|
21
22
|
watchRuntimeComponents,
|
|
22
23
|
watchRuntimeConfiguration,
|
|
@@ -224,12 +225,14 @@ export const watchApp = async (handoff: Handoff): Promise<void> => {
|
|
|
224
225
|
runtimeComponentsWatcher: null,
|
|
225
226
|
runtimeConfigurationWatcher: null,
|
|
226
227
|
componentDirectoriesWatcher: null,
|
|
228
|
+
patternDirectoriesWatcher: null,
|
|
227
229
|
};
|
|
228
230
|
|
|
229
231
|
watchPublicDirectory(handoff, wss, state, chokidarConfig);
|
|
230
232
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
231
233
|
watchRuntimeConfiguration(handoff, state);
|
|
232
234
|
watchComponentDirectories(handoff, state, chokidarConfig);
|
|
235
|
+
watchPatternDirectories(handoff, state, chokidarConfig);
|
|
233
236
|
watchGlobalEntries(handoff, state, chokidarConfig);
|
|
234
237
|
watchPages(handoff, chokidarConfig);
|
|
235
238
|
};
|
|
@@ -7,6 +7,8 @@ export interface WatcherState {
|
|
|
7
7
|
runtimeComponentsWatcher: chokidar.FSWatcher | null;
|
|
8
8
|
runtimeConfigurationWatcher: chokidar.FSWatcher | null;
|
|
9
9
|
componentDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
10
|
+
/** Watches entries.patterns parent directories for newly created pattern subdirectories. */
|
|
11
|
+
patternDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -2,6 +2,7 @@ import chokidar from 'chokidar';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import Handoff from '..';
|
|
5
|
+
import { buildPatterns } from '../pipeline/patterns';
|
|
5
6
|
import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
|
|
6
7
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
7
8
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
@@ -209,6 +210,15 @@ export const watchRuntimeComponents = (
|
|
|
209
210
|
}
|
|
210
211
|
};
|
|
211
212
|
|
|
213
|
+
const rebuildPatternComponentPreviews = async (handoff: Handoff, patternId: string) => {
|
|
214
|
+
const pattern = handoff.runtimeConfig?.entries?.patterns?.[patternId];
|
|
215
|
+
if (!pattern?.components?.length) return;
|
|
216
|
+
|
|
217
|
+
for (const ref of pattern.components) {
|
|
218
|
+
await processComponents(handoff, ref.id, ComponentSegment.Previews);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
212
222
|
/**
|
|
213
223
|
* Watches the runtime configuration for changes.
|
|
214
224
|
*
|
|
@@ -256,6 +266,8 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
|
|
|
256
266
|
await processComponents(handoff);
|
|
257
267
|
} else if (handle) {
|
|
258
268
|
await handle.apply(handoff, entryAfter?.entityId);
|
|
269
|
+
} else if (entryAfter?.kind === 'pattern') {
|
|
270
|
+
await rebuildPatternComponentPreviews(handoff, entryAfter.entityId);
|
|
259
271
|
} else {
|
|
260
272
|
const effectiveId = entryAfter?.entityId ?? path.basename(path.dirname(changedFile));
|
|
261
273
|
await processComponents(handoff, effectiveId);
|
|
@@ -273,22 +285,50 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
|
|
|
273
285
|
};
|
|
274
286
|
|
|
275
287
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
288
|
+
* Shared factory for watching a parent directory for newly created entity
|
|
289
|
+
* subdirectories (components or patterns). Handles the common scaffolding so
|
|
290
|
+
* that callers only describe what differs for their entity kind:
|
|
291
|
+
*
|
|
292
|
+
* - which config-entry paths to watch
|
|
293
|
+
* - how to read the current known ids after a reload
|
|
294
|
+
* - which WatcherState slot to use
|
|
295
|
+
* - a log label for the entity kind
|
|
296
|
+
* - the rebuild work to run once the runtime config is fresh
|
|
280
297
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
298
|
+
const watchEntityDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions, options: {
|
|
299
|
+
/** Config paths to watch, e.g. handoff.config.entries?.components */
|
|
300
|
+
getConfigPaths: (handoff: Handoff) => string[];
|
|
301
|
+
/** Current known entity ids from runtime config, called again after reload to refresh the set */
|
|
302
|
+
getKnownIds: (handoff: Handoff) => string[];
|
|
303
|
+
/** Read the active FSWatcher from state */
|
|
304
|
+
getWatcher: (state: WatcherState) => chokidar.FSWatcher | null;
|
|
305
|
+
/** Write the active FSWatcher back into state */
|
|
306
|
+
setWatcher: (state: WatcherState, watcher: chokidar.FSWatcher) => void;
|
|
307
|
+
/** Prefix for the scheduleHandler key, e.g. 'newComponent' or 'newPattern' */
|
|
308
|
+
scheduleKeyPrefix: string;
|
|
309
|
+
/** Human-readable label used in log messages, e.g. 'component' or 'pattern' */
|
|
310
|
+
entityLabel: string;
|
|
311
|
+
/**
|
|
312
|
+
* Entity-specific rebuild work, called after reload + persistClientConfig +
|
|
313
|
+
* watcher re-registration. Receives the handoff instance (with fresh runtime
|
|
314
|
+
* config), the triggering directory name, and actual entity ids discovered
|
|
315
|
+
* after reload.
|
|
316
|
+
*/
|
|
317
|
+
onDetected: (handoff: Handoff, context: { dirName: string; addedIds: string[] }) => Promise<void>;
|
|
318
|
+
}) => {
|
|
319
|
+
const { getConfigPaths, getKnownIds, getWatcher, setWatcher, scheduleKeyPrefix, entityLabel, onDetected } = options;
|
|
320
|
+
|
|
321
|
+
const existingWatcher = getWatcher(state);
|
|
322
|
+
if (existingWatcher) {
|
|
323
|
+
existingWatcher.close();
|
|
284
324
|
}
|
|
285
325
|
|
|
286
|
-
const
|
|
287
|
-
if (
|
|
326
|
+
const configPaths = getConfigPaths(handoff);
|
|
327
|
+
if (configPaths.length === 0) return;
|
|
288
328
|
|
|
289
329
|
const dirsToWatch: string[] = [];
|
|
290
|
-
for (const
|
|
291
|
-
const resolved = path.resolve(handoff.workingPath,
|
|
330
|
+
for (const configPath of configPaths) {
|
|
331
|
+
const resolved = path.resolve(handoff.workingPath, configPath);
|
|
292
332
|
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
293
333
|
dirsToWatch.push(resolved);
|
|
294
334
|
}
|
|
@@ -296,40 +336,124 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
|
|
|
296
336
|
|
|
297
337
|
if (dirsToWatch.length === 0) return;
|
|
298
338
|
|
|
299
|
-
|
|
339
|
+
// Snapshot known ids at watch startup so we only react to genuinely new
|
|
340
|
+
// entities, not to file changes inside already-known directories.
|
|
341
|
+
const knownIds = new Set(getKnownIds(handoff));
|
|
300
342
|
|
|
301
|
-
|
|
343
|
+
const watcher = chokidar.watch(dirsToWatch, {
|
|
302
344
|
...chokidarConfig,
|
|
303
345
|
depth: 1,
|
|
304
346
|
});
|
|
347
|
+
setWatcher(state, watcher);
|
|
305
348
|
|
|
306
|
-
|
|
349
|
+
watcher.on('add', async (file) => {
|
|
307
350
|
const basename = path.basename(file);
|
|
308
351
|
const dirName = path.basename(path.dirname(file));
|
|
309
352
|
|
|
310
|
-
|
|
311
|
-
|
|
353
|
+
// Only react to the primary declaration file for the directory (e.g.
|
|
354
|
+
// button.json, button.js, or button.handoff.ts inside a button/ subdir).
|
|
355
|
+
// The basename.startsWith(dirName) guard below ensures .ts here only
|
|
356
|
+
// ever matches files named after their parent directory, not arbitrary
|
|
357
|
+
// TypeScript source files.
|
|
358
|
+
const isConfigFile = basename.endsWith('.json') || basename.endsWith('.js') || basename.endsWith('.cjs') || basename.endsWith('.ts');
|
|
359
|
+
const isNewEntity = isConfigFile && basename.startsWith(dirName) && !knownIds.has(dirName);
|
|
312
360
|
|
|
313
|
-
if (!
|
|
361
|
+
if (!isNewEntity) return;
|
|
314
362
|
|
|
315
|
-
await scheduleHandler(state,
|
|
363
|
+
await scheduleHandler(state, `${scheduleKeyPrefix}:${dirName}`, async () => {
|
|
316
364
|
try {
|
|
317
|
-
Logger.warn(`New
|
|
365
|
+
Logger.warn(`New ${entityLabel} detected: ${dirName}. Reloading configuration...`);
|
|
366
|
+
|
|
367
|
+
// Snapshot ids before reload so we can tell whether the new entity
|
|
368
|
+
// was successfully registered afterwards.
|
|
369
|
+
const idsBefore = new Set(getKnownIds(handoff));
|
|
370
|
+
|
|
318
371
|
handoff.reload();
|
|
319
|
-
knownComponents.add(dirName);
|
|
320
372
|
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
// Refresh from the post-reload runtime config so any ids that were
|
|
374
|
+
// discovered alongside the new entity are also marked as known.
|
|
375
|
+
const idsAfter = getKnownIds(handoff);
|
|
376
|
+
const addedIds = idsAfter.filter((id) => !idsBefore.has(id));
|
|
377
|
+
for (const id of idsAfter) {
|
|
378
|
+
knownIds.add(id);
|
|
323
379
|
}
|
|
324
380
|
|
|
325
381
|
await persistClientConfig(handoff);
|
|
382
|
+
|
|
383
|
+
// Re-register file watchers so the new config file and any runtime
|
|
384
|
+
// files it introduces are covered going forward. This must happen even
|
|
385
|
+
// when the file failed to parse so that watchRuntimeConfiguration can
|
|
386
|
+
// trigger a retry the moment the file is saved with valid content.
|
|
326
387
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
327
388
|
watchRuntimeConfiguration(handoff, state);
|
|
328
|
-
|
|
329
|
-
|
|
389
|
+
|
|
390
|
+
// If no new ids appeared after reload the file was empty or invalid.
|
|
391
|
+
// Skip the build — watchRuntimeConfiguration will fire when it is saved.
|
|
392
|
+
if (addedIds.length === 0) {
|
|
393
|
+
Logger.warn(`${entityLabel} "${dirName}" config is empty or incomplete — build will run automatically once the file is saved.`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await onDetected(handoff, { dirName, addedIds });
|
|
330
398
|
} catch (e) {
|
|
331
|
-
Logger.error(
|
|
399
|
+
Logger.error(`Error processing new ${entityLabel}:`, e);
|
|
332
400
|
}
|
|
333
401
|
});
|
|
334
402
|
});
|
|
335
403
|
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Watches the parent component directories from config.entries.components for
|
|
407
|
+
* new components being added. When a new config file appears in a new
|
|
408
|
+
* subdirectory, reloads the runtime config, restarts the component and
|
|
409
|
+
* configuration watchers, then builds the new component and any patterns that
|
|
410
|
+
* reference it.
|
|
411
|
+
*/
|
|
412
|
+
export const watchComponentDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
|
|
413
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
414
|
+
getConfigPaths: (h) => h.config.entries?.components ?? [],
|
|
415
|
+
getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.components ?? {}),
|
|
416
|
+
getWatcher: (s) => s.componentDirectoriesWatcher,
|
|
417
|
+
setWatcher: (s, w) => { s.componentDirectoriesWatcher = w; },
|
|
418
|
+
scheduleKeyPrefix: 'newComponent',
|
|
419
|
+
entityLabel: 'component',
|
|
420
|
+
onDetected: async (handoff, { addedIds }) => {
|
|
421
|
+
for (const componentId of addedIds) {
|
|
422
|
+
await processComponents(handoff, componentId);
|
|
423
|
+
await runAllFinalizers(handoff, { patternRebuildComponentIds: [componentId] });
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Watches the parent pattern directories from config.entries.patterns for new
|
|
431
|
+
* patterns being added. The rebuild sequence intentionally differs from
|
|
432
|
+
* watchComponentDirectories because the dependency arrow is reversed: a new
|
|
433
|
+
* pattern references existing components, so we rebuild only the preview
|
|
434
|
+
* segment of those components (to produce the __pattern_* HTML files that
|
|
435
|
+
* injectPatternPreviews registered) before composing the pattern itself.
|
|
436
|
+
*/
|
|
437
|
+
export const watchPatternDirectories = (handoff: Handoff, state: WatcherState, chokidarConfig: chokidar.WatchOptions) => {
|
|
438
|
+
watchEntityDirectories(handoff, state, chokidarConfig, {
|
|
439
|
+
getConfigPaths: (h) => h.config.entries?.patterns ?? [],
|
|
440
|
+
getKnownIds: (h) => Object.keys(h.runtimeConfig?.entries?.patterns ?? {}),
|
|
441
|
+
getWatcher: (s) => s.patternDirectoriesWatcher,
|
|
442
|
+
setWatcher: (s, w) => { s.patternDirectoriesWatcher = w; },
|
|
443
|
+
scheduleKeyPrefix: 'newPattern',
|
|
444
|
+
entityLabel: 'pattern',
|
|
445
|
+
onDetected: async (handoff, { addedIds }) => {
|
|
446
|
+
// Rebuild only the previews segment for each component the new pattern
|
|
447
|
+
// references. Their JS/CSS/structure are unchanged — only the new
|
|
448
|
+
// synthetic preview HTML files need to be generated so that buildPatterns
|
|
449
|
+
// can assemble the pattern from them.
|
|
450
|
+
for (const patternId of addedIds) {
|
|
451
|
+
await rebuildPatternComponentPreviews(handoff, patternId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Build only the new pattern directly rather than going through
|
|
455
|
+
// runAllFinalizers, which would rebuild all patterns unnecessarily.
|
|
456
|
+
await buildPatterns(handoff, { onlyPatternIds: new Set(addedIds) });
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
};
|
package/src/cache/build-cache.ts
CHANGED
|
@@ -226,6 +226,23 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
|
|
|
226
226
|
files.push(matchingConfigPath);
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
// Include every pattern config file whose declaration references this component.
|
|
230
|
+
// When a pattern is added or modified, injectPatternPreviews injects new
|
|
231
|
+
// __pattern_* synthetic preview keys onto the referenced component, changing
|
|
232
|
+
// what processComponents must render. Without tracking these files here, the
|
|
233
|
+
// cache would incorrectly consider the component unchanged and skip the preview
|
|
234
|
+
// rebuild, leaving buildPatterns unable to find the required HTML fragments.
|
|
235
|
+
const runtimePatterns = handoff.runtimeConfig?.entries?.patterns ?? {};
|
|
236
|
+
for (const configPath of configPaths) {
|
|
237
|
+
const entry = handoff.getConfigFileEntry(configPath);
|
|
238
|
+
if (entry?.kind !== 'pattern') continue;
|
|
239
|
+
const pattern = runtimePatterns[entry.entityId];
|
|
240
|
+
if (!pattern) continue;
|
|
241
|
+
if (pattern.components?.some((ref) => ref.id === componentId)) {
|
|
242
|
+
files.push(configPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
229
246
|
return { files, templateDir };
|
|
230
247
|
}
|
|
231
248
|
|
package/src/config/runtime.ts
CHANGED
|
@@ -284,7 +284,12 @@ export const initRuntimeConfig = (handoff: HandoffContext): [runtimeConfig: Runt
|
|
|
284
284
|
fallbackId: patternBaseName,
|
|
285
285
|
});
|
|
286
286
|
} catch (err) {
|
|
287
|
-
|
|
287
|
+
// Treat as a warning rather than an error: the file may be empty or
|
|
288
|
+
// mid-edit. It is already tracked in configFiles above, so
|
|
289
|
+
// watchRuntimeConfiguration will pick it up once the file is saved
|
|
290
|
+
// with valid content.
|
|
291
|
+
Logger.warn(`Pattern config skipped (incomplete or invalid) — will retry on next save: ${resolvedPatternConfigPath}`);
|
|
292
|
+
Logger.debug(`Pattern parse detail:`, err);
|
|
288
293
|
continue;
|
|
289
294
|
}
|
|
290
295
|
|
package/handoff.config.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/** @type {import('handoff-app').Config} */
|
|
2
|
-
const path = require('path');
|
|
3
|
-
module.exports = {
|
|
4
|
-
// Set your Figma project ID here or in the environment variable HANDOFF_FIGMA_PROJECT_ID
|
|
5
|
-
figma_project_id: null,
|
|
6
|
-
// Set the output directory for the exported components
|
|
7
|
-
exportsOutputDirectory: 'exported',
|
|
8
|
-
// Set the output directory for the sites
|
|
9
|
-
sitesOutputDirectory: 'out',
|
|
10
|
-
// These settings control the behavior of the application
|
|
11
|
-
app: {
|
|
12
|
-
// Set the theme for the application
|
|
13
|
-
theme: 'default',
|
|
14
|
-
// Set the title for the application
|
|
15
|
-
title: 'Handoff Design System',
|
|
16
|
-
// Set the client for the application, used in the footer
|
|
17
|
-
client: 'Handoff Technologies',
|
|
18
|
-
// Set the Google Tag Manager ID to add GTM scripts
|
|
19
|
-
google_tag_manager: null,
|
|
20
|
-
// Set the attribution for the application
|
|
21
|
-
attribution: true, // Should we remove this?
|
|
22
|
-
// Set the type copy for the typeography page
|
|
23
|
-
type_copy: 'Almost before we knew it, we had left the ground.',
|
|
24
|
-
// Set the type sort for the typeography page
|
|
25
|
-
type_sort: [
|
|
26
|
-
'Heading 1',
|
|
27
|
-
'Heading 2',
|
|
28
|
-
'Heading 3',
|
|
29
|
-
'Heading 4',
|
|
30
|
-
'Heading 5',
|
|
31
|
-
'Heading 6',
|
|
32
|
-
'Paragraph',
|
|
33
|
-
'Subheading',
|
|
34
|
-
'Blockquote',
|
|
35
|
-
'Input Labels',
|
|
36
|
-
'Link',
|
|
37
|
-
],
|
|
38
|
-
// Set the color sort for the color page
|
|
39
|
-
color_sort: ['primary', 'secondary', 'extra', 'system'],
|
|
40
|
-
// Set the component sort for the component page
|
|
41
|
-
component_sort: ['primary', 'secondary', 'transparent'],
|
|
42
|
-
// Set the base path for the application
|
|
43
|
-
base_path: '',
|
|
44
|
-
// Set the breakpoints for the application
|
|
45
|
-
breakpoints: {
|
|
46
|
-
sm: { size: 576, name: 'Small' },
|
|
47
|
-
md: { size: 768, name: 'Medium' },
|
|
48
|
-
lg: { size: 992, name: 'Large' },
|
|
49
|
-
xl: { size: 1200, name: 'Extra Large' },
|
|
50
|
-
},
|
|
51
|
-
// Set the ports for the application
|
|
52
|
-
ports: { app: 3000, websocket: 3001 },
|
|
53
|
-
},
|
|
54
|
-
// These are the entry points used to compile the target application
|
|
55
|
-
entries: {
|
|
56
|
-
scss: './sass/main.scss',
|
|
57
|
-
js: './js/main.js',
|
|
58
|
-
bundle: './js/main.js',
|
|
59
|
-
//components: ['./test_components'],
|
|
60
|
-
//patterns: ['./test_patterns'],
|
|
61
|
-
},
|
|
62
|
-
// These are the asset zip file download links
|
|
63
|
-
assets_zip_links: {
|
|
64
|
-
icons: null,
|
|
65
|
-
logos: null,
|
|
66
|
-
},
|
|
67
|
-
// Hooks allow you to override the default behavior of the application
|
|
68
|
-
hooks: {
|
|
69
|
-
// Alter the vite css build config
|
|
70
|
-
cssBuildConfig: (config) => {
|
|
71
|
-
return config;
|
|
72
|
-
},
|
|
73
|
-
// Validate each component before it is exported
|
|
74
|
-
validateComponent: (component) => {
|
|
75
|
-
return {};
|
|
76
|
-
},
|
|
77
|
-
// Alter the vite client build config
|
|
78
|
-
clientBuildConfig: (config) => {
|
|
79
|
-
return config;
|
|
80
|
-
},
|
|
81
|
-
// Alter the vite html build config
|
|
82
|
-
htmlBuildConfig: (config) => {
|
|
83
|
-
return config;
|
|
84
|
-
},
|
|
85
|
-
// Alter the vite js build config
|
|
86
|
-
jsBuildConfig: (config) => {
|
|
87
|
-
return config;
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
};
|