mapples 0.2.0-beta.3 → 0.2.0-beta.5
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/dist/api/objects.d.ts +3 -1
- package/dist/api/objects.js +24 -1
- package/dist/api/objects.js.map +1 -1
- package/dist/api/txPlan.d.ts +15 -0
- package/dist/api/txPlan.js +196 -0
- package/dist/api/txPlan.js.map +1 -0
- package/dist/codegen/emit.d.ts +2 -31
- package/dist/codegen/emit.js +7 -147
- package/dist/codegen/emit.js.map +1 -1
- package/dist/codegen/imports.d.ts +1 -17
- package/dist/codegen/imports.js +2 -116
- package/dist/codegen/imports.js.map +1 -1
- package/dist/codegen/instances.d.ts +2 -13
- package/dist/codegen/instances.js +23 -134
- package/dist/codegen/instances.js.map +1 -1
- package/dist/codegen/styles.d.ts +1 -11
- package/dist/codegen/styles.js +2 -130
- package/dist/codegen/styles.js.map +1 -1
- package/dist/merge/applyOps.d.ts +4 -1
- package/dist/merge/applyOps.js +42 -30
- package/dist/merge/applyOps.js.map +1 -1
- package/dist/merge/childrenOrder.d.ts +13 -0
- package/dist/merge/childrenOrder.js +51 -0
- package/dist/merge/childrenOrder.js.map +1 -0
- package/dist/merge/threeWay.d.ts +1 -1
- package/dist/merge/toEdits.d.ts +1 -3
- package/dist/merge/toEdits.js +3 -94
- package/dist/merge/toEdits.js.map +1 -1
- package/dist/project/baseStore.d.ts +11 -1
- package/dist/project/baseStore.js +13 -0
- package/dist/project/baseStore.js.map +1 -1
- package/dist/services/adoptService.js +11 -0
- package/dist/services/adoptService.js.map +1 -1
- package/dist/services/syncEngine.d.ts +1 -1
- package/dist/services/syncEngine.js +297 -60
- package/dist/services/syncEngine.js.map +1 -1
- package/dist/types.d.ts +28 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/dist/ui/report.d.ts +7 -0
- package/dist/ui/report.js +8 -0
- package/dist/ui/report.js.map +1 -1
- package/package.json +3 -2
package/dist/codegen/imports.js
CHANGED
|
@@ -1,117 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
const idx = ref.lastIndexOf('#');
|
|
4
|
-
if (idx === -1)
|
|
5
|
-
throw new Error(`invalid importRef (missing #): ${ref}`);
|
|
6
|
-
return { module: ref.slice(0, idx), exportName: ref.slice(idx + 1) };
|
|
7
|
-
}
|
|
8
|
-
export function collectImports(root) {
|
|
9
|
-
const uiNames = new Set();
|
|
10
|
-
const customs = new Map();
|
|
11
|
-
const walk = (node) => {
|
|
12
|
-
if (node.type === 'Custom') {
|
|
13
|
-
const name = node.props.name;
|
|
14
|
-
const importRef = node.props.importRef;
|
|
15
|
-
if (typeof name !== 'string' || typeof importRef !== 'string') {
|
|
16
|
-
throw new Error(`Custom node ${node.sid} is missing name/importRef`);
|
|
17
|
-
}
|
|
18
|
-
const origin = parseImportRef(importRef);
|
|
19
|
-
if (origin.module === '') {
|
|
20
|
-
throw new Error(`Custom node ${node.sid} (${name}) has a local importRef (${importRef}); cannot emit an import for it`);
|
|
21
|
-
}
|
|
22
|
-
const existing = customs.get(name);
|
|
23
|
-
if (existing &&
|
|
24
|
-
(existing.module !== origin.module || existing.exportName !== origin.exportName)) {
|
|
25
|
-
throw new Error(`Custom name collision: ${name} maps to both ${existing.module} and ${origin.module}`);
|
|
26
|
-
}
|
|
27
|
-
customs.set(name, origin);
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
uiNames.add(node.type);
|
|
31
|
-
}
|
|
32
|
-
for (const child of node.children)
|
|
33
|
-
walk(child);
|
|
34
|
-
};
|
|
35
|
-
walk(root);
|
|
36
|
-
for (const name of customs.keys()) {
|
|
37
|
-
if (uiNames.has(name)) {
|
|
38
|
-
throw new Error(`Custom component ${name} collides with a @mapples/ui primitive of the same name`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return { uiNames: [...uiNames].sort(), customs };
|
|
42
|
-
}
|
|
43
|
-
export function printImports(resolved) {
|
|
44
|
-
const lines = [];
|
|
45
|
-
if (resolved.uiNames.length > 0) {
|
|
46
|
-
lines.push(`import { ${resolved.uiNames.join(', ')} } from '${UI_MODULE}';`);
|
|
47
|
-
}
|
|
48
|
-
const byModule = new Map();
|
|
49
|
-
for (const [local, origin] of resolved.customs) {
|
|
50
|
-
let entry = byModule.get(origin.module);
|
|
51
|
-
if (!entry) {
|
|
52
|
-
entry = { defaults: [], named: [] };
|
|
53
|
-
byModule.set(origin.module, entry);
|
|
54
|
-
}
|
|
55
|
-
if (origin.exportName === 'default')
|
|
56
|
-
entry.defaults.push(local);
|
|
57
|
-
else
|
|
58
|
-
entry.named.push([origin.exportName, local]);
|
|
59
|
-
}
|
|
60
|
-
for (const module of [...byModule.keys()].sort()) {
|
|
61
|
-
const entry = byModule.get(module);
|
|
62
|
-
const pieces = [];
|
|
63
|
-
if (entry.defaults.length > 1) {
|
|
64
|
-
throw new Error(`two default imports from ${module}: ${entry.defaults.join(', ')}`);
|
|
65
|
-
}
|
|
66
|
-
if (entry.defaults.length === 1)
|
|
67
|
-
pieces.push(entry.defaults[0]);
|
|
68
|
-
if (entry.named.length > 0) {
|
|
69
|
-
const named = entry.named
|
|
70
|
-
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
71
|
-
.map(([exp, local]) => (exp === local ? exp : `${exp} as ${local}`))
|
|
72
|
-
.join(', ');
|
|
73
|
-
pieces.push(`{ ${named} }`);
|
|
74
|
-
}
|
|
75
|
-
lines.push(`import ${pieces.join(', ')} from '${module}';`);
|
|
76
|
-
}
|
|
77
|
-
return lines.join('\n');
|
|
78
|
-
}
|
|
79
|
-
/** ImportNeeds for @mapples/sid-ast upsertImports, from a merged tree. */
|
|
80
|
-
export function importNeedsFor(root) {
|
|
81
|
-
const { uiNames, customs } = collectImportsTolerant(root);
|
|
82
|
-
const needs = [];
|
|
83
|
-
if (uiNames.length > 0)
|
|
84
|
-
needs.push({ module: UI_MODULE, named: uiNames });
|
|
85
|
-
for (const [local, origin] of customs) {
|
|
86
|
-
if (origin.module === '')
|
|
87
|
-
continue; // locally-defined component: nothing to import
|
|
88
|
-
needs.push(origin.exportName === 'default'
|
|
89
|
-
? { module: origin.module, defaultName: local }
|
|
90
|
-
: { module: origin.module, named: [origin.exportName] });
|
|
91
|
-
}
|
|
92
|
-
return needs;
|
|
93
|
-
}
|
|
94
|
-
/** Like collectImports but tolerant of local (#Name) customs — used for healing. */
|
|
95
|
-
function collectImportsTolerant(root) {
|
|
96
|
-
const uiNames = new Set();
|
|
97
|
-
const customs = new Map();
|
|
98
|
-
const walk = (node) => {
|
|
99
|
-
if (node.type === 'Custom') {
|
|
100
|
-
const name = node.props.name;
|
|
101
|
-
const importRef = node.props.importRef;
|
|
102
|
-
if (typeof name === 'string' && typeof importRef === 'string') {
|
|
103
|
-
const origin = parseImportRef(importRef);
|
|
104
|
-
if (!customs.has(name))
|
|
105
|
-
customs.set(name, origin);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
uiNames.add(node.type);
|
|
110
|
-
}
|
|
111
|
-
for (const child of node.children)
|
|
112
|
-
walk(child);
|
|
113
|
-
};
|
|
114
|
-
walk(root);
|
|
115
|
-
return { uiNames: [...uiNames].sort(), customs };
|
|
116
|
-
}
|
|
1
|
+
// The import block of a generated file — lives in @mapples/codegen (pure).
|
|
2
|
+
export { collectImports, importNeedsFor, parseImportRef, printImports, UI_MODULE, } from '@mapples/codegen';
|
|
117
3
|
//# sourceMappingURL=imports.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imports.js","sourceRoot":"","sources":["../../src/codegen/imports.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"imports.js","sourceRoot":"","sources":["../../src/codegen/imports.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,SAAS,GAEV,MAAM,kBAAkB,CAAC"}
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InstanceLinker } from '@mapples/codegen';
|
|
2
2
|
import type { ComponentEntity } from '../api/objects.js';
|
|
3
3
|
import type { ComponentsMap } from '../project/componentsMap.js';
|
|
4
|
-
|
|
5
|
-
export interface InstanceLinker {
|
|
6
|
-
/** File language -> canvas language, in place. Reports what could not link. */
|
|
7
|
-
link(dom: RenderDomNode, file: string, warn: (w: ExtractWarning) => void): void;
|
|
8
|
-
/** Canvas language -> file language, as a copy. Reports instances with no component. */
|
|
9
|
-
unlink(dom: RenderDomNode, file: string, warn: (w: ExtractWarning) => void): RenderDomNode;
|
|
10
|
-
/** Does this import, from this file, name a managed component? */
|
|
11
|
-
isLinked(origin: {
|
|
12
|
-
module: string;
|
|
13
|
-
exportName: string;
|
|
14
|
-
}, file: string): boolean;
|
|
15
|
-
}
|
|
4
|
+
export type { InstanceLinker } from '@mapples/codegen';
|
|
16
5
|
export declare function instanceLinker(componentsMap: ComponentsMap, components: Map<string, ComponentEntity>): InstanceLinker;
|
|
@@ -1,141 +1,30 @@
|
|
|
1
|
-
// Linked component instances
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `<ProfileCard $sid="…" styled={{ … }} />` — which extraction reads as a
|
|
6
|
-
// `Custom` node with that import ref. On the CANVAS the same thing is a
|
|
7
|
-
// `ComponentInstance` node: `{ componentUuid, componentName, styled }`, the
|
|
8
|
-
// Creator's linked instance of the saved component. Base and remote trees
|
|
9
|
-
// always speak canvas; a tree read from a file speaks file. `link` and
|
|
10
|
-
// `unlink` convert, so the merge compares like with like and the file never
|
|
11
|
-
// sees a `ComponentInstance` tag (nothing exports one).
|
|
1
|
+
// Linked component instances (file language <-> canvas language) — the
|
|
2
|
+
// conversion lives in @mapples/codegen; this binds it to the CLI's
|
|
3
|
+
// components map and to `node:path`, so OS-native paths in the map resolve
|
|
4
|
+
// the way they always did.
|
|
12
5
|
import { dirname, posix, relative, sep } from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import { parseImportRef } from './imports.js';
|
|
6
|
+
import { instanceLinker as linker, } from '@mapples/codegen';
|
|
15
7
|
const toPosix = (p) => p.split(sep).join(posix.sep);
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
8
|
+
/** `node:path`-backed resolver over the project's OS paths. */
|
|
9
|
+
const nodeResolver = {
|
|
10
|
+
fileFor(fromFile, module) {
|
|
11
|
+
if (!module.startsWith('.'))
|
|
12
|
+
return null;
|
|
13
|
+
const resolved = posix.normalize(posix.join(toPosix(dirname(fromFile)), module));
|
|
14
|
+
return resolved.endsWith('.tsx') ? resolved : `${resolved}.tsx`;
|
|
15
|
+
},
|
|
16
|
+
moduleFor(fromFile, targetFile) {
|
|
17
|
+
let rel = toPosix(relative(dirname(fromFile), targetFile)).replace(/\.tsx$/, '');
|
|
18
|
+
if (!rel.startsWith('.'))
|
|
19
|
+
rel = `./${rel}`;
|
|
20
|
+
return rel;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
30
23
|
export function instanceLinker(componentsMap, components) {
|
|
31
|
-
const
|
|
32
|
-
const byUuid = new Map();
|
|
24
|
+
const posixMap = {};
|
|
33
25
|
for (const [id, mapping] of Object.entries(componentsMap)) {
|
|
34
|
-
|
|
35
|
-
const linked = {
|
|
36
|
-
id,
|
|
37
|
-
file: toPosix(mapping.file),
|
|
38
|
-
name: entity?.name ?? mapping.name,
|
|
39
|
-
uuid: entity?.uuid,
|
|
40
|
-
};
|
|
41
|
-
byFile.set(linked.file, linked);
|
|
42
|
-
if (linked.uuid)
|
|
43
|
-
byUuid.set(linked.uuid, linked);
|
|
26
|
+
posixMap[id] = { file: toPosix(mapping.file), name: mapping.name };
|
|
44
27
|
}
|
|
45
|
-
|
|
46
|
-
if (origin.exportName !== 'default')
|
|
47
|
-
return null;
|
|
48
|
-
const target = resolveModuleFile(file, origin.module);
|
|
49
|
-
return target ? (byFile.get(target) ?? null) : null;
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
isLinked: (origin, file) => componentFor(origin, file) !== null,
|
|
53
|
-
link(dom, file, warn) {
|
|
54
|
-
const walk = (node) => {
|
|
55
|
-
node.children.forEach(walk);
|
|
56
|
-
if (node.type !== 'Custom')
|
|
57
|
-
return;
|
|
58
|
-
const { name, importRef, layout } = node.props;
|
|
59
|
-
if (typeof name !== 'string' || typeof importRef !== 'string')
|
|
60
|
-
return;
|
|
61
|
-
const component = componentFor(parseImportRef(importRef), file);
|
|
62
|
-
if (!component)
|
|
63
|
-
return;
|
|
64
|
-
if (!component.uuid) {
|
|
65
|
-
warn({
|
|
66
|
-
kind: 'UNKNOWN_ELEMENT_WITH_SID',
|
|
67
|
-
sid: node.sid,
|
|
68
|
-
detail: `<${name}> is the Creator component “${component.name}”, but that component has no uuid yet — pushed as a Custom placeholder; run \`mapples sync\` again after the component reaches the Creator`,
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const attrs = (layout ?? {});
|
|
73
|
-
const { styled, ...rest } = attrs;
|
|
74
|
-
for (const key of Object.keys(rest)) {
|
|
75
|
-
warn({
|
|
76
|
-
kind: 'NON_LITERAL_PROP',
|
|
77
|
-
sid: node.sid,
|
|
78
|
-
prop: key,
|
|
79
|
-
detail: `prop \`${key}\` on <${name}> stays in code only — a Creator component instance takes no props besides \`styled\``,
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
if (node.children.length > 0) {
|
|
83
|
-
warn({
|
|
84
|
-
kind: 'NON_LITERAL_PROP',
|
|
85
|
-
sid: node.sid,
|
|
86
|
-
prop: 'children',
|
|
87
|
-
detail: `children of <${name}> are not part of the Creator instance — kept in code only`,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
node.type = 'ComponentInstance';
|
|
91
|
-
node.props = {
|
|
92
|
-
componentUuid: component.uuid,
|
|
93
|
-
componentName: component.name,
|
|
94
|
-
...(styled !== undefined ? { styled } : {}),
|
|
95
|
-
};
|
|
96
|
-
node.children = [];
|
|
97
|
-
};
|
|
98
|
-
walk(dom);
|
|
99
|
-
},
|
|
100
|
-
unlink(dom, file, warn) {
|
|
101
|
-
// A component the Creator named after its root ("View", "Button"…)
|
|
102
|
-
// cannot share a local name with the primitive the file imports.
|
|
103
|
-
const primitiveTypes = new Set();
|
|
104
|
-
const collectTypes = (node) => {
|
|
105
|
-
if (node.type !== 'Custom' && node.type !== 'ComponentInstance') {
|
|
106
|
-
primitiveTypes.add(node.type);
|
|
107
|
-
}
|
|
108
|
-
node.children.forEach(collectTypes);
|
|
109
|
-
};
|
|
110
|
-
collectTypes(dom);
|
|
111
|
-
const localName = (component) => pageComponentName(component.name, primitiveTypes, 'Component');
|
|
112
|
-
const convert = (node) => {
|
|
113
|
-
const children = node.children.map(convert);
|
|
114
|
-
if (node.type !== 'ComponentInstance')
|
|
115
|
-
return { ...node, children };
|
|
116
|
-
const { componentUuid, componentName, styled } = node.props;
|
|
117
|
-
const component = typeof componentUuid === 'string' ? byUuid.get(componentUuid) : undefined;
|
|
118
|
-
if (!component) {
|
|
119
|
-
warn({
|
|
120
|
-
kind: 'UNKNOWN_ELEMENT_WITH_SID',
|
|
121
|
-
sid: node.sid,
|
|
122
|
-
detail: `instance of component “${String(componentName ?? componentUuid)}” has no component file to import — left as a ComponentInstance tag`,
|
|
123
|
-
});
|
|
124
|
-
return { ...node, children };
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
...node,
|
|
128
|
-
type: 'Custom',
|
|
129
|
-
props: {
|
|
130
|
-
name: localName(component),
|
|
131
|
-
importRef: `${moduleForFile(file, component.file)}#default`,
|
|
132
|
-
layout: styled !== undefined ? { styled } : {},
|
|
133
|
-
},
|
|
134
|
-
children,
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
return convert(dom);
|
|
138
|
-
},
|
|
139
|
-
};
|
|
28
|
+
return linker(posixMap, components, nodeResolver);
|
|
140
29
|
}
|
|
141
30
|
//# sourceMappingURL=instances.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instances.js","sourceRoot":"","sources":["../../src/codegen/instances.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"instances.js","sourceRoot":"","sources":["../../src/codegen/instances.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,mEAAmE;AACnE,2EAA2E;AAC3E,2BAA2B;AAE3B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EACL,cAAc,IAAI,MAAM,GAGzB,MAAM,kBAAkB,CAAC;AAO1B,MAAM,OAAO,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAEpE,+DAA+D;AAC/D,MAAM,YAAY,GAAmB;IACnC,OAAO,CAAC,QAAQ,EAAE,MAAM;QACtB,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACjF,OAAO,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,MAAM,CAAC;IAClE,CAAC;IACD,SAAS,CAAC,QAAQ,EAAE,UAAU;QAC5B,IAAI,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACjF,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,GAAG,GAAG,KAAK,GAAG,EAAE,CAAC;QAC3C,OAAO,GAAG,CAAC;IACb,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,cAAc,CAC5B,aAA4B,EAC5B,UAAwC;IAExC,MAAM,QAAQ,GAAmD,EAAE,CAAC;IACpE,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QAC1D,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IACrE,CAAC;IACD,OAAO,MAAM,CAAC,QAAQ,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC"}
|
package/dist/codegen/styles.d.ts
CHANGED
|
@@ -1,11 +1 @@
|
|
|
1
|
-
|
|
2
|
-
/** Where `Styler` comes from in app code. */
|
|
3
|
-
export declare const STYLE_MODULE = "@mapples/style";
|
|
4
|
-
export interface HoistedStyles {
|
|
5
|
-
dictName: string;
|
|
6
|
-
/** Dictionary entries in document order: `[key, style object]`. */
|
|
7
|
-
entries: [string, unknown][];
|
|
8
|
-
/** The expression a node's `styled` key prints as, or undefined to print the literal. */
|
|
9
|
-
refFor: (node: RenderDomNode, key: string, value: unknown) => string | undefined;
|
|
10
|
-
}
|
|
11
|
-
export declare function hoistStyles(root: RenderDomNode, dictName?: string): HoistedStyles;
|
|
1
|
+
export { hoistStyles, STYLE_MODULE, type HoistedStyles } from '@mapples/codegen';
|
package/dist/codegen/styles.js
CHANGED
|
@@ -1,131 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// of the file, and the tag reads `styled={{ style: styles.signInText }}`.
|
|
4
|
-
// Keys come from what the element shows (its text, label, placeholder…) and
|
|
5
|
-
// what it is, so a developer can find a style by reading the JSX.
|
|
6
|
-
/** Where `Styler` comes from in app code. */
|
|
7
|
-
export const STYLE_MODULE = '@mapples/style';
|
|
8
|
-
/** `styled` keys that hold a style object, and the word appended to the entry key for each. */
|
|
9
|
-
const STYLED_SUFFIX = {
|
|
10
|
-
style: '',
|
|
11
|
-
styleImage: 'Picture',
|
|
12
|
-
styleTypography: 'Label',
|
|
13
|
-
styleSvg: 'Glyph',
|
|
14
|
-
contentContainerStyle: 'Content',
|
|
15
|
-
};
|
|
16
|
-
/** The word for a node type inside an entry key. */
|
|
17
|
-
const TYPE_WORD = {
|
|
18
|
-
View: 'view',
|
|
19
|
-
SafeAreaView: 'screen',
|
|
20
|
-
ScrollView: 'scroll',
|
|
21
|
-
FlatList: 'list',
|
|
22
|
-
FormView: 'form',
|
|
23
|
-
Text: 'text',
|
|
24
|
-
Image: 'image',
|
|
25
|
-
Button: 'button',
|
|
26
|
-
SubmitButton: 'button',
|
|
27
|
-
TextInput: 'input',
|
|
28
|
-
TextField: 'input',
|
|
29
|
-
Vector: 'icon',
|
|
30
|
-
Icon: 'icon',
|
|
31
|
-
Custom: 'custom',
|
|
32
|
-
};
|
|
33
|
-
/** Props whose text names an element well enough to name its style. */
|
|
34
|
-
const HINT_PROPS = ['text', 'label', 'placeholder', 'title', 'alt', 'fieldName'];
|
|
35
|
-
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
36
|
-
/**
|
|
37
|
-
* "Please enter username and password" -> "pleaseEnterUsername". Words are
|
|
38
|
-
* letters only — a price like "0.99$" names nothing — and the result is a
|
|
39
|
-
* valid identifier start or empty.
|
|
40
|
-
*/
|
|
41
|
-
function hintWords(text) {
|
|
42
|
-
const words = text
|
|
43
|
-
.replace(/[^A-Za-z ]+/g, ' ')
|
|
44
|
-
.split(/\s+/)
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
.slice(0, 3)
|
|
47
|
-
.map((w) => w.toLowerCase());
|
|
48
|
-
if (words.length === 0)
|
|
49
|
-
return '';
|
|
50
|
-
return words[0] + words.slice(1).map(capitalize).join('');
|
|
51
|
-
}
|
|
52
|
-
function typeWord(node) {
|
|
53
|
-
if (node.type === 'Custom') {
|
|
54
|
-
const name = node.props.name;
|
|
55
|
-
return typeof name === 'string' && name !== ''
|
|
56
|
-
? name.charAt(0).toLowerCase() + name.slice(1)
|
|
57
|
-
: 'custom';
|
|
58
|
-
}
|
|
59
|
-
return TYPE_WORD[node.type] ?? node.type.charAt(0).toLowerCase() + node.type.slice(1);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* The words an element's style keys are built from: `stem` is what it shows
|
|
63
|
-
* (`signIn`, `companyLogo`) or what it is when it shows nothing (`view`);
|
|
64
|
-
* `style` adds the type to a shown stem (`signInText`), the other `styled`
|
|
65
|
-
* keys add their own word to the stem (`signInLabel`, `companyLogoPicture`).
|
|
66
|
-
*/
|
|
67
|
-
function keyWords(node, isRoot) {
|
|
68
|
-
if (isRoot)
|
|
69
|
-
return { stem: 'root', style: 'root' };
|
|
70
|
-
const props = node.type === 'Custom' ? (node.props.layout ?? {}) : node.props;
|
|
71
|
-
const type = typeWord(node);
|
|
72
|
-
for (const prop of HINT_PROPS) {
|
|
73
|
-
const v = props[prop];
|
|
74
|
-
if (typeof v === 'string' && v.trim() !== '') {
|
|
75
|
-
const hint = hintWords(v);
|
|
76
|
-
if (hint !== '')
|
|
77
|
-
return { stem: hint, style: `${hint}${capitalize(type)}` };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return { stem: type, style: type };
|
|
81
|
-
}
|
|
82
|
-
function styledOf(node) {
|
|
83
|
-
const styled = node.type === 'Custom'
|
|
84
|
-
? (node.props.layout ?? {}).styled
|
|
85
|
-
: node.props.styled;
|
|
86
|
-
return typeof styled === 'object' && styled !== null && !Array.isArray(styled)
|
|
87
|
-
? styled
|
|
88
|
-
: null;
|
|
89
|
-
}
|
|
90
|
-
export function hoistStyles(root, dictName = 'styles') {
|
|
91
|
-
const entries = [];
|
|
92
|
-
const taken = new Set();
|
|
93
|
-
const refs = new Map(); // `${sid}|${styledKey}` -> entry key
|
|
94
|
-
const unique = (key) => {
|
|
95
|
-
let candidate = key;
|
|
96
|
-
for (let n = 2; taken.has(candidate); n += 1)
|
|
97
|
-
candidate = `${key}${n}`;
|
|
98
|
-
taken.add(candidate);
|
|
99
|
-
return candidate;
|
|
100
|
-
};
|
|
101
|
-
const visit = (node, isRoot) => {
|
|
102
|
-
const styled = styledOf(node);
|
|
103
|
-
if (styled) {
|
|
104
|
-
const words = keyWords(node, isRoot);
|
|
105
|
-
for (const [styledKey, value] of Object.entries(styled)) {
|
|
106
|
-
if (!(styledKey in STYLED_SUFFIX))
|
|
107
|
-
continue;
|
|
108
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
109
|
-
continue;
|
|
110
|
-
if (Object.keys(value).length === 0)
|
|
111
|
-
continue;
|
|
112
|
-
const suffix = STYLED_SUFFIX[styledKey];
|
|
113
|
-
const key = unique(suffix === '' ? words.style : `${words.stem}${suffix}`);
|
|
114
|
-
entries.push([key, value]);
|
|
115
|
-
refs.set(`${node.sid}|${styledKey}`, key);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
for (const child of node.children)
|
|
119
|
-
visit(child, false);
|
|
120
|
-
};
|
|
121
|
-
visit(root, true);
|
|
122
|
-
return {
|
|
123
|
-
dictName,
|
|
124
|
-
entries,
|
|
125
|
-
refFor: (node, key) => {
|
|
126
|
-
const entry = refs.get(`${node.sid}|${key}`);
|
|
127
|
-
return entry === undefined ? undefined : `${dictName}.${entry}`;
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
1
|
+
// Hoisted `Styler.create` dictionaries — live in @mapples/codegen (pure).
|
|
2
|
+
export { hoistStyles, STYLE_MODULE } from '@mapples/codegen';
|
|
131
3
|
//# sourceMappingURL=styles.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/codegen/styles.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/codegen/styles.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAsB,MAAM,kBAAkB,CAAC"}
|
package/dist/merge/applyOps.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ import { type JpatchOp, type RenderDomNode } from '../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Apply RenderNode ops to one page tree. Ops whose target is not in this tree
|
|
4
4
|
* are ignored (they belong to another page) and returned in `skipped` so the
|
|
5
|
-
* caller can route them; nothing is dropped silently.
|
|
5
|
+
* caller can route them; nothing is dropped silently. `warnings` carries
|
|
6
|
+
* CHILDREN_REBASED when a directive resolved differently from its literal
|
|
7
|
+
* intent, exactly when the server flags it.
|
|
6
8
|
*/
|
|
7
9
|
export declare function applyNodeOps(root: RenderDomNode | null, ops: JpatchOp[]): {
|
|
8
10
|
root: RenderDomNode | null;
|
|
9
11
|
skipped: JpatchOp[];
|
|
10
12
|
orphans: Map<string, RenderDomNode>;
|
|
13
|
+
warnings: string[];
|
|
11
14
|
};
|
package/dist/merge/applyOps.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Apply RenderNode ops to a page tree (the pull direction). The inverse of
|
|
2
2
|
// diffTree: applyNodeOps(base, diffTree(base, x)) ≡ x.
|
|
3
3
|
import { sidecarFilled, sidecarOf, } from '../types.js';
|
|
4
|
+
import { resolveChildOrder } from './childrenOrder.js';
|
|
4
5
|
import { applyMergePatch } from './mergePatch.js';
|
|
5
6
|
function buildState(root) {
|
|
6
7
|
const index = new Map();
|
|
@@ -20,13 +21,16 @@ function buildState(root) {
|
|
|
20
21
|
/**
|
|
21
22
|
* Apply RenderNode ops to one page tree. Ops whose target is not in this tree
|
|
22
23
|
* are ignored (they belong to another page) and returned in `skipped` so the
|
|
23
|
-
* caller can route them; nothing is dropped silently.
|
|
24
|
+
* caller can route them; nothing is dropped silently. `warnings` carries
|
|
25
|
+
* CHILDREN_REBASED when a directive resolved differently from its literal
|
|
26
|
+
* intent, exactly when the server flags it.
|
|
24
27
|
*/
|
|
25
28
|
export function applyNodeOps(root, ops) {
|
|
26
29
|
const state = buildState(root);
|
|
27
30
|
const skipped = [];
|
|
28
31
|
// Created-but-not-yet-linked nodes park here until a children directive links them.
|
|
29
32
|
const orphans = new Map();
|
|
33
|
+
let rebased = false;
|
|
30
34
|
for (const op of ops) {
|
|
31
35
|
if (op.entity !== 'RenderNode') {
|
|
32
36
|
skipped.push(op);
|
|
@@ -114,8 +118,8 @@ export function applyNodeOps(root, ops) {
|
|
|
114
118
|
else
|
|
115
119
|
delete node.actionProps;
|
|
116
120
|
}
|
|
117
|
-
if (op.children) {
|
|
118
|
-
|
|
121
|
+
if (op.children && applyChildrenDirective(state, node, op.children, orphans)) {
|
|
122
|
+
rebased = true;
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
// Root replacement: the old root was deleted and a new tree created with no
|
|
@@ -127,39 +131,47 @@ export function applyNodeOps(root, ops) {
|
|
|
127
131
|
orphans.clear();
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
|
-
return { root: state.root, skipped, orphans };
|
|
134
|
+
return { root: state.root, skipped, orphans, warnings: rebased ? ['CHILDREN_REBASED'] : [] };
|
|
131
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve a children directive the way the server does (RFC-0001 §5.2,
|
|
138
|
+
* service--mapplesorg #724/#725): order via resolveChildOrder, then every ref
|
|
139
|
+
* must resolve in this tree and must not be the target or one of its
|
|
140
|
+
* ancestors — such a ref is skipped. A ref linked elsewhere is reparented; a
|
|
141
|
+
* child left out parks in `orphans`. Returns whether the result was rebased.
|
|
142
|
+
*/
|
|
132
143
|
function applyChildrenDirective(state, node, directive, orphans) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
orphans.set(ref, removed); // may be re-linked under a new parent
|
|
140
|
-
}
|
|
141
|
-
}
|
|
144
|
+
const current = node.children.map((c) => c.sid);
|
|
145
|
+
const resolved = resolveChildOrder(current, directive);
|
|
146
|
+
let rebased = resolved.rebased;
|
|
147
|
+
const lineage = new Set();
|
|
148
|
+
for (let sid = node.sid; sid !== undefined; sid = state.parents.get(sid)) {
|
|
149
|
+
lineage.add(sid);
|
|
142
150
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
state.parents.
|
|
151
|
+
const kept = [];
|
|
152
|
+
for (const sid of resolved.order) {
|
|
153
|
+
const child = state.index.get(sid) ?? orphans.get(sid);
|
|
154
|
+
if (!child || lineage.has(sid)) {
|
|
155
|
+
rebased = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!current.includes(sid)) {
|
|
159
|
+
const oldParent = state.parents.get(sid);
|
|
160
|
+
const parent = oldParent === undefined ? undefined : state.index.get(oldParent);
|
|
161
|
+
if (parent)
|
|
162
|
+
parent.children = parent.children.filter((c) => c.sid !== sid);
|
|
163
|
+
orphans.delete(sid);
|
|
152
164
|
}
|
|
165
|
+
state.parents.set(sid, node.sid);
|
|
166
|
+
kept.push(child);
|
|
153
167
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
continue; // clamped server-side; mirror the tolerance
|
|
159
|
-
const [child] = node.children.splice(from, 1);
|
|
160
|
-
const to = Math.max(0, Math.min(mv.to, node.children.length));
|
|
161
|
-
node.children.splice(to, 0, child);
|
|
168
|
+
for (const child of node.children) {
|
|
169
|
+
if (!kept.includes(child)) {
|
|
170
|
+
state.parents.delete(child.sid);
|
|
171
|
+
orphans.set(child.sid, child); // may be re-linked under a new parent
|
|
162
172
|
}
|
|
163
173
|
}
|
|
174
|
+
node.children = kept;
|
|
175
|
+
return rebased;
|
|
164
176
|
}
|
|
165
177
|
//# sourceMappingURL=applyOps.js.map
|