@trojanbox-vcp-test/site-edit-engine 0.1.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/README.md +85 -0
- package/dist/execute-integration/execute-fixture-harness.d.ts +25 -0
- package/dist/execute-integration/execute-fixture-harness.js +37 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/internal/ast/diagnostics/index.d.ts +5 -0
- package/dist/internal/ast/diagnostics/index.js +25 -0
- package/dist/internal/ast/history/index.d.ts +15 -0
- package/dist/internal/ast/history/index.js +62 -0
- package/dist/internal/ast/index.d.ts +8 -0
- package/dist/internal/ast/index.js +5 -0
- package/dist/internal/ast/locators/index.d.ts +1 -0
- package/dist/internal/ast/locators/index.js +1 -0
- package/dist/internal/ast/locators/resolve-locator.d.ts +16 -0
- package/dist/internal/ast/locators/resolve-locator.js +920 -0
- package/dist/internal/ast/parser/SourceParser.d.ts +30 -0
- package/dist/internal/ast/parser/SourceParser.js +49 -0
- package/dist/internal/ast/parser/index.d.ts +21 -0
- package/dist/internal/ast/parser/index.js +64 -0
- package/dist/internal/ast/primitives/conditional/conditional-primitives.d.ts +18 -0
- package/dist/internal/ast/primitives/conditional/conditional-primitives.js +237 -0
- package/dist/internal/ast/primitives/conditional/index.d.ts +1 -0
- package/dist/internal/ast/primitives/conditional/index.js +1 -0
- package/dist/internal/ast/primitives/imports/add-import.d.ts +18 -0
- package/dist/internal/ast/primitives/imports/add-import.js +111 -0
- package/dist/internal/ast/primitives/imports/index.d.ts +2 -0
- package/dist/internal/ast/primitives/imports/index.js +2 -0
- package/dist/internal/ast/primitives/imports/remove-import.d.ts +15 -0
- package/dist/internal/ast/primitives/imports/remove-import.js +72 -0
- package/dist/internal/ast/primitives/index.d.ts +10 -0
- package/dist/internal/ast/primitives/index.js +10 -0
- package/dist/internal/ast/primitives/jsx/index.d.ts +4 -0
- package/dist/internal/ast/primitives/jsx/index.js +4 -0
- package/dist/internal/ast/primitives/jsx/insert-child.d.ts +11 -0
- package/dist/internal/ast/primitives/jsx/insert-child.js +69 -0
- package/dist/internal/ast/primitives/jsx/move-node.d.ts +9 -0
- package/dist/internal/ast/primitives/jsx/move-node.js +76 -0
- package/dist/internal/ast/primitives/jsx/remove-node.d.ts +7 -0
- package/dist/internal/ast/primitives/jsx/remove-node.js +36 -0
- package/dist/internal/ast/primitives/jsx/update-text.d.ts +8 -0
- package/dist/internal/ast/primitives/jsx/update-text.js +81 -0
- package/dist/internal/ast/primitives/next/index.d.ts +1 -0
- package/dist/internal/ast/primitives/next/index.js +1 -0
- package/dist/internal/ast/primitives/next/next-primitives.d.ts +43 -0
- package/dist/internal/ast/primitives/next/next-primitives.js +211 -0
- package/dist/internal/ast/primitives/shared.d.ts +60 -0
- package/dist/internal/ast/primitives/shared.js +176 -0
- package/dist/internal/ast/primitives/style/class-expression.d.ts +23 -0
- package/dist/internal/ast/primitives/style/class-expression.js +174 -0
- package/dist/internal/ast/primitives/style/index.d.ts +1 -0
- package/dist/internal/ast/primitives/style/index.js +1 -0
- package/dist/internal/ast/primitives/style/style-primitives.d.ts +49 -0
- package/dist/internal/ast/primitives/style/style-primitives.js +555 -0
- package/dist/internal/ast/primitives/values/index.d.ts +1 -0
- package/dist/internal/ast/primitives/values/index.js +1 -0
- package/dist/internal/ast/primitives/values/value-primitives.d.ts +42 -0
- package/dist/internal/ast/primitives/values/value-primitives.js +158 -0
- package/dist/internal/ast/printer/SourcePrinter.d.ts +21 -0
- package/dist/internal/ast/printer/SourcePrinter.js +76 -0
- package/dist/internal/ast/printer/index.d.ts +6 -0
- package/dist/internal/ast/printer/index.js +126 -0
- package/dist/internal/ast/types.d.ts +190 -0
- package/dist/internal/ast/types.js +1 -0
- package/dist/internal/capability/capability-resolver.d.ts +16 -0
- package/dist/internal/capability/capability-resolver.js +127 -0
- package/dist/internal/classname-source.d.ts +24 -0
- package/dist/internal/classname-source.js +220 -0
- package/dist/internal/contracts/IEditEngineRuntime.d.ts +18 -0
- package/dist/internal/contracts/IEditEngineRuntime.js +1 -0
- package/dist/internal/domain/EditDiagnostic.d.ts +38 -0
- package/dist/internal/domain/EditDiagnostic.js +43 -0
- package/dist/internal/events/event-bus.d.ts +14 -0
- package/dist/internal/events/event-bus.js +21 -0
- package/dist/internal/graph/graph-builder.d.ts +12 -0
- package/dist/internal/graph/graph-builder.js +1371 -0
- package/dist/internal/graph/import-resolver.d.ts +31 -0
- package/dist/internal/graph/import-resolver.js +109 -0
- package/dist/internal/graph/project-graph-builder.d.ts +32 -0
- package/dist/internal/graph/project-graph-builder.js +133 -0
- package/dist/internal/graph/types.d.ts +114 -0
- package/dist/internal/graph/types.js +6 -0
- package/dist/internal/history/undo-redo.d.ts +28 -0
- package/dist/internal/history/undo-redo.js +42 -0
- package/dist/internal/index.d.ts +2 -0
- package/dist/internal/index.js +1 -0
- package/dist/internal/planner/planner.d.ts +104 -0
- package/dist/internal/planner/planner.js +2533 -0
- package/dist/internal/planner/types.d.ts +275 -0
- package/dist/internal/planner/types.js +6 -0
- package/dist/internal/protocol/boundary.d.ts +10 -0
- package/dist/internal/protocol/boundary.js +3 -0
- package/dist/internal/protocol/capability.d.ts +47 -0
- package/dist/internal/protocol/capability.js +8 -0
- package/dist/internal/protocol/error.d.ts +43 -0
- package/dist/internal/protocol/error.js +38 -0
- package/dist/internal/protocol/event.d.ts +39 -0
- package/dist/internal/protocol/event.js +3 -0
- package/dist/internal/protocol/identity.d.ts +26 -0
- package/dist/internal/protocol/identity.js +30 -0
- package/dist/internal/protocol/operation.d.ts +224 -0
- package/dist/internal/protocol/operation.js +8 -0
- package/dist/internal/protocol/render.d.ts +212 -0
- package/dist/internal/protocol/render.js +3 -0
- package/dist/internal/protocol.d.ts +9 -0
- package/dist/internal/protocol.js +2 -0
- package/dist/internal/provenance/binding-graph.d.ts +39 -0
- package/dist/internal/provenance/binding-graph.js +184 -0
- package/dist/internal/provenance/capability-policy.d.ts +15 -0
- package/dist/internal/provenance/capability-policy.js +96 -0
- package/dist/internal/provenance/data-source-classifier.d.ts +14 -0
- package/dist/internal/provenance/data-source-classifier.js +281 -0
- package/dist/internal/provenance/resolve-text-provenance.d.ts +45 -0
- package/dist/internal/provenance/resolve-text-provenance.js +3090 -0
- package/dist/internal/provenance/types.d.ts +89 -0
- package/dist/internal/provenance/types.js +1 -0
- package/dist/internal/render/component-semantic.d.ts +11 -0
- package/dist/internal/render/component-semantic.js +141 -0
- package/dist/internal/render/content-model.d.ts +3 -0
- package/dist/internal/render/content-model.js +89 -0
- package/dist/internal/render/media-model.d.ts +3 -0
- package/dist/internal/render/media-model.js +45 -0
- package/dist/internal/render/provenance-types.d.ts +33 -0
- package/dist/internal/render/provenance-types.js +1 -0
- package/dist/internal/render/render-projection.d.ts +24 -0
- package/dist/internal/render/render-projection.js +281 -0
- package/dist/internal/render/tailwind-style-model.d.ts +19 -0
- package/dist/internal/render/tailwind-style-model.js +1187 -0
- package/dist/internal/runtime/EditEngineRuntime.d.ts +25 -0
- package/dist/internal/runtime/EditEngineRuntime.js +89 -0
- package/dist/internal/runtime/EditEngineRuntimeSnapshot.d.ts +31 -0
- package/dist/internal/runtime/EditEngineRuntimeSnapshot.js +15 -0
- package/dist/internal/runtime/InternalEditEngine.d.ts +44 -0
- package/dist/internal/runtime/InternalEditEngine.js +1391 -0
- package/dist/internal/runtime.d.ts +3 -0
- package/dist/internal/runtime.js +1 -0
- package/dist/internal/topology/topology.d.ts +6 -0
- package/dist/internal/topology/topology.js +98 -0
- package/dist/internal/topology/types.d.ts +35 -0
- package/dist/internal/topology/types.js +5 -0
- package/dist/internal/types.d.ts +1 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/writeback/in-memory-fs.d.ts +7 -0
- package/dist/internal/writeback/in-memory-fs.js +44 -0
- package/dist/internal/writeback/types.d.ts +45 -0
- package/dist/internal/writeback/types.js +7 -0
- package/dist/internal/writeback/writeback-service.d.ts +7 -0
- package/dist/internal/writeback/writeback-service.js +568 -0
- package/dist/internal-adapter.d.ts +18 -0
- package/dist/internal-adapter.js +350 -0
- package/dist/next-app-router-fs.d.ts +2 -0
- package/dist/next-app-router-fs.js +64 -0
- package/dist/next-app-router.d.ts +11 -0
- package/dist/next-app-router.js +140 -0
- package/dist/preview-runtime.d.ts +394 -0
- package/dist/preview-runtime.js +102 -0
- package/dist/public-file-system.d.ts +7 -0
- package/dist/public-file-system.js +1 -0
- package/dist/runtime-sync.d.ts +95 -0
- package/dist/runtime-sync.js +321 -0
- package/dist/runtime.d.ts +340 -0
- package/dist/runtime.js +134 -0
- package/dist/site-edit-instrumentation.d.ts +19 -0
- package/dist/site-edit-instrumentation.js +322 -0
- package/dist/snapshot-file-system.d.ts +19 -0
- package/dist/snapshot-file-system.js +49 -0
- package/dist/source-watcher.d.ts +20 -0
- package/dist/source-watcher.js +150 -0
- package/dist/source-writeback-test-harness.d.ts +244 -0
- package/dist/source-writeback-test-harness.js +119 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/dist/webpack-loader.cjs +592 -0
- package/dist/webpack-loader.d.ts +27 -0
- package/package.json +66 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { SiteEditPatchPlanSchema } from "@trojanbox-vcp-test/contracts";
|
|
2
|
+
import { toInternalOperationRequest, toSiteEditEvent, toSiteEditObjectContentDetail, toSiteEditObjectDetail, toSiteEditObjectEditContext, toSiteEditObjectMediaDetail, toSiteEditObjectStyleDetail, toSiteEditObjectSummary, toSiteEditOperationResult, toSiteEditRenderDocument, } from "./internal-adapter.js";
|
|
3
|
+
import { createInternalEditEngine } from "./internal/runtime/InternalEditEngine.js";
|
|
4
|
+
import { sha256ContentHash, SnapshotFileSystem, } from "./snapshot-file-system.js";
|
|
5
|
+
export class SnapshotSiteEditEngineRuntime {
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
this.fileSystem = new SnapshotFileSystem(options.files);
|
|
9
|
+
this.engine = this.createEngine(this.fileSystem);
|
|
10
|
+
}
|
|
11
|
+
get status() {
|
|
12
|
+
return this.engine.status;
|
|
13
|
+
}
|
|
14
|
+
async setActiveRoute(routeId) {
|
|
15
|
+
await this.engine.setActiveRoute(routeId);
|
|
16
|
+
}
|
|
17
|
+
getDocument(routeId) {
|
|
18
|
+
return toSiteEditRenderDocument(this.engine.getDocument(routeId), {
|
|
19
|
+
siteId: this.options.siteId,
|
|
20
|
+
snapshotId: this.options.snapshotId,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
getObjectCapabilities(key, routeId) {
|
|
24
|
+
const detail = toSiteEditObjectDetail(this.engine.getRenderDetail(key, routeId));
|
|
25
|
+
return {
|
|
26
|
+
key: detail.key,
|
|
27
|
+
canUpdateText: detail.capabilities.canUpdateText,
|
|
28
|
+
canInsertChild: detail.capabilities.canInsertChild,
|
|
29
|
+
canMove: detail.capabilities.canMove,
|
|
30
|
+
canRemove: detail.capabilities.canRemove,
|
|
31
|
+
boundaryKind: detail.boundaryKind,
|
|
32
|
+
isOpaque: detail.isOpaque,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
getObjectSummary(key, routeId) {
|
|
36
|
+
return toSiteEditObjectSummary(this.engine.getRenderDetail(key, routeId));
|
|
37
|
+
}
|
|
38
|
+
getObjectEditContext(key, routeId) {
|
|
39
|
+
return toSiteEditObjectEditContext(this.engine.getRenderDetail(key, routeId));
|
|
40
|
+
}
|
|
41
|
+
getObjectStyleDetail(key, routeId) {
|
|
42
|
+
return toSiteEditObjectStyleDetail(this.engine.getRenderDetail(key, routeId));
|
|
43
|
+
}
|
|
44
|
+
getObjectContentDetail(key, routeId) {
|
|
45
|
+
return toSiteEditObjectContentDetail(this.engine.getRenderDetail(key, routeId));
|
|
46
|
+
}
|
|
47
|
+
getObjectMediaDetail(key, routeId) {
|
|
48
|
+
return toSiteEditObjectMediaDetail(this.engine.getRenderDetail(key, routeId));
|
|
49
|
+
}
|
|
50
|
+
getClassNameSource(key, routeId) {
|
|
51
|
+
return this.engine.getClassNameSource(key, routeId);
|
|
52
|
+
}
|
|
53
|
+
setClassNameSource(request) {
|
|
54
|
+
return this.engine.setClassNameSource(request);
|
|
55
|
+
}
|
|
56
|
+
async planOperation(request) {
|
|
57
|
+
const scratchFileSystem = new SnapshotFileSystem(this.options.files);
|
|
58
|
+
const scratchEngine = this.createEngine(scratchFileSystem);
|
|
59
|
+
for (const route of this.options.routes) {
|
|
60
|
+
await scratchEngine.setActiveRoute(route.routeId);
|
|
61
|
+
}
|
|
62
|
+
const result = toSiteEditOperationResult(await scratchEngine.execute(toInternalOperationRequest(request)));
|
|
63
|
+
if (!result.ok) {
|
|
64
|
+
return { result };
|
|
65
|
+
}
|
|
66
|
+
const changedFiles = scratchFileSystem.getChangedFiles();
|
|
67
|
+
if (changedFiles.length === 0) {
|
|
68
|
+
return { result };
|
|
69
|
+
}
|
|
70
|
+
const patchPlan = SiteEditPatchPlanSchema.parse({
|
|
71
|
+
siteId: request.siteId,
|
|
72
|
+
baseSnapshotId: request.baseSnapshotId,
|
|
73
|
+
operationId: request.id,
|
|
74
|
+
reason: `site-edit:${request.kind}`,
|
|
75
|
+
patches: changedFiles.map((file) => ({
|
|
76
|
+
path: file.path,
|
|
77
|
+
operation: file.before === undefined ? "create" : "update",
|
|
78
|
+
expectedHash: file.before === undefined
|
|
79
|
+
? undefined
|
|
80
|
+
: sha256ContentHash(file.before),
|
|
81
|
+
fullContent: file.after,
|
|
82
|
+
})),
|
|
83
|
+
changedRouteIds: this.getChangedRouteIds(changedFiles.map((file) => file.path)),
|
|
84
|
+
diagnostics: [],
|
|
85
|
+
});
|
|
86
|
+
this.options.onEvent?.({
|
|
87
|
+
type: "site-edit.operation.planned",
|
|
88
|
+
siteId: request.siteId,
|
|
89
|
+
operationId: request.id,
|
|
90
|
+
baseSnapshotId: request.baseSnapshotId,
|
|
91
|
+
changedFiles: patchPlan.patches.map((patch) => patch.path),
|
|
92
|
+
at: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
return { result, patchPlan };
|
|
95
|
+
}
|
|
96
|
+
dispose() {
|
|
97
|
+
this.engine.dispose();
|
|
98
|
+
}
|
|
99
|
+
createEngine(fileSystem) {
|
|
100
|
+
return createInternalEditEngine({
|
|
101
|
+
projectRoot: "/snapshot",
|
|
102
|
+
fileSystem,
|
|
103
|
+
routes: this.options.routes.map((route) => ({
|
|
104
|
+
routeId: route.routeId,
|
|
105
|
+
entryFile: route.entryFile,
|
|
106
|
+
})),
|
|
107
|
+
onEvent: this.options.onEvent
|
|
108
|
+
? (event) => {
|
|
109
|
+
const siteEditEvent = toSiteEditEvent(event, {
|
|
110
|
+
siteId: this.options.siteId,
|
|
111
|
+
snapshotId: this.options.snapshotId,
|
|
112
|
+
});
|
|
113
|
+
if (siteEditEvent) {
|
|
114
|
+
this.options.onEvent?.(siteEditEvent);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
: undefined,
|
|
118
|
+
dev: this.options.dev,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
getChangedRouteIds(changedPaths) {
|
|
122
|
+
const changedPathSet = new Set(changedPaths);
|
|
123
|
+
const directlyChangedRoutes = this.options.routes
|
|
124
|
+
.filter((route) => changedPathSet.has(route.entryFile))
|
|
125
|
+
.map((route) => route.routeId);
|
|
126
|
+
if (directlyChangedRoutes.length > 0) {
|
|
127
|
+
return directlyChangedRoutes;
|
|
128
|
+
}
|
|
129
|
+
return this.options.routes.map((route) => route.routeId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export function createSiteEditEngineRuntime(options) {
|
|
133
|
+
return new SnapshotSiteEditEngineRuntime(options);
|
|
134
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SiteEditRuntimeAttributes } from "./runtime-sync.js";
|
|
2
|
+
export type SiteEditInstrumentationOptions = {
|
|
3
|
+
source: string;
|
|
4
|
+
file: string;
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
attributes?: SiteEditRuntimeAttributes;
|
|
7
|
+
};
|
|
8
|
+
export type SiteEditInstrumentedElement = {
|
|
9
|
+
key: string;
|
|
10
|
+
tag: string;
|
|
11
|
+
componentName: string;
|
|
12
|
+
sourceFile: string;
|
|
13
|
+
source_kind: "jsx" | "component-call";
|
|
14
|
+
};
|
|
15
|
+
export type SiteEditInstrumentationResult = {
|
|
16
|
+
code: string;
|
|
17
|
+
elements: SiteEditInstrumentedElement[];
|
|
18
|
+
};
|
|
19
|
+
export declare function instrumentSiteEditRuntimeAttributes(options: SiteEditInstrumentationOptions): SiteEditInstrumentationResult;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import generateModule from "@babel/generator";
|
|
2
|
+
import * as parser from "@babel/parser";
|
|
3
|
+
import _traverse from "@babel/traverse";
|
|
4
|
+
import * as t from "@babel/types";
|
|
5
|
+
import { computeSiteEditSourceIdentityKey } from "@trojanbox-vcp-test/contracts";
|
|
6
|
+
import { SITE_EDIT_RUNTIME_ATTRIBUTES } from "./runtime-sync.js";
|
|
7
|
+
const generate = (typeof generateModule === "function"
|
|
8
|
+
? generateModule
|
|
9
|
+
: generateModule.default);
|
|
10
|
+
const traverse = (typeof _traverse === "function" ? _traverse : _traverse.default);
|
|
11
|
+
const NON_EDITABLE_INTRINSIC_ELEMENTS = new Set([
|
|
12
|
+
"base",
|
|
13
|
+
"body",
|
|
14
|
+
"head",
|
|
15
|
+
"html",
|
|
16
|
+
"link",
|
|
17
|
+
"meta",
|
|
18
|
+
"noscript",
|
|
19
|
+
"script",
|
|
20
|
+
"style",
|
|
21
|
+
"template",
|
|
22
|
+
"title",
|
|
23
|
+
]);
|
|
24
|
+
const SUPPRESS_HYDRATION_WARNING_PROP = "suppressHydrationWarning";
|
|
25
|
+
export function instrumentSiteEditRuntimeAttributes(options) {
|
|
26
|
+
if (!mayContainJsx(options.source)) {
|
|
27
|
+
return { code: options.source, elements: [] };
|
|
28
|
+
}
|
|
29
|
+
const attributes = options.attributes ?? SITE_EDIT_RUNTIME_ATTRIBUTES;
|
|
30
|
+
const ast = parser.parse(options.source, {
|
|
31
|
+
sourceType: "module",
|
|
32
|
+
plugins: ["jsx", "typescript"],
|
|
33
|
+
});
|
|
34
|
+
const elements = [];
|
|
35
|
+
const sourceFile = toProjectRelativeFile(options.file, options.projectRoot);
|
|
36
|
+
traverse(ast, {
|
|
37
|
+
JSXElement(path) {
|
|
38
|
+
const opening = path.node.openingElement;
|
|
39
|
+
const name = opening.name;
|
|
40
|
+
if (!isSiteEditMarkableElement(name) ||
|
|
41
|
+
hasAttribute(opening, attributes.key)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const componentName = findOwningComponentName(path) ?? "default";
|
|
45
|
+
const structuralPath = computeStructuralPathFromAst(path);
|
|
46
|
+
const key = computeSiteEditSourceIdentityKey(sourceFile, componentName, structuralPath);
|
|
47
|
+
const tag = getJsxElementName(name) ?? "unknown";
|
|
48
|
+
const sourceKind = isConcreteElement(name) ? "jsx" : "component-call";
|
|
49
|
+
if (sourceKind === "component-call" &&
|
|
50
|
+
!hasStructuredChild(path.node.children)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
appendStringAttribute(opening, attributes.key, key);
|
|
54
|
+
appendBooleanAttribute(opening, SUPPRESS_HYDRATION_WARNING_PROP);
|
|
55
|
+
elements.push({
|
|
56
|
+
key,
|
|
57
|
+
tag,
|
|
58
|
+
componentName: componentName,
|
|
59
|
+
sourceFile: sourceFile,
|
|
60
|
+
source_kind: sourceKind,
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
code: generate(ast, { jsescOption: { minimal: true } }).code,
|
|
66
|
+
elements,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function mayContainJsx(source) {
|
|
70
|
+
return source.includes("<");
|
|
71
|
+
}
|
|
72
|
+
function toProjectRelativeFile(filename, projectRoot) {
|
|
73
|
+
const normalized = filename.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
74
|
+
const normalizedRoot = projectRoot?.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
75
|
+
if (normalizedRoot && normalized.startsWith(`${normalizedRoot}/`)) {
|
|
76
|
+
return normalized.slice(normalizedRoot.length + 1);
|
|
77
|
+
}
|
|
78
|
+
for (const [marker, offset] of [
|
|
79
|
+
["/src/app/", 1],
|
|
80
|
+
["/src/components/", 1],
|
|
81
|
+
["/app/", 1],
|
|
82
|
+
["/components/", 1],
|
|
83
|
+
]) {
|
|
84
|
+
const index = normalized.indexOf(marker);
|
|
85
|
+
if (index >= 0) {
|
|
86
|
+
return normalized.slice(index + offset);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const fallback = normalized.match(/(?:^|\/)((?:src\/app|src\/components|app|components)\/.+)$/);
|
|
90
|
+
return fallback?.[1] ?? normalized;
|
|
91
|
+
}
|
|
92
|
+
function isConcreteElement(name) {
|
|
93
|
+
return t.isJSXIdentifier(name) && /^[a-z]/.test(name.name);
|
|
94
|
+
}
|
|
95
|
+
function getJsxElementName(name) {
|
|
96
|
+
if (t.isJSXIdentifier(name)) {
|
|
97
|
+
return name.name;
|
|
98
|
+
}
|
|
99
|
+
if (t.isJSXMemberExpression(name)) {
|
|
100
|
+
return getJsxMemberExpressionName(name);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function isSiteEditMarkableElement(name) {
|
|
105
|
+
const elementName = getJsxElementName(name);
|
|
106
|
+
if (!elementName ||
|
|
107
|
+
elementName === "Fragment" ||
|
|
108
|
+
elementName.endsWith(".Fragment")) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return !NON_EDITABLE_INTRINSIC_ELEMENTS.has(elementName);
|
|
112
|
+
}
|
|
113
|
+
function hasAttribute(opening, name) {
|
|
114
|
+
return opening.attributes.some((attribute) => t.isJSXAttribute(attribute) &&
|
|
115
|
+
t.isJSXIdentifier(attribute.name, { name }));
|
|
116
|
+
}
|
|
117
|
+
function appendStringAttribute(opening, name, value) {
|
|
118
|
+
opening.attributes.push(t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value)));
|
|
119
|
+
}
|
|
120
|
+
function appendOptionalStringAttribute(opening, name, value) {
|
|
121
|
+
if (value === undefined || hasAttribute(opening, name)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
appendStringAttribute(opening, name, value);
|
|
125
|
+
}
|
|
126
|
+
function appendBooleanAttribute(opening, name) {
|
|
127
|
+
if (hasAttribute(opening, name)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
opening.attributes.push(t.jsxAttribute(t.jsxIdentifier(name), null));
|
|
131
|
+
}
|
|
132
|
+
function getJsxMemberExpressionName(name) {
|
|
133
|
+
const objectName = t.isJSXIdentifier(name.object)
|
|
134
|
+
? name.object.name
|
|
135
|
+
: getJsxMemberExpressionName(name.object);
|
|
136
|
+
return `${objectName}.${name.property.name}`;
|
|
137
|
+
}
|
|
138
|
+
function findOwningComponentName(path) {
|
|
139
|
+
let current = path;
|
|
140
|
+
while (current) {
|
|
141
|
+
if (current.isFunctionDeclaration() || current.isClassDeclaration()) {
|
|
142
|
+
return current.node.id?.name ?? null;
|
|
143
|
+
}
|
|
144
|
+
if (current.isVariableDeclarator() && t.isIdentifier(current.node.id)) {
|
|
145
|
+
const init = current.get("init");
|
|
146
|
+
if (!Array.isArray(init) &&
|
|
147
|
+
(init.isArrowFunctionExpression() || init.isFunctionExpression())) {
|
|
148
|
+
return current.node.id.name;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
current = current.parentPath;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function hasStructuredChild(children) {
|
|
156
|
+
return children.some((child) => t.isJSXElement(child) || t.isJSXFragment(child));
|
|
157
|
+
}
|
|
158
|
+
const structuralPathCache = new WeakMap();
|
|
159
|
+
const structuralChildrenCache = new WeakMap();
|
|
160
|
+
const siblingOrdinalCache = new WeakMap();
|
|
161
|
+
function computeStructuralPathFromAst(path) {
|
|
162
|
+
const cached = structuralPathCache.get(path.node);
|
|
163
|
+
if (cached !== undefined) {
|
|
164
|
+
return cached;
|
|
165
|
+
}
|
|
166
|
+
const segments = [];
|
|
167
|
+
let current = path;
|
|
168
|
+
while (current) {
|
|
169
|
+
if (current.isJSXElement()) {
|
|
170
|
+
const segment = getStructuralSegment(current);
|
|
171
|
+
if (segment) {
|
|
172
|
+
segments.unshift(segment);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
current = current.parentPath;
|
|
176
|
+
}
|
|
177
|
+
const structuralPath = segments.join("/");
|
|
178
|
+
structuralPathCache.set(path.node, structuralPath);
|
|
179
|
+
return structuralPath;
|
|
180
|
+
}
|
|
181
|
+
function getStructuralSegment(path) {
|
|
182
|
+
const tag = getJsxElementName(path.node.openingElement.name);
|
|
183
|
+
if (!tag || tag === "Fragment" || tag.endsWith(".Fragment")) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (/^[A-Z]/.test(tag)) {
|
|
187
|
+
return `component:${tag}#${computeSiblingOrdinal(path, tag)}`;
|
|
188
|
+
}
|
|
189
|
+
return `${tag}#${computeSiblingOrdinal(path, tag)}`;
|
|
190
|
+
}
|
|
191
|
+
function computeSiblingOrdinal(path, tag) {
|
|
192
|
+
const owner = findStructuralParent(path);
|
|
193
|
+
if (!owner) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
const cached = siblingOrdinalCache.get(owner.node)?.get(tag)?.get(path.node);
|
|
197
|
+
if (cached !== undefined) {
|
|
198
|
+
return cached;
|
|
199
|
+
}
|
|
200
|
+
let ownerCache = siblingOrdinalCache.get(owner.node);
|
|
201
|
+
if (!ownerCache) {
|
|
202
|
+
ownerCache = new Map();
|
|
203
|
+
siblingOrdinalCache.set(owner.node, ownerCache);
|
|
204
|
+
}
|
|
205
|
+
let tagOrdinals = ownerCache.get(tag);
|
|
206
|
+
if (!tagOrdinals) {
|
|
207
|
+
tagOrdinals = new WeakMap();
|
|
208
|
+
ownerCache.set(tag, tagOrdinals);
|
|
209
|
+
}
|
|
210
|
+
let ordinal = 0;
|
|
211
|
+
for (const sibling of collectStructuralChildren(owner)) {
|
|
212
|
+
const siblingTag = getJsxElementName(sibling.node.openingElement.name);
|
|
213
|
+
if (siblingTag === tag) {
|
|
214
|
+
tagOrdinals.set(sibling.node, ordinal);
|
|
215
|
+
if (sibling.node === path.node) {
|
|
216
|
+
return ordinal;
|
|
217
|
+
}
|
|
218
|
+
ordinal += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return tagOrdinals.get(path.node) ?? 0;
|
|
222
|
+
}
|
|
223
|
+
function findStructuralParent(path) {
|
|
224
|
+
let current = path.parentPath;
|
|
225
|
+
let fragmentOwner = null;
|
|
226
|
+
while (current) {
|
|
227
|
+
if (current.isJSXFragment()) {
|
|
228
|
+
fragmentOwner = current;
|
|
229
|
+
current = current.parentPath;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (current.isJSXElement()) {
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
current = current.parentPath;
|
|
236
|
+
}
|
|
237
|
+
return fragmentOwner;
|
|
238
|
+
}
|
|
239
|
+
function collectStructuralChildren(owner) {
|
|
240
|
+
const cached = structuralChildrenCache.get(owner.node);
|
|
241
|
+
if (cached) {
|
|
242
|
+
return cached;
|
|
243
|
+
}
|
|
244
|
+
const structuralChildren = [];
|
|
245
|
+
const children = owner.get("children");
|
|
246
|
+
for (const child of children) {
|
|
247
|
+
if (Array.isArray(child)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (child.isJSXElement()) {
|
|
251
|
+
structuralChildren.push(child);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (child.isJSXFragment()) {
|
|
255
|
+
structuralChildren.push(...collectStructuralChildren(child));
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (child.isJSXExpressionContainer()) {
|
|
259
|
+
structuralChildren.push(...collectExpressionStructuralChildren(child.get("expression")));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
structuralChildrenCache.set(owner.node, structuralChildren);
|
|
263
|
+
return structuralChildren;
|
|
264
|
+
}
|
|
265
|
+
function collectExpressionStructuralChildren(expression) {
|
|
266
|
+
if (expression.isJSXElement()) {
|
|
267
|
+
return [expression];
|
|
268
|
+
}
|
|
269
|
+
if (expression.isJSXFragment()) {
|
|
270
|
+
return collectStructuralChildren(expression);
|
|
271
|
+
}
|
|
272
|
+
if (expression.isParenthesizedExpression()) {
|
|
273
|
+
return collectExpressionStructuralChildren(expression.get("expression"));
|
|
274
|
+
}
|
|
275
|
+
if (expression.isLogicalExpression() && expression.node.operator === "&&") {
|
|
276
|
+
return collectExpressionStructuralChildren(expression.get("right"));
|
|
277
|
+
}
|
|
278
|
+
if (expression.isConditionalExpression()) {
|
|
279
|
+
return [
|
|
280
|
+
...collectExpressionStructuralChildren(expression.get("consequent")),
|
|
281
|
+
...collectExpressionStructuralChildren(expression.get("alternate")),
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
if (expression.isCallExpression() && isMapCallExpression(expression.node)) {
|
|
285
|
+
const callback = expression.get("arguments")[0];
|
|
286
|
+
return collectFunctionReturnStructuralChildren(callback);
|
|
287
|
+
}
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
function collectFunctionReturnStructuralChildren(path) {
|
|
291
|
+
if (!path.isArrowFunctionExpression() && !path.isFunctionExpression()) {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
const body = path.get("body");
|
|
295
|
+
if (body.isJSXElement()) {
|
|
296
|
+
return [body];
|
|
297
|
+
}
|
|
298
|
+
if (body.isJSXFragment()) {
|
|
299
|
+
return collectStructuralChildren(body);
|
|
300
|
+
}
|
|
301
|
+
if (body.isParenthesizedExpression()) {
|
|
302
|
+
return collectExpressionStructuralChildren(body.get("expression"));
|
|
303
|
+
}
|
|
304
|
+
if (!body.isBlockStatement()) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
for (const statement of body.get("body")) {
|
|
308
|
+
if (!statement.isReturnStatement()) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const argument = statement.get("argument");
|
|
312
|
+
return argument.node
|
|
313
|
+
? collectExpressionStructuralChildren(argument)
|
|
314
|
+
: [];
|
|
315
|
+
}
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
function isMapCallExpression(node) {
|
|
319
|
+
return (t.isMemberExpression(node.callee) &&
|
|
320
|
+
t.isIdentifier(node.callee.property) &&
|
|
321
|
+
node.callee.property.name === "map");
|
|
322
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProjectFileSystem } from "./internal/types.js";
|
|
2
|
+
import type { SiteEditSnapshotFile } from "./types.js";
|
|
3
|
+
export declare class SnapshotFileSystem implements ProjectFileSystem {
|
|
4
|
+
private readonly original;
|
|
5
|
+
private readonly files;
|
|
6
|
+
private readonly versions;
|
|
7
|
+
constructor(files: SiteEditSnapshotFile[]);
|
|
8
|
+
readFile(file: string): Promise<string>;
|
|
9
|
+
writeFile(file: string, content: string): Promise<void>;
|
|
10
|
+
exists(file: string): Promise<boolean>;
|
|
11
|
+
getVersion(file: string): number;
|
|
12
|
+
bumpVersion(file: string): number;
|
|
13
|
+
getChangedFiles(): Array<{
|
|
14
|
+
path: string;
|
|
15
|
+
before?: string;
|
|
16
|
+
after?: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export declare function sha256ContentHash(content: string): `sha256:${string}`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export class SnapshotFileSystem {
|
|
3
|
+
constructor(files) {
|
|
4
|
+
this.original = new Map();
|
|
5
|
+
this.files = new Map();
|
|
6
|
+
this.versions = new Map();
|
|
7
|
+
for (const file of files) {
|
|
8
|
+
this.original.set(file.path, file.content);
|
|
9
|
+
this.files.set(file.path, file.content);
|
|
10
|
+
this.versions.set(file.path, file.version ?? 0);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async readFile(file) {
|
|
14
|
+
const content = this.files.get(file);
|
|
15
|
+
if (content === undefined) {
|
|
16
|
+
throw new Error(`Snapshot file not found: ${file}`);
|
|
17
|
+
}
|
|
18
|
+
return content;
|
|
19
|
+
}
|
|
20
|
+
async writeFile(file, content) {
|
|
21
|
+
this.files.set(file, content);
|
|
22
|
+
}
|
|
23
|
+
async exists(file) {
|
|
24
|
+
return this.files.has(file);
|
|
25
|
+
}
|
|
26
|
+
getVersion(file) {
|
|
27
|
+
return this.versions.get(file) ?? 0;
|
|
28
|
+
}
|
|
29
|
+
bumpVersion(file) {
|
|
30
|
+
const next = this.getVersion(file) + 1;
|
|
31
|
+
this.versions.set(file, next);
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
getChangedFiles() {
|
|
35
|
+
const paths = new Set([...this.original.keys(), ...this.files.keys()]);
|
|
36
|
+
const changes = [];
|
|
37
|
+
for (const path of [...paths].sort()) {
|
|
38
|
+
const before = this.original.get(path);
|
|
39
|
+
const after = this.files.get(path);
|
|
40
|
+
if (before !== after) {
|
|
41
|
+
changes.push({ path, before, after });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return changes;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function sha256ContentHash(content) {
|
|
48
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type SiteEditSourceWatcherOptions = {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
watch_dirs?: string[];
|
|
4
|
+
interval_ms?: number;
|
|
5
|
+
on_change(file: string): void;
|
|
6
|
+
};
|
|
7
|
+
export declare class SiteEditSourceWatcher {
|
|
8
|
+
private readonly options;
|
|
9
|
+
private readonly watchers;
|
|
10
|
+
private readonly fileMtimes;
|
|
11
|
+
private pollingTimer;
|
|
12
|
+
private isPolling;
|
|
13
|
+
constructor(options: SiteEditSourceWatcherOptions);
|
|
14
|
+
start(): () => void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
private getWatchDirs;
|
|
17
|
+
private registerWatcher;
|
|
18
|
+
private poll;
|
|
19
|
+
private scanFiles;
|
|
20
|
+
}
|