handoff-app 1.1.3 → 1.2.0-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/config.template.js +21 -14
- package/config/config.template.ts +54 -0
- package/config/docs/system.md +2 -0
- package/dist/app-builder/build.js +7 -2
- package/dist/app-builder/config-diff/index.d.ts +4 -0
- package/dist/app-builder/config-diff/index.js +12 -0
- package/dist/app-builder/config-diff/registry.d.ts +9 -0
- package/dist/app-builder/config-diff/registry.js +30 -0
- package/dist/app-builder/config-diff/snapshot.d.ts +13 -0
- package/dist/app-builder/config-diff/snapshot.js +43 -0
- package/dist/app-builder/config-diff/strategies/pattern.d.ts +2 -0
- package/dist/app-builder/config-diff/strategies/pattern.js +93 -0
- package/dist/app-builder/config-diff/types.d.ts +19 -0
- package/dist/app-builder/config-diff/types.js +2 -0
- package/dist/app-builder/watchers/component.d.ts +15 -0
- package/dist/app-builder/watchers/component.js +69 -0
- package/dist/app-builder/watchers/utils.d.ts +14 -0
- package/dist/app-builder/watchers/utils.js +44 -0
- package/dist/app-builder/watchers.d.ts +10 -19
- package/dist/app-builder/watchers.js +56 -102
- package/dist/cache/build-cache.js +14 -5
- package/dist/cli/create.js +67 -23
- package/dist/cli/eject.js +48 -7
- package/dist/cli/make.js +29 -4
- package/dist/cli/scaffold.js +60 -19
- package/dist/cli.js +1 -1
- package/dist/config/helpers.d.ts +7 -0
- package/dist/config/helpers.js +31 -0
- package/dist/config/index.d.ts +2 -1
- package/dist/config/index.js +5 -1
- package/dist/config/loader.d.ts +10 -1
- package/dist/config/loader.js +92 -28
- package/dist/config/normalizers/declaration.d.ts +11 -0
- package/dist/config/normalizers/declaration.js +119 -0
- package/dist/config/normalizers/pattern.d.ts +9 -0
- package/dist/config/normalizers/pattern.js +38 -0
- package/dist/config/runtime.d.ts +9 -3
- package/dist/config/runtime.js +254 -33
- package/dist/declarations/helpers.d.ts +6 -0
- package/dist/declarations/helpers.js +19 -0
- package/dist/declarations/index.d.ts +2 -0
- package/dist/declarations/index.js +9 -0
- package/dist/declarations/types.d.ts +60 -0
- package/dist/declarations/types.js +2 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +34 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline/patterns.d.ts +6 -0
- package/dist/pipeline/patterns.js +21 -0
- package/dist/transformers/docgen/index.js +12 -2
- package/dist/transformers/plugins/csf-render.js +170 -34
- package/dist/transformers/plugins/ssr-render.js +18 -2
- package/dist/transformers/preview/component/api.js +16 -7
- package/dist/transformers/preview/component/builder.js +9 -3
- package/dist/transformers/preview/component/css.d.ts +8 -0
- package/dist/transformers/preview/component/html.js +14 -5
- package/dist/transformers/preview/component/previews.d.ts +9 -0
- package/dist/transformers/preview/component/previews.js +10 -0
- package/dist/transformers/preview/pattern/api.d.ts +7 -0
- package/dist/transformers/preview/pattern/api.js +61 -0
- package/dist/transformers/preview/pattern/builder.d.ts +9 -0
- package/dist/transformers/preview/pattern/builder.js +86 -0
- package/dist/transformers/preview/pattern/html.d.ts +19 -0
- package/dist/transformers/preview/pattern/html.js +94 -0
- package/dist/transformers/preview/types.d.ts +54 -0
- package/dist/transformers/utils/csf.d.ts +16 -0
- package/dist/transformers/utils/csf.js +30 -0
- package/dist/transformers/utils/index.d.ts +1 -0
- package/dist/transformers/utils/index.js +2 -0
- package/dist/transformers/utils/module.d.ts +5 -0
- package/dist/transformers/utils/module.js +26 -13
- package/dist/transformers/utils/usage.d.ts +23 -0
- package/dist/transformers/utils/usage.js +294 -0
- package/dist/types/config.d.ts +32 -7
- package/dist/types/preview.d.ts +2 -0
- package/docs/cli.md +3 -3
- package/package.json +3 -1
- package/src/app/components/Component/PageSliceResolver.tsx +6 -6
- package/src/app/components/Component/Preview.tsx +17 -12
- package/src/app/components/Component/slices/ComponentDisplaySlice.tsx +16 -6
- package/src/app/components/Markdown/CodeHighlight.tsx +60 -11
- package/src/app/components/util/index.ts +71 -2
- package/src/app/pages/system/pattern/[pattern]/index.tsx +223 -0
- package/src/app/pages/system/pattern/index.tsx +14 -0
- package/src/app-builder/README.md +37 -9
- package/src/app-builder/build.ts +8 -2
- package/src/app-builder/config-diff/index.ts +4 -0
- package/src/app-builder/config-diff/registry.ts +23 -0
- package/src/app-builder/config-diff/snapshot.ts +51 -0
- package/src/app-builder/config-diff/strategies/pattern.ts +65 -0
- package/src/app-builder/config-diff/types.ts +21 -0
- package/src/app-builder/watchers/component.ts +72 -0
- package/src/app-builder/watchers/utils.ts +44 -0
- package/src/app-builder/watchers.ts +62 -80
- package/src/cache/build-cache.ts +14 -5
- package/src/cli/create.ts +86 -25
- package/src/cli/eject.ts +29 -7
- package/src/cli/make.ts +32 -5
- package/src/cli/scaffold.ts +68 -21
- package/src/cli.ts +1 -1
- package/src/config/README.md +10 -1
- package/src/config/helpers.ts +43 -0
- package/src/config/index.ts +4 -1
- package/src/config/loader.ts +110 -31
- package/src/config/normalizers/declaration.ts +176 -0
- package/src/config/normalizers/pattern.ts +63 -0
- package/src/config/runtime.ts +310 -35
- package/src/declarations/helpers.ts +36 -0
- package/src/declarations/index.ts +19 -0
- package/src/declarations/types.ts +70 -0
- package/src/index.ts +47 -6
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/patterns.ts +10 -0
- package/src/transformers/README.md +2 -2
- package/src/transformers/docgen/index.ts +13 -2
- package/src/transformers/plugins/csf-render.ts +216 -49
- package/src/transformers/plugins/ssr-render.ts +23 -7
- package/src/transformers/preview/component/api.ts +27 -7
- package/src/transformers/preview/component/builder.ts +21 -4
- package/src/transformers/preview/component/html.ts +18 -5
- package/src/transformers/preview/component/previews.ts +12 -0
- package/src/transformers/preview/pattern/api.ts +66 -0
- package/src/transformers/preview/pattern/builder.ts +94 -0
- package/src/transformers/preview/pattern/html.ts +107 -0
- package/src/transformers/preview/types.ts +58 -1
- package/src/transformers/utils/csf.ts +48 -0
- package/src/transformers/utils/index.ts +3 -0
- package/src/transformers/utils/module.ts +33 -18
- package/src/transformers/utils/usage.ts +319 -0
- package/src/types/README.md +1 -1
- package/src/types/config.ts +33 -7
- package/src/types/preview.ts +2 -0
- package/templates/default/components/button/Button.jsx.tpl +64 -0
- package/templates/default/components/button/button.react.handoff.js.tpl +31 -0
- package/templates/default/components/button/button.react.handoff.ts.tpl +31 -0
- package/templates/default/handoff.config.js.tpl +29 -16
- package/templates/default/handoff.config.ts.tpl +196 -0
- package/templates/default/handoff.config.with-examples.js.tpl +28 -17
- package/templates/default/handoff.config.with-examples.ts.tpl +195 -0
- package/templates/default/package.json.tpl +1 -3
- package/handoff.config.js +0 -90
- package/templates/default/components/button/button.js.tpl +0 -62
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Handoff from '../../..';
|
|
2
|
+
import { buildPatterns } from '../../../pipeline/patterns';
|
|
3
|
+
import processComponents, { ComponentSegment } from '../../../transformers/preview/component/builder';
|
|
4
|
+
import { Logger } from '../../../utils/logger';
|
|
5
|
+
import { diffMapSnapshots, MapSnapshot, stableStringify } from '../snapshot';
|
|
6
|
+
import { ConfigDiffStrategy } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Snapshots every __pattern_{patternId}_* synthetic preview entry across all
|
|
10
|
+
* components. Outer key is component id, inner key is synthetic preview key.
|
|
11
|
+
*/
|
|
12
|
+
const snapshotPatternSynthetics = (handoff: Handoff, patternId: string): MapSnapshot => {
|
|
13
|
+
const components = handoff.runtimeConfig?.entries?.components ?? {};
|
|
14
|
+
const prefix = `__pattern_${patternId}_`;
|
|
15
|
+
const result: MapSnapshot = new Map();
|
|
16
|
+
|
|
17
|
+
for (const [componentId, component] of Object.entries(components)) {
|
|
18
|
+
const previews = component.internalPatternPreviews ?? {};
|
|
19
|
+
const entries = new Map<string, string>();
|
|
20
|
+
for (const [key, preview] of Object.entries(previews)) {
|
|
21
|
+
if (key.startsWith(prefix)) {
|
|
22
|
+
entries.set(key, stableStringify(preview.values));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (entries.size > 0) {
|
|
26
|
+
result.set(componentId, entries);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const patternDiffStrategy: ConfigDiffStrategy = {
|
|
34
|
+
kind: 'pattern',
|
|
35
|
+
|
|
36
|
+
capture(handoff, entityId) {
|
|
37
|
+
const oldSnapshot = snapshotPatternSynthetics(handoff, entityId);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
async apply(handoff, entityIdAfterReload) {
|
|
41
|
+
const newSnapshot = entityIdAfterReload
|
|
42
|
+
? snapshotPatternSynthetics(handoff, entityIdAfterReload)
|
|
43
|
+
: new Map();
|
|
44
|
+
|
|
45
|
+
const affectedIds = diffMapSnapshots(oldSnapshot, newSnapshot);
|
|
46
|
+
const displayId = entityIdAfterReload || entityId;
|
|
47
|
+
|
|
48
|
+
if (affectedIds.length > 0) {
|
|
49
|
+
Logger.debug(
|
|
50
|
+
`Pattern "${displayId}" changed: rebuilding ${affectedIds.length} affected component(s): ${affectedIds.join(', ')}`
|
|
51
|
+
);
|
|
52
|
+
for (const componentId of affectedIds) {
|
|
53
|
+
await processComponents(handoff, componentId, ComponentSegment.Previews);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
Logger.debug(`Pattern "${displayId}" changed: no synthetic previews affected, skipping component rebuild.`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
async finalize(handoff) {
|
|
63
|
+
await buildPatterns(handoff);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Handoff from '../..';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle returned by ConfigDiffStrategy.capture().
|
|
5
|
+
*
|
|
6
|
+
* Captures the pre-reload state in a closure so the snapshot type stays
|
|
7
|
+
* fully encapsulated and the watcher only sees this opaque handle.
|
|
8
|
+
*/
|
|
9
|
+
export interface RebuildHandle {
|
|
10
|
+
apply(handoff: Handoff, entityIdAfterReload: string | undefined): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Encapsulates the snapshot -> reload -> diff -> selective-rebuild protocol
|
|
15
|
+
* for a single config-file entity kind.
|
|
16
|
+
*/
|
|
17
|
+
export interface ConfigDiffStrategy {
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
capture(handoff: Handoff, entityId: string): RebuildHandle;
|
|
20
|
+
finalize(handoff: Handoff): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import Handoff from '../..';
|
|
4
|
+
import { ComponentSegment } from '../../transformers/preview/component/builder';
|
|
5
|
+
import { ComponentListObject } from '../../transformers/preview/types';
|
|
6
|
+
import { normalizePathForCompare } from '../../utils/path';
|
|
7
|
+
|
|
8
|
+
export type RuntimeComponentEntryType = keyof ComponentListObject['entries'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps configuration entry types to component segments.
|
|
12
|
+
*/
|
|
13
|
+
export const mapEntryTypeToSegment = (type: RuntimeComponentEntryType): ComponentSegment | undefined => {
|
|
14
|
+
return {
|
|
15
|
+
js: ComponentSegment.JavaScript,
|
|
16
|
+
scss: ComponentSegment.Style,
|
|
17
|
+
template: ComponentSegment.Previews,
|
|
18
|
+
templates: ComponentSegment.Previews,
|
|
19
|
+
component: ComponentSegment.Previews,
|
|
20
|
+
story: ComponentSegment.Previews,
|
|
21
|
+
}[type];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const resolveComponentIdForChangedFile = (
|
|
25
|
+
handoff: Handoff,
|
|
26
|
+
changedFilePath: string
|
|
27
|
+
): string | undefined => {
|
|
28
|
+
const normalizedChangedPath = normalizePathForCompare(changedFilePath);
|
|
29
|
+
const runtimeComponents = handoff.runtimeConfig?.entries?.components ?? {};
|
|
30
|
+
|
|
31
|
+
for (const [componentId, componentDef] of Object.entries(runtimeComponents)) {
|
|
32
|
+
const entries = componentDef.entries ?? {};
|
|
33
|
+
for (const entryPath of Object.values(entries)) {
|
|
34
|
+
if (!entryPath) continue;
|
|
35
|
+
const normalizedEntryPath = normalizePathForCompare(entryPath as string);
|
|
36
|
+
if (normalizedEntryPath === normalizedChangedPath) {
|
|
37
|
+
return componentId;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fallback for legacy path conventions when exact match is unavailable.
|
|
43
|
+
return path.basename(path.dirname(changedFilePath));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the paths of runtime components to watch.
|
|
48
|
+
*
|
|
49
|
+
* @returns A map of paths to watch and their entry types.
|
|
50
|
+
*/
|
|
51
|
+
export const getRuntimeComponentsPathsToWatch = (
|
|
52
|
+
handoff: Handoff
|
|
53
|
+
): Map<string, RuntimeComponentEntryType> => {
|
|
54
|
+
const result: Map<string, RuntimeComponentEntryType> = new Map();
|
|
55
|
+
|
|
56
|
+
for (const runtimeComponentId of Object.keys(handoff.runtimeConfig?.entries.components ?? {})) {
|
|
57
|
+
const runtimeComponent = handoff.runtimeConfig.entries.components[runtimeComponentId];
|
|
58
|
+
for (const [runtimeComponentEntryType, runtimeComponentEntryPath] of Object.entries(runtimeComponent.entries ?? {})) {
|
|
59
|
+
const normalizedComponentEntryPath = runtimeComponentEntryPath as string;
|
|
60
|
+
if (fs.existsSync(normalizedComponentEntryPath)) {
|
|
61
|
+
const entryType = runtimeComponentEntryType as RuntimeComponentEntryType;
|
|
62
|
+
if (fs.statSync(normalizedComponentEntryPath).isFile()) {
|
|
63
|
+
result.set(path.resolve(normalizedComponentEntryPath), entryType);
|
|
64
|
+
} else {
|
|
65
|
+
result.set(normalizedComponentEntryPath, entryType);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import { Logger } from '../../utils/logger';
|
|
3
|
+
|
|
4
|
+
export interface WatcherState {
|
|
5
|
+
busy: boolean;
|
|
6
|
+
pendingHandlers: Map<string, () => Promise<void>>;
|
|
7
|
+
runtimeComponentsWatcher: chokidar.FSWatcher | null;
|
|
8
|
+
runtimeConfigurationWatcher: chokidar.FSWatcher | null;
|
|
9
|
+
componentDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Schedules a handler for execution with mutual exclusion and per-source
|
|
14
|
+
* coalescing. When the lock is free the handler runs immediately. When busy,
|
|
15
|
+
* only the latest handler per source key is kept.
|
|
16
|
+
*/
|
|
17
|
+
export const scheduleHandler = async (
|
|
18
|
+
state: WatcherState,
|
|
19
|
+
source: string,
|
|
20
|
+
handler: () => Promise<void>
|
|
21
|
+
): Promise<void> => {
|
|
22
|
+
if (state.busy) {
|
|
23
|
+
state.pendingHandlers.set(source, handler);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
state.busy = true;
|
|
28
|
+
try {
|
|
29
|
+
await handler();
|
|
30
|
+
while (state.pendingHandlers.size > 0) {
|
|
31
|
+
const entries = Array.from(state.pendingHandlers.entries());
|
|
32
|
+
state.pendingHandlers.clear();
|
|
33
|
+
for (const [pendingSource, pendingHandler] of entries) {
|
|
34
|
+
try {
|
|
35
|
+
await pendingHandler();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
Logger.error(`Unhandled error in scheduled handler for source "${pendingSource}":`, e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} finally {
|
|
42
|
+
state.busy = false;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -2,21 +2,24 @@ import chokidar from 'chokidar';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import Handoff from '..';
|
|
5
|
+
import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
|
|
5
6
|
import { buildMainCss } from '../transformers/preview/component/css';
|
|
6
7
|
import { buildMainJS } from '../transformers/preview/component/javascript';
|
|
7
|
-
import processComponents, { ComponentSegment } from '../transformers/preview/component/builder';
|
|
8
|
-
import { ComponentListObject } from '../transformers/preview/types';
|
|
9
8
|
import { Logger } from '../utils/logger';
|
|
10
9
|
import { isPathInside, normalizePathForCompare } from '../utils/path';
|
|
11
10
|
import { persistClientConfig } from './client-config';
|
|
11
|
+
import { getStrategy, runAllFinalizers } from './config-diff';
|
|
12
12
|
import { syncPublicFiles } from './paths';
|
|
13
|
+
import {
|
|
14
|
+
getRuntimeComponentsPathsToWatch,
|
|
15
|
+
mapEntryTypeToSegment,
|
|
16
|
+
resolveComponentIdForChangedFile,
|
|
17
|
+
RuntimeComponentEntryType,
|
|
18
|
+
} from './watchers/component';
|
|
19
|
+
import { scheduleHandler, WatcherState } from './watchers/utils';
|
|
13
20
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
runtimeComponentsWatcher: chokidar.FSWatcher | null;
|
|
17
|
-
runtimeConfigurationWatcher: chokidar.FSWatcher | null;
|
|
18
|
-
componentDirectoriesWatcher: chokidar.FSWatcher | null;
|
|
19
|
-
}
|
|
21
|
+
export { getRuntimeComponentsPathsToWatch, mapEntryTypeToSegment } from './watchers/component';
|
|
22
|
+
export type { WatcherState } from './watchers/utils';
|
|
20
23
|
|
|
21
24
|
/**
|
|
22
25
|
* Watches the working public directory for changes and updates the app.
|
|
@@ -28,18 +31,15 @@ export const watchPublicDirectory = (handoff: Handoff, wss: (msg: string) => voi
|
|
|
28
31
|
case 'add':
|
|
29
32
|
case 'change':
|
|
30
33
|
case 'unlink':
|
|
31
|
-
|
|
32
|
-
state.debounce = true;
|
|
34
|
+
await scheduleHandler(state, 'publicDirectory', async () => {
|
|
33
35
|
try {
|
|
34
36
|
Logger.warn('Public directory changed. Handoff will ingest the new data...');
|
|
35
37
|
await syncPublicFiles(handoff);
|
|
36
38
|
wss(JSON.stringify({ type: 'reload' }));
|
|
37
39
|
} catch (e) {
|
|
38
40
|
Logger.error('Error syncing public directory:', e);
|
|
39
|
-
} finally {
|
|
40
|
-
state.debounce = false;
|
|
41
41
|
}
|
|
42
|
-
}
|
|
42
|
+
});
|
|
43
43
|
break;
|
|
44
44
|
}
|
|
45
45
|
});
|
|
@@ -122,8 +122,7 @@ export const watchGlobalEntries = (handoff: Handoff, state: WatcherState, chokid
|
|
|
122
122
|
case 'add':
|
|
123
123
|
case 'change':
|
|
124
124
|
case 'unlink':
|
|
125
|
-
|
|
126
|
-
state.debounce = true;
|
|
125
|
+
await scheduleHandler(state, 'globalEntries', async () => {
|
|
127
126
|
try {
|
|
128
127
|
Logger.warn('Global entry changed. Rebuilding bundles and components...');
|
|
129
128
|
|
|
@@ -139,62 +138,23 @@ export const watchGlobalEntries = (handoff: Handoff, state: WatcherState, chokid
|
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
await processComponents(handoff);
|
|
141
|
+
await runAllFinalizers(handoff);
|
|
142
142
|
} catch (e) {
|
|
143
143
|
Logger.error('Error rebuilding after global entry change:', e);
|
|
144
|
-
} finally {
|
|
145
|
-
state.debounce = false;
|
|
146
144
|
}
|
|
147
|
-
}
|
|
145
|
+
});
|
|
148
146
|
break;
|
|
149
147
|
}
|
|
150
148
|
});
|
|
151
149
|
};
|
|
152
150
|
|
|
153
|
-
/**
|
|
154
|
-
* Maps configuration entry types to component segments.
|
|
155
|
-
*/
|
|
156
|
-
export const mapEntryTypeToSegment = (type: keyof ComponentListObject['entries']): ComponentSegment | undefined => {
|
|
157
|
-
return {
|
|
158
|
-
js: ComponentSegment.JavaScript,
|
|
159
|
-
scss: ComponentSegment.Style,
|
|
160
|
-
template: ComponentSegment.Previews,
|
|
161
|
-
templates: ComponentSegment.Previews,
|
|
162
|
-
}[type];
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Gets the paths of runtime components to watch.
|
|
167
|
-
*
|
|
168
|
-
* @returns A Map of paths to watch and their entry types
|
|
169
|
-
*/
|
|
170
|
-
export const getRuntimeComponentsPathsToWatch = (handoff: Handoff) => {
|
|
171
|
-
const result: Map<string, keyof ComponentListObject['entries']> = new Map();
|
|
172
|
-
|
|
173
|
-
for (const runtimeComponentId of Object.keys(handoff.runtimeConfig?.entries.components ?? {})) {
|
|
174
|
-
const runtimeComponent = handoff.runtimeConfig.entries.components[runtimeComponentId];
|
|
175
|
-
for (const [runtimeComponentEntryType, runtimeComponentEntryPath] of Object.entries(runtimeComponent.entries ?? {})) {
|
|
176
|
-
const normalizedComponentEntryPath = runtimeComponentEntryPath as string;
|
|
177
|
-
if (fs.existsSync(normalizedComponentEntryPath)) {
|
|
178
|
-
const entryType = runtimeComponentEntryType as keyof ComponentListObject['entries'];
|
|
179
|
-
if (fs.statSync(normalizedComponentEntryPath).isFile()) {
|
|
180
|
-
result.set(path.resolve(normalizedComponentEntryPath), entryType);
|
|
181
|
-
} else {
|
|
182
|
-
result.set(normalizedComponentEntryPath, entryType);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return result;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
151
|
/**
|
|
192
152
|
* Watches runtime components for changes.
|
|
193
153
|
*/
|
|
194
154
|
export const watchRuntimeComponents = (
|
|
195
155
|
handoff: Handoff,
|
|
196
156
|
state: WatcherState,
|
|
197
|
-
runtimeComponentPathsToWatch: Map<string,
|
|
157
|
+
runtimeComponentPathsToWatch: Map<string, RuntimeComponentEntryType>
|
|
198
158
|
) => {
|
|
199
159
|
if (state.runtimeComponentsWatcher) {
|
|
200
160
|
state.runtimeComponentsWatcher.close();
|
|
@@ -202,7 +162,7 @@ export const watchRuntimeComponents = (
|
|
|
202
162
|
|
|
203
163
|
if (runtimeComponentPathsToWatch.size > 0) {
|
|
204
164
|
const pathsToWatch = Array.from(runtimeComponentPathsToWatch.keys());
|
|
205
|
-
const runtimeComponentEntryTypeByPath = new Map<string,
|
|
165
|
+
const runtimeComponentEntryTypeByPath = new Map<string, RuntimeComponentEntryType>(
|
|
206
166
|
Array.from(runtimeComponentPathsToWatch.entries()).map(([watchPath, entryType]) => [normalizePathForCompare(watchPath), entryType])
|
|
207
167
|
);
|
|
208
168
|
const configFileSet = new Set(handoff.getConfigFilePaths().map((configPath) => normalizePathForCompare(configPath)));
|
|
@@ -220,20 +180,17 @@ export const watchRuntimeComponents = (
|
|
|
220
180
|
case 'add':
|
|
221
181
|
case 'change':
|
|
222
182
|
case 'unlink':
|
|
223
|
-
|
|
224
|
-
state.debounce = true;
|
|
183
|
+
await scheduleHandler(state, `runtimeComponent:${normalizedFile}`, async () => {
|
|
225
184
|
try {
|
|
226
185
|
const entryType = runtimeComponentEntryTypeByPath.get(normalizedFile);
|
|
227
|
-
const segmentToUpdate: ComponentSegment = entryType ? mapEntryTypeToSegment(entryType) : undefined;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
await
|
|
186
|
+
const segmentToUpdate: ComponentSegment | undefined = entryType ? mapEntryTypeToSegment(entryType) : undefined;
|
|
187
|
+
const componentId = resolveComponentIdForChangedFile(handoff, file);
|
|
188
|
+
await processComponents(handoff, componentId, segmentToUpdate);
|
|
189
|
+
await runAllFinalizers(handoff);
|
|
231
190
|
} catch (e) {
|
|
232
191
|
Logger.error('Error processing component:', e);
|
|
233
|
-
} finally {
|
|
234
|
-
state.debounce = false;
|
|
235
192
|
}
|
|
236
|
-
}
|
|
193
|
+
});
|
|
237
194
|
break;
|
|
238
195
|
}
|
|
239
196
|
});
|
|
@@ -242,6 +199,11 @@ export const watchRuntimeComponents = (
|
|
|
242
199
|
|
|
243
200
|
/**
|
|
244
201
|
* Watches the runtime configuration for changes.
|
|
202
|
+
*
|
|
203
|
+
* Uses the generic {@link ConfigDiffStrategy} registry so that each entity
|
|
204
|
+
* kind (patterns today, potentially themes / slots later) supplies its own
|
|
205
|
+
* snapshot → diff → selective-rebuild logic without the watcher needing to
|
|
206
|
+
* know the details.
|
|
245
207
|
*/
|
|
246
208
|
export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState) => {
|
|
247
209
|
if (state.runtimeConfigurationWatcher) {
|
|
@@ -251,24 +213,47 @@ export const watchRuntimeConfiguration = (handoff: Handoff, state: WatcherState)
|
|
|
251
213
|
if (handoff.getConfigFilePaths().length > 0) {
|
|
252
214
|
state.runtimeConfigurationWatcher = chokidar.watch(handoff.getConfigFilePaths(), { ignoreInitial: true });
|
|
253
215
|
state.runtimeConfigurationWatcher.on('all', async (event, file) => {
|
|
216
|
+
const normalizedFile = normalizePathForCompare(file);
|
|
254
217
|
switch (event) {
|
|
255
218
|
case 'add':
|
|
256
219
|
case 'change':
|
|
257
220
|
case 'unlink':
|
|
258
|
-
|
|
259
|
-
state.debounce = true;
|
|
221
|
+
await scheduleHandler(state, `runtimeConfig:${normalizedFile}`, async () => {
|
|
260
222
|
try {
|
|
261
|
-
|
|
223
|
+
const changedFile = file;
|
|
224
|
+
|
|
225
|
+
// Look up the entity this config file belongs to BEFORE reload.
|
|
226
|
+
const entryBefore = handoff.getConfigFileEntry(changedFile);
|
|
227
|
+
const strategy = entryBefore ? getStrategy(entryBefore.kind) : undefined;
|
|
228
|
+
const handle =
|
|
229
|
+
entryBefore && strategy ? strategy.capture(handoff, entryBefore.entityId) : undefined;
|
|
230
|
+
|
|
262
231
|
handoff.reload();
|
|
263
232
|
await persistClientConfig(handoff);
|
|
264
233
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
265
|
-
|
|
234
|
+
watchRuntimeConfiguration(handoff, state);
|
|
235
|
+
|
|
236
|
+
const entryAfter = handoff.getConfigFileEntry(changedFile);
|
|
237
|
+
|
|
238
|
+
const normalizedChanged = normalizePathForCompare(changedFile);
|
|
239
|
+
const normalizedMainConfig = handoff.getMainConfigFilePath()
|
|
240
|
+
? normalizePathForCompare(handoff.getMainConfigFilePath() as string)
|
|
241
|
+
: undefined;
|
|
242
|
+
|
|
243
|
+
if (normalizedMainConfig && normalizedChanged === normalizedMainConfig) {
|
|
244
|
+
await processComponents(handoff);
|
|
245
|
+
} else if (handle) {
|
|
246
|
+
await handle.apply(handoff, entryAfter?.entityId);
|
|
247
|
+
} else {
|
|
248
|
+
const effectiveId = entryAfter?.entityId ?? path.basename(path.dirname(changedFile));
|
|
249
|
+
await processComponents(handoff, effectiveId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await runAllFinalizers(handoff);
|
|
266
253
|
} catch (e) {
|
|
267
254
|
Logger.error('Error reloading runtime configuration:', e);
|
|
268
|
-
} finally {
|
|
269
|
-
state.debounce = false;
|
|
270
255
|
}
|
|
271
|
-
}
|
|
256
|
+
});
|
|
272
257
|
break;
|
|
273
258
|
}
|
|
274
259
|
});
|
|
@@ -315,14 +300,12 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
|
|
|
315
300
|
|
|
316
301
|
if (!isNewComponent) return;
|
|
317
302
|
|
|
318
|
-
|
|
319
|
-
state.debounce = true;
|
|
303
|
+
await scheduleHandler(state, `newComponent:${dirName}`, async () => {
|
|
320
304
|
try {
|
|
321
305
|
Logger.warn(`New component detected: ${dirName}. Reloading configuration...`);
|
|
322
306
|
handoff.reload();
|
|
323
307
|
knownComponents.add(dirName);
|
|
324
308
|
|
|
325
|
-
// Update known set with all current components
|
|
326
309
|
for (const id of Object.keys(handoff.runtimeConfig?.entries?.components ?? {})) {
|
|
327
310
|
knownComponents.add(id);
|
|
328
311
|
}
|
|
@@ -331,11 +314,10 @@ export const watchComponentDirectories = (handoff: Handoff, state: WatcherState,
|
|
|
331
314
|
watchRuntimeComponents(handoff, state, getRuntimeComponentsPathsToWatch(handoff));
|
|
332
315
|
watchRuntimeConfiguration(handoff, state);
|
|
333
316
|
await processComponents(handoff, dirName);
|
|
317
|
+
await runAllFinalizers(handoff);
|
|
334
318
|
} catch (e) {
|
|
335
319
|
Logger.error('Error processing new component:', e);
|
|
336
|
-
} finally {
|
|
337
|
-
state.debounce = false;
|
|
338
320
|
}
|
|
339
|
-
}
|
|
321
|
+
});
|
|
340
322
|
});
|
|
341
323
|
};
|
package/src/cache/build-cache.ts
CHANGED
|
@@ -176,8 +176,8 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
|
|
|
176
176
|
files.push(entries.scss);
|
|
177
177
|
componentDirs.add(normalizePathForCompare(path.dirname(entries.scss)));
|
|
178
178
|
}
|
|
179
|
-
// Handle
|
|
180
|
-
const templatePath = entries.template || entries.templates;
|
|
179
|
+
// Handle template-style entries across renderer families
|
|
180
|
+
const templatePath = entries.template || entries.templates || entries.component || entries.story;
|
|
181
181
|
if (templatePath) {
|
|
182
182
|
try {
|
|
183
183
|
const stat = fs.statSync(templatePath);
|
|
@@ -198,10 +198,19 @@ export function getComponentFilePaths(handoff: Handoff, componentId: string): {
|
|
|
198
198
|
|
|
199
199
|
// Find the config file path for this component using exact config filename + directory matching.
|
|
200
200
|
const configPaths = handoff.getConfigFilePaths();
|
|
201
|
-
const expectedConfigFileNames = new Set([
|
|
201
|
+
const expectedConfigFileNames = new Set([
|
|
202
|
+
`${componentId}.handoff.ts`,
|
|
203
|
+
`${componentId}.handoff.js`,
|
|
204
|
+
`${componentId}.handoff.cjs`,
|
|
205
|
+
`${componentId}.handoff.json`,
|
|
206
|
+
`${componentId}.json`,
|
|
207
|
+
`${componentId}.js`,
|
|
208
|
+
`${componentId}.cjs`,
|
|
209
|
+
]);
|
|
202
210
|
const matchingConfigPath = configPaths.find((configPath) => {
|
|
203
211
|
const configFileName = path.basename(configPath);
|
|
204
|
-
|
|
212
|
+
const isModernDeclaration = /\.handoff\.(ts|js|cjs|json)$/.test(configFileName);
|
|
213
|
+
if (!expectedConfigFileNames.has(configFileName) && !isModernDeclaration) {
|
|
205
214
|
return false;
|
|
206
215
|
}
|
|
207
216
|
|
|
@@ -240,7 +249,7 @@ export async function computeComponentFileStates(
|
|
|
240
249
|
|
|
241
250
|
let templateDirFiles: Record<string, FileState> | undefined;
|
|
242
251
|
if (templateDir) {
|
|
243
|
-
templateDirFiles = await computeDirectoryState(templateDir, ['.hbs', '.html', '.tsx', '.ts']);
|
|
252
|
+
templateDirFiles = await computeDirectoryState(templateDir, ['.hbs', '.html', '.tsx', '.ts', '.jsx', '.js']);
|
|
244
253
|
}
|
|
245
254
|
|
|
246
255
|
return { files, templateDirFiles };
|
package/src/cli/create.ts
CHANGED
|
@@ -21,7 +21,9 @@ interface CreateOptions {
|
|
|
21
21
|
projectName: string;
|
|
22
22
|
figmaProjectId: string;
|
|
23
23
|
figmaAccessToken: string;
|
|
24
|
-
|
|
24
|
+
languagePreference: 'typescript' | 'javascript';
|
|
25
|
+
includeExamples: boolean;
|
|
26
|
+
[key: string]: string | boolean; // Allow dynamic access for template replacement
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
class CreateError extends Error {
|
|
@@ -63,23 +65,44 @@ interface TemplateConfig {
|
|
|
63
65
|
* Get mandatory templates based on whether examples are included
|
|
64
66
|
* Uses different handoff config templates for blank vs. with-examples projects
|
|
65
67
|
*/
|
|
66
|
-
const getMandatoryTemplates = (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
const getMandatoryTemplates = (
|
|
69
|
+
includeExamples: boolean,
|
|
70
|
+
languagePreference: 'typescript' | 'javascript'
|
|
71
|
+
): TemplateConfig[] => {
|
|
72
|
+
const configFormat = languagePreference;
|
|
73
|
+
const configTemplateBaseName = includeExamples ? 'handoff.config.with-examples' : 'handoff.config';
|
|
74
|
+
const configTemplateSource = `${configTemplateBaseName}.${configFormat === 'typescript' ? 'ts' : 'js'}.tpl`;
|
|
75
|
+
const configDestination = `handoff.config.${configFormat === 'typescript' ? 'ts' : 'js'}`;
|
|
76
|
+
|
|
77
|
+
const templates: TemplateConfig[] = [
|
|
78
|
+
{ source: 'package.json.tpl', destination: 'package.json' },
|
|
79
|
+
{
|
|
80
|
+
source: configTemplateSource,
|
|
81
|
+
destination: configDestination,
|
|
82
|
+
},
|
|
83
|
+
{ source: 'gitignore.tpl', destination: '.gitignore' },
|
|
84
|
+
{ source: 'env.tpl', destination: '.env' },
|
|
85
|
+
{ source: 'README.md.tpl', destination: 'README.md' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
if (languagePreference === 'typescript') {
|
|
89
|
+
templates.push({ source: 'tsconfig.json.tpl', destination: 'tsconfig.json' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return templates;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getExampleTemplates = (languagePreference: 'typescript' | 'javascript'): TemplateConfig[] => {
|
|
96
|
+
const declarationSuffix = languagePreference === 'typescript' ? 'ts' : 'js';
|
|
97
|
+
const declarationFile = `components/button/button.react.handoff.${declarationSuffix}.tpl`;
|
|
98
|
+
const componentFile =
|
|
99
|
+
languagePreference === 'typescript' ? 'components/button/Button.tsx.tpl' : 'components/button/Button.jsx.tpl';
|
|
100
|
+
|
|
101
|
+
return [
|
|
102
|
+
{ source: declarationFile, destination: `components/button/button.handoff.${languagePreference === 'typescript' ? 'ts' : 'js'}`, isExample: true },
|
|
103
|
+
{ source: componentFile, destination: `components/button/Button.${languagePreference === 'typescript' ? 'tsx' : 'jsx'}`, isExample: true },
|
|
104
|
+
];
|
|
105
|
+
};
|
|
83
106
|
|
|
84
107
|
/**
|
|
85
108
|
* Render a template string by replacing placeholders with values
|
|
@@ -89,19 +112,39 @@ const renderTemplate = (content: string, data: CreateOptions): string => {
|
|
|
89
112
|
if (!(key in data)) {
|
|
90
113
|
console.warn(chalk.yellow(`Warning: Unknown template placeholder '{{${key}}}'`));
|
|
91
114
|
}
|
|
92
|
-
|
|
115
|
+
const value = data[key];
|
|
116
|
+
return typeof value === 'string' ? value : '';
|
|
93
117
|
});
|
|
94
118
|
};
|
|
95
119
|
|
|
120
|
+
const getDependenciesToInstall = (options: CreateOptions): { dependencies: string[]; devDependencies: string[] } => {
|
|
121
|
+
const dependencies = ['handoff-app@latest'];
|
|
122
|
+
const devDependencies: string[] = [];
|
|
123
|
+
|
|
124
|
+
if (options.includeExamples) {
|
|
125
|
+
dependencies.push('react', 'react-dom');
|
|
126
|
+
if (options.languagePreference === 'typescript') {
|
|
127
|
+
devDependencies.push('@types/react', '@types/react-dom');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { dependencies, devDependencies };
|
|
132
|
+
};
|
|
133
|
+
|
|
96
134
|
/**
|
|
97
|
-
* Execute npm install
|
|
135
|
+
* Execute npm install for required dependencies.
|
|
98
136
|
*/
|
|
99
|
-
const
|
|
137
|
+
const installDependencies = async (projectPath: string, options: CreateOptions): Promise<void> => {
|
|
138
|
+
const { dependencies, devDependencies } = getDependenciesToInstall(options);
|
|
100
139
|
const spinner = p.spinner();
|
|
101
140
|
spinner.start('Installing dependencies...');
|
|
102
141
|
|
|
103
142
|
return new Promise((resolve, reject) => {
|
|
104
|
-
const
|
|
143
|
+
const installCommand = devDependencies.length
|
|
144
|
+
? `npm install --save-prefix="^" ${dependencies.join(' ')} && npm install --save-dev --save-prefix="^" ${devDependencies.join(' ')}`
|
|
145
|
+
: `npm install --save-prefix="^" ${dependencies.join(' ')}`;
|
|
146
|
+
|
|
147
|
+
const npmProcess = spawn(installCommand, {
|
|
105
148
|
cwd: projectPath,
|
|
106
149
|
stdio: 'pipe',
|
|
107
150
|
shell: true,
|
|
@@ -204,6 +247,22 @@ const create = async (): Promise<void> => {
|
|
|
204
247
|
|
|
205
248
|
const includeExamples = projectType === 'with-examples';
|
|
206
249
|
|
|
250
|
+
const languagePreferenceSelection = await p.select({
|
|
251
|
+
message: 'Preferred project language (declarations, examples, and handoff.config)?',
|
|
252
|
+
options: [
|
|
253
|
+
{ value: 'typescript', label: 'TypeScript', hint: 'Uses .handoff.ts and handoff.config.ts' },
|
|
254
|
+
{ value: 'javascript', label: 'JavaScript', hint: 'Uses .handoff.js and handoff.config.js' },
|
|
255
|
+
],
|
|
256
|
+
initialValue: 'typescript',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (p.isCancel(languagePreferenceSelection)) {
|
|
260
|
+
p.cancel('Project creation cancelled.');
|
|
261
|
+
process.exit(0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const languagePreference = languagePreferenceSelection as 'typescript' | 'javascript';
|
|
265
|
+
|
|
207
266
|
// Get Figma project ID
|
|
208
267
|
p.log.step(chalk.blue('Figma Configuration'));
|
|
209
268
|
const figmaProjectId = await p.text({
|
|
@@ -234,6 +293,8 @@ const create = async (): Promise<void> => {
|
|
|
234
293
|
projectName: projectName as string,
|
|
235
294
|
figmaProjectId: ((figmaProjectId as string) || '').trim(),
|
|
236
295
|
figmaAccessToken: ((figmaAccessToken as string) || '').trim(),
|
|
296
|
+
languagePreference,
|
|
297
|
+
includeExamples,
|
|
237
298
|
};
|
|
238
299
|
|
|
239
300
|
// Create files
|
|
@@ -248,8 +309,8 @@ const create = async (): Promise<void> => {
|
|
|
248
309
|
|
|
249
310
|
// Combine mandatory templates with example templates based on user choice
|
|
250
311
|
const templates = [
|
|
251
|
-
...getMandatoryTemplates(includeExamples),
|
|
252
|
-
...(includeExamples ?
|
|
312
|
+
...getMandatoryTemplates(includeExamples, languagePreference),
|
|
313
|
+
...(includeExamples ? getExampleTemplates(languagePreference) : []),
|
|
253
314
|
];
|
|
254
315
|
|
|
255
316
|
try {
|
|
@@ -273,7 +334,7 @@ const create = async (): Promise<void> => {
|
|
|
273
334
|
}
|
|
274
335
|
|
|
275
336
|
// Install dependencies
|
|
276
|
-
await
|
|
337
|
+
await installDependencies(projectPath, options);
|
|
277
338
|
|
|
278
339
|
// Success message
|
|
279
340
|
const nextSteps = [`cd ${projectName}`];
|