@sola-air-ui/compiler 1.1.0 → 1.3.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/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/index.js +296 -93
- package/types/index.d.ts +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
|
18
18
|
--target <esm|iife> Output format (default: esm)
|
|
19
19
|
esm — ES module with import/export (default, for bundlers)
|
|
20
20
|
iife — Self-contained IIFE for no-bundler environments
|
|
21
|
-
(
|
|
21
|
+
(enterprise portal widgets, plain <script> tags)
|
|
22
22
|
--export-name <Name> Global name for IIFE target (default: SolaComponent)
|
|
23
23
|
The compiled component mounts as window['Name'](el, props)
|
|
24
24
|
-v, --version Print compiler version
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,14 @@ const { version: COMPILER_VERSION } = JSON.parse(
|
|
|
6
6
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
+
// Names every compiled component imports from core under their own name. A
|
|
10
|
+
// script that imports one of these again is not given a second binding.
|
|
11
|
+
const CORE_PROVIDED = new Set([
|
|
12
|
+
'createSignal', 'createDerived', 'createEffect', 'createIntent', 'createData',
|
|
13
|
+
'createPersistentSignal', 'onMount', 'onDestroy', 'pushContext', 'popContext',
|
|
14
|
+
'__flush_mounts', '__flush_destroys', '__untrack'
|
|
15
|
+
]);
|
|
16
|
+
|
|
9
17
|
const VOID_ELEMENTS = new Set([
|
|
10
18
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
11
19
|
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
@@ -32,18 +40,81 @@ function hashString(str) {
|
|
|
32
40
|
return 'sola-' + Math.abs(hash).toString(36);
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
// Split on `sep` characters that are not inside (), [] or a string, so
|
|
44
|
+
// `:is(a, b)` and `:global(.x, .y)` are not torn apart at their commas.
|
|
45
|
+
function splitTopLevel(text, isSep) {
|
|
46
|
+
const parts = [];
|
|
47
|
+
let depth = 0, quote = null, start = 0;
|
|
48
|
+
for (let i = 0; i < text.length; i++) {
|
|
49
|
+
const c = text[i];
|
|
50
|
+
if (quote) { if (c === '\\') i++; else if (c === quote) quote = null; continue; }
|
|
51
|
+
if (c === '"' || c === "'") { quote = c; continue; }
|
|
52
|
+
if (c === '(' || c === '[') depth++;
|
|
53
|
+
else if (c === ')' || c === ']') depth--;
|
|
54
|
+
else if (depth === 0 && isSep(c)) { parts.push(text.slice(start, i)); start = i + 1; }
|
|
55
|
+
}
|
|
56
|
+
parts.push(text.slice(start));
|
|
57
|
+
return parts;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Put the scope class on one compound selector, before any pseudo-class or
|
|
61
|
+
// pseudo-element (button:hover → button.hash:hover).
|
|
62
|
+
function addScopeToCompound(compound, scopeHash) {
|
|
63
|
+
const pseudoIdx = compound.search(/::?[\w-]/);
|
|
64
|
+
if (pseudoIdx > 0) {
|
|
65
|
+
return `${compound.slice(0, pseudoIdx)}.${scopeHash}${compound.slice(pseudoIdx)}`;
|
|
66
|
+
}
|
|
67
|
+
return `${compound}.${scopeHash}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// `:global(sel)` opts a piece of a selector out of scoping, so a scoped
|
|
71
|
+
// component can style markup it did not create (e.g. the output of {@html}):
|
|
72
|
+
// .msg :global(table) → .msg.hash table
|
|
73
|
+
// :global(body) → body
|
|
74
|
+
// The scope class always goes on the last compound that is not global.
|
|
75
|
+
function scopeSelector(selector, scopeHash) {
|
|
76
|
+
const tokens = []; // alternating compounds and combinators
|
|
77
|
+
let depth = 0, cur = '', comb = '';
|
|
78
|
+
const flushCompound = () => { if (cur) { tokens.push({ compound: cur }); cur = ''; } };
|
|
79
|
+
for (let i = 0; i < selector.length; i++) {
|
|
80
|
+
const c = selector[i];
|
|
81
|
+
if (c === '(' || c === '[') depth++;
|
|
82
|
+
else if (c === ')' || c === ']') depth--;
|
|
83
|
+
if (depth === 0 && /[\s>+~]/.test(c)) {
|
|
84
|
+
flushCompound();
|
|
85
|
+
comb += c;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (comb) { tokens.push({ comb }); comb = ''; }
|
|
89
|
+
cur += c;
|
|
90
|
+
}
|
|
91
|
+
flushCompound();
|
|
92
|
+
if (comb) tokens.push({ comb });
|
|
93
|
+
|
|
94
|
+
let last = -1;
|
|
95
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
96
|
+
const t = tokens[i];
|
|
97
|
+
if (t.compound === undefined) continue;
|
|
98
|
+
const m = t.compound.match(/^:global\(([\s\S]*)\)$/);
|
|
99
|
+
// A comma list must stay grouped, or `.a :global(.x, .y)` would turn into
|
|
100
|
+
// two selectors and leave `.y` outside `.a`.
|
|
101
|
+
const group = (inner) => (splitTopLevel(inner, c => c === ',').length > 1 ? `:is(${inner})` : inner);
|
|
102
|
+
if (m) { t.compound = group(m[1]); t.global = true; }
|
|
103
|
+
else if (t.compound.includes(':global(')) {
|
|
104
|
+
t.compound = t.compound.replace(/:global\(([\s\S]*?)\)/g, (_, inner) => group(inner));
|
|
105
|
+
t.global = true;
|
|
106
|
+
} else { last = i; }
|
|
107
|
+
}
|
|
108
|
+
if (last >= 0) tokens[last].compound = addScopeToCompound(tokens[last].compound, scopeHash);
|
|
109
|
+
return tokens.map(t => t.compound ?? t.comb).join('');
|
|
110
|
+
}
|
|
111
|
+
|
|
35
112
|
function scopeSelectorList(selector, scopeHash) {
|
|
36
|
-
return selector
|
|
37
|
-
.split(',')
|
|
113
|
+
return splitTopLevel(selector, c => c === ',')
|
|
38
114
|
.map(part => {
|
|
39
115
|
const trimmed = part.trim();
|
|
40
116
|
if (!trimmed) return part;
|
|
41
|
-
|
|
42
|
-
const pseudoIdx = trimmed.search(/::?[\w-]/);
|
|
43
|
-
if (pseudoIdx > 0) {
|
|
44
|
-
return `${trimmed.slice(0, pseudoIdx)}.${scopeHash}${trimmed.slice(pseudoIdx)}`;
|
|
45
|
-
}
|
|
46
|
-
return `${trimmed}.${scopeHash}`;
|
|
117
|
+
return scopeSelector(trimmed, scopeHash);
|
|
47
118
|
})
|
|
48
119
|
.join(', ');
|
|
49
120
|
}
|
|
@@ -262,7 +333,7 @@ function preprocessTemplate(template, dynAttrs, handlers) {
|
|
|
262
333
|
} else if (hasDyn) {
|
|
263
334
|
// Build a string-concatenation expression instead of a runtime backtick
|
|
264
335
|
// template literal. Confirmed by driving a real browser against a live
|
|
265
|
-
//
|
|
336
|
+
// enterprise portal instance: its widget-script delivery pipeline
|
|
266
337
|
// silently mangles `${expr}` interpolation inside a template literal down
|
|
267
338
|
// to bare, unevaluated expression text (e.g. `background: ${x}` becomes
|
|
268
339
|
// the literal string "background: x" — the function reference itself,
|
|
@@ -540,7 +611,7 @@ function collectReactiveVars(ast) {
|
|
|
540
611
|
decl.init.callee.type === 'Identifier' && decl.id.type === 'Identifier') {
|
|
541
612
|
const macro = decl.init.callee.name;
|
|
542
613
|
if (macro === '$state' || macro === '$persist') vars.state.add(decl.id.name);
|
|
543
|
-
else if (macro === '$derived') vars.derived.add(decl.id.name);
|
|
614
|
+
else if (macro === '$derived' || macro === '$lastEvent') vars.derived.add(decl.id.name);
|
|
544
615
|
else if (macro === '$intent') vars.intent.add(decl.id.name);
|
|
545
616
|
else if (macro === '$data') vars.data.add(decl.id.name);
|
|
546
617
|
}
|
|
@@ -638,7 +709,7 @@ function emitReactiveEdits(rootNode, src, vars, edits, initialShadows) {
|
|
|
638
709
|
const isMemberProp = p.type === 'MemberExpression' && p.property === node && !p.computed;
|
|
639
710
|
const isCallee = (p.type === 'CallExpression' || p.type === 'NewExpression') && p.callee === node;
|
|
640
711
|
const isAssignTarget = (p.type === 'AssignmentExpression' && p.left === node) || p.type === 'UpdateExpression';
|
|
641
|
-
const isPropertyKey = p.type === 'Property' && p.key === node && !p.computed
|
|
712
|
+
const isPropertyKey = p.type === 'Property' && p.key === node && !p.computed;
|
|
642
713
|
const isShorthand = p.type === 'Property' && p.shorthand && p.value === node;
|
|
643
714
|
const isMemberObject = p.type === 'MemberExpression' && p.object === node;
|
|
644
715
|
|
|
@@ -826,6 +897,12 @@ export function compile(source, options = {}) {
|
|
|
826
897
|
const stateVars = new Set();
|
|
827
898
|
const exportedProps = [];
|
|
828
899
|
const componentImports = [];
|
|
900
|
+
// Every other import in the script. They used to be deleted, so a package
|
|
901
|
+
// import (`import DataCard from '@sola-air-ui/ui/data-card'`, or anything from
|
|
902
|
+
// '@sola-air-ui/core' that the compiler does not already provide) was gone from
|
|
903
|
+
// the output and left an undefined name behind.
|
|
904
|
+
const otherImports = [];
|
|
905
|
+
const coreImportExtras = [];
|
|
829
906
|
|
|
830
907
|
let reactiveVars = NO_REACTIVE_VARS;
|
|
831
908
|
if (strippedScript.trim()) {
|
|
@@ -851,6 +928,21 @@ export function compile(source, options = {}) {
|
|
|
851
928
|
node.specifiers.forEach(spec => {
|
|
852
929
|
componentImports.push({ localName: spec.local.name, path: src });
|
|
853
930
|
});
|
|
931
|
+
} else if (src === '@sola-air-ui/core' || src === '@sola/core') {
|
|
932
|
+
// The compiler already imports these names under their own names.
|
|
933
|
+
// Importing one again would be a duplicate binding, so keep only the
|
|
934
|
+
// rest: names it does not provide, and aliased ones.
|
|
935
|
+
node.specifiers.forEach(spec => {
|
|
936
|
+
if (spec.type !== 'ImportSpecifier') return;
|
|
937
|
+
const imported = spec.imported.name ?? spec.imported.value;
|
|
938
|
+
if (spec.local.name === imported && CORE_PROVIDED.has(imported)) return;
|
|
939
|
+
coreImportExtras.push({ imported, local: spec.local.name });
|
|
940
|
+
});
|
|
941
|
+
} else {
|
|
942
|
+
otherImports.push({
|
|
943
|
+
code: strippedScript.slice(node.start, node.end).trim().replace(/;?$/, ';'),
|
|
944
|
+
locals: node.specifiers.map(spec => spec.local.name)
|
|
945
|
+
});
|
|
854
946
|
}
|
|
855
947
|
edits.push({
|
|
856
948
|
start: node.start,
|
|
@@ -874,6 +966,16 @@ export function compile(source, options = {}) {
|
|
|
874
966
|
removedRanges.push([node.start, node.end]);
|
|
875
967
|
}
|
|
876
968
|
|
|
969
|
+
// $on('type', handler): subscribe to a bus event for the life of the
|
|
970
|
+
// component. Compiles to onDestroy(__solaBus.on(type, handler)). The handler
|
|
971
|
+
// stays in place so signals read inside it are rewritten like any other.
|
|
972
|
+
if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' &&
|
|
973
|
+
node.expression.callee.type === 'Identifier' && node.expression.callee.name === '$on') {
|
|
974
|
+
const call = node.expression;
|
|
975
|
+
edits.push({ start: call.callee.start, end: call.callee.end, replacement: 'onDestroy(__solaBus.on' });
|
|
976
|
+
edits.push({ start: call.end, end: call.end, replacement: ')' });
|
|
977
|
+
}
|
|
978
|
+
|
|
877
979
|
// $state()/$derived()/$effect()/$intent()/$data() macro declarations.
|
|
878
980
|
// Bracket edits (head + tail) leave the argument expression in place so
|
|
879
981
|
// the reactive rewrite pass below can still auto-call signals inside it.
|
|
@@ -907,6 +1009,16 @@ export function compile(source, options = {}) {
|
|
|
907
1009
|
} else {
|
|
908
1010
|
edits.push({ start: node.start, end: node.end, replacement: `const ${varName} = createDerived(() => undefined);` });
|
|
909
1011
|
}
|
|
1012
|
+
} else if (macro === '$lastEvent') {
|
|
1013
|
+
// let ctx = $lastEvent('type'): the latest payload of a bus event as a
|
|
1014
|
+
// reactive accessor, released when the component is destroyed.
|
|
1015
|
+
const varName = decl.id.name;
|
|
1016
|
+
if (arg) {
|
|
1017
|
+
edits.push({ start: node.start, end: arg.start, replacement: `const ${varName} = __solaBus.signal(` });
|
|
1018
|
+
edits.push({ start: lastArg.end, end: node.end, replacement: `); onDestroy(${varName}.dispose);` });
|
|
1019
|
+
} else {
|
|
1020
|
+
edits.push({ start: node.start, end: node.end, replacement: `const ${varName} = () => undefined;` });
|
|
1021
|
+
}
|
|
910
1022
|
} else if (macro === '$effect') {
|
|
911
1023
|
if (arg) {
|
|
912
1024
|
edits.push({ start: node.start, end: arg.start, replacement: `createEffect(` });
|
|
@@ -967,6 +1079,11 @@ export function compile(source, options = {}) {
|
|
|
967
1079
|
let uid = 0;
|
|
968
1080
|
let domCode = '';
|
|
969
1081
|
const importedComponentSet = new Set(componentImports.map(i => i.localName));
|
|
1082
|
+
// A capitalised tag that matches a name imported from a package is a component
|
|
1083
|
+
// too (`import DataCard from '@sola-air-ui/ui/data-card'`), not only a `.sola` path.
|
|
1084
|
+
for (const imp of otherImports) {
|
|
1085
|
+
for (const name of imp.locals) if (/^[A-Z]/.test(name)) importedComponentSet.add(name);
|
|
1086
|
+
}
|
|
970
1087
|
|
|
971
1088
|
const sourceFile = options.filename || '<component>';
|
|
972
1089
|
|
|
@@ -1047,25 +1164,43 @@ export function compile(source, options = {}) {
|
|
|
1047
1164
|
domCode += ` ${parentVar}.appendChild(${cid}_target);\n`;
|
|
1048
1165
|
|
|
1049
1166
|
// Dynamically-bound props (rows={items}) arrive here as __soladyn_N__ placeholders —
|
|
1050
|
-
// resolve them back to the real expression from dynAttrs
|
|
1051
|
-
// (unquoted) property so the child receives live data, not the placeholder text.
|
|
1167
|
+
// resolve them back to the real expression from dynAttrs.
|
|
1052
1168
|
//
|
|
1053
1169
|
// `onXxx`-named props (e.g. onChange={handler}) arrive as a
|
|
1054
1170
|
// __solahandler_N__ marker, because preprocessTemplate cannot tell a
|
|
1055
1171
|
// component apart from a native element and lifts both the same way.
|
|
1056
1172
|
// Either way the expression is passed through as a live function.
|
|
1173
|
+
//
|
|
1174
|
+
// Three kinds of dynamic prop:
|
|
1175
|
+
// - a bare name (rows={items}) is passed as-is. For a $state that is the
|
|
1176
|
+
// signal accessor, which is how a child receives live data (the shipped
|
|
1177
|
+
// components accept either an accessor or a plain value);
|
|
1178
|
+
// - an expression that reads a signal (title={ctx.title}) is evaluated
|
|
1179
|
+
// with the signal reads rewritten, and the child is re-mounted when
|
|
1180
|
+
// the value changes;
|
|
1181
|
+
// - an expression that reads no signal (text={msg.text} in an {#each})
|
|
1182
|
+
// is evaluated once, when the child is mounted.
|
|
1057
1183
|
const propEntries = [];
|
|
1184
|
+
let reactiveProps = false;
|
|
1058
1185
|
for (const [key, val] of Object.entries(node.attribs || {})) {
|
|
1059
1186
|
const dynMatch = /^__soladyn_(\d+)__$/.exec(val);
|
|
1060
1187
|
const handlerMatch = /^__solahandler_(\d+)__$/.exec(val);
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1188
|
+
let propValue;
|
|
1189
|
+
if (dynMatch) {
|
|
1190
|
+
const raw = dynAttrs[parseInt(dynMatch[1], 10)];
|
|
1191
|
+
if (/^\s*[A-Za-z_$][\w$]*\s*$/.test(raw)) {
|
|
1192
|
+
propValue = `(${raw})`;
|
|
1193
|
+
} else {
|
|
1194
|
+
checkExpression(raw, 'component prop');
|
|
1195
|
+
const rewritten = rewrite(raw);
|
|
1196
|
+
if (rewritten !== raw) reactiveProps = true;
|
|
1197
|
+
propValue = `(${rewritten})`;
|
|
1198
|
+
}
|
|
1199
|
+
} else if (handlerMatch) {
|
|
1200
|
+
propValue = `(${rewrite(handlers[parseInt(handlerMatch[1], 10)])})`;
|
|
1201
|
+
} else {
|
|
1202
|
+
propValue = JSON.stringify(val);
|
|
1203
|
+
}
|
|
1069
1204
|
propEntries.push(`${JSON.stringify(key)}: ${propValue}`);
|
|
1070
1205
|
}
|
|
1071
1206
|
|
|
@@ -1079,7 +1214,21 @@ export function compile(source, options = {}) {
|
|
|
1079
1214
|
propEntries.push(`children: ${slotVar}`);
|
|
1080
1215
|
}
|
|
1081
1216
|
|
|
1082
|
-
|
|
1217
|
+
// The child mounts untracked, so nothing it reads while building itself
|
|
1218
|
+
// can re-run the parent's effect. Its teardown is kept and registered with
|
|
1219
|
+
// whatever scope is being built, so it runs when that scope is destroyed.
|
|
1220
|
+
domCode += ` let ${cid}_unmount = null;\n`;
|
|
1221
|
+
domCode += ` onDestroy(() => { if (typeof ${cid}_unmount === 'function') ${cid}_unmount(); ${cid}_unmount = null; });\n`;
|
|
1222
|
+
if (reactiveProps) {
|
|
1223
|
+
domCode += ` createEffect(() => {\n`;
|
|
1224
|
+
domCode += ` const ${cid}_props = { ${propEntries.join(', ')} };\n`;
|
|
1225
|
+
domCode += ` if (typeof ${cid}_unmount === 'function') __untrack(${cid}_unmount);\n`;
|
|
1226
|
+
domCode += ` ${cid}_target.textContent = '';\n`;
|
|
1227
|
+
domCode += ` ${cid}_unmount = __untrack(() => ${node.name}(${cid}_target, ${cid}_props));\n`;
|
|
1228
|
+
domCode += ` });\n`;
|
|
1229
|
+
} else {
|
|
1230
|
+
domCode += ` ${cid}_unmount = __untrack(() => ${node.name}(${cid}_target, { ${propEntries.join(', ')} }));\n`;
|
|
1231
|
+
}
|
|
1083
1232
|
return;
|
|
1084
1233
|
}
|
|
1085
1234
|
|
|
@@ -1090,37 +1239,50 @@ export function compile(source, options = {}) {
|
|
|
1090
1239
|
}
|
|
1091
1240
|
|
|
1092
1241
|
// {#if ...} {:else} ... {/if}
|
|
1242
|
+
//
|
|
1243
|
+
// Each time a branch is built it gets its own scope (pushContext). Effects,
|
|
1244
|
+
// onDestroy callbacks and child components created inside register with that
|
|
1245
|
+
// scope, and switching or removing the branch destroys it, so nothing built
|
|
1246
|
+
// for a branch that is no longer on the page keeps running.
|
|
1093
1247
|
if (node.name === 'sola-if') {
|
|
1094
1248
|
const cid = `c${uid++}`;
|
|
1095
1249
|
domCode += ` const ${cid}_a = document.createComment('if');\n`;
|
|
1096
1250
|
domCode += ` ${parentVar}.appendChild(${cid}_a);\n`;
|
|
1097
1251
|
domCode += ` let ${cid}_els = [];\n`;
|
|
1098
|
-
|
|
1252
|
+
domCode += ` let ${cid}_ctx = null;\n`;
|
|
1253
|
+
domCode += ` onDestroy(() => { if (${cid}_ctx) { __flush_destroys(${cid}_ctx); ${cid}_ctx = null; } });\n`;
|
|
1254
|
+
|
|
1099
1255
|
const thenChild = node.children.find(c => c.name === 'sola-then');
|
|
1100
1256
|
const elseChild = node.children.find(c => c.name === 'sola-else');
|
|
1101
1257
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1258
|
+
const emitBranch = (branch) => {
|
|
1259
|
+
if (!branch) return;
|
|
1260
|
+
domCode += ` ${cid}_ctx = pushContext();\n`;
|
|
1261
|
+
domCode += ` try {\n`;
|
|
1106
1262
|
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1107
|
-
|
|
1263
|
+
branch.children.forEach(child => emitNode(child, 'f', locals));
|
|
1108
1264
|
domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
|
|
1109
1265
|
domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
|
|
1110
|
-
}
|
|
1266
|
+
domCode += ` } finally { popContext(${cid}_ctx); }\n`;
|
|
1267
|
+
};
|
|
1268
|
+
|
|
1269
|
+
domCode += ` createEffect(() => {\n`;
|
|
1270
|
+
domCode += ` if (${cid}_ctx) { __flush_destroys(${cid}_ctx); ${cid}_ctx = null; }\n`;
|
|
1271
|
+
domCode += ` ${cid}_els.forEach(e => e.remove()); ${cid}_els = [];\n`;
|
|
1272
|
+
domCode += ` if (${rewrite(node.attribs.condition)}) {\n`;
|
|
1273
|
+
emitBranch(thenChild);
|
|
1111
1274
|
domCode += ` } else {\n`;
|
|
1112
|
-
|
|
1113
|
-
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1114
|
-
elseChild.children.forEach(child => emitNode(child, 'f', locals));
|
|
1115
|
-
domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
|
|
1116
|
-
domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
|
|
1117
|
-
}
|
|
1275
|
+
emitBranch(elseChild);
|
|
1118
1276
|
domCode += ` }\n`;
|
|
1119
1277
|
domCode += ` });\n`;
|
|
1120
1278
|
return;
|
|
1121
1279
|
}
|
|
1122
1280
|
|
|
1123
1281
|
// {#each} with optional keyed diffing: {#each items as item (item.id)}
|
|
1282
|
+
//
|
|
1283
|
+
// Like {#if}, what is built for the list gets its own scope so it can be
|
|
1284
|
+
// destroyed: the whole list for an unkeyed each, one scope per key for a
|
|
1285
|
+
// keyed one (so removing an item stops that item's effects and children).
|
|
1124
1286
|
if (node.name === 'sola-each') {
|
|
1125
1287
|
const eid = `e${uid++}`;
|
|
1126
1288
|
// Item/index names shadow signals inside the loop body and key expression
|
|
@@ -1134,70 +1296,53 @@ export function compile(source, options = {}) {
|
|
|
1134
1296
|
domCode += ` const ${eid}_a = document.createComment('each');\n`;
|
|
1135
1297
|
domCode += ` ${parentVar}.appendChild(${eid}_a);\n`;
|
|
1136
1298
|
if (keyExpr) {
|
|
1137
|
-
// Keyed: maintain a Map<key,
|
|
1299
|
+
// Keyed: maintain a Map<key, { nodes, ctx }> and reconcile on each run
|
|
1138
1300
|
// Reconcile by walking the new order and moving each key's nodes into
|
|
1139
1301
|
// place against a cursor. The previous version only ever inserted, and
|
|
1140
1302
|
// against an anchor it never advanced — so reordering a keyed list (a
|
|
1141
1303
|
// sort, a drag, a reverse) left the DOM in its original order, silently.
|
|
1142
|
-
domCode += ` let ${eid}_keyMap = new Map()
|
|
1143
|
-
`;
|
|
1144
|
-
domCode += ` createEffect(() => {
|
|
1145
|
-
`;
|
|
1146
|
-
domCode += ` const
|
|
1147
|
-
`;
|
|
1148
|
-
domCode += `
|
|
1149
|
-
`;
|
|
1150
|
-
domCode += `
|
|
1151
|
-
`;
|
|
1152
|
-
domCode += `
|
|
1153
|
-
`;
|
|
1154
|
-
domCode += `
|
|
1155
|
-
`;
|
|
1156
|
-
domCode += `
|
|
1157
|
-
`;
|
|
1158
|
-
|
|
1159
|
-
`;
|
|
1160
|
-
domCode += ` const _key = String(${keyExpr});
|
|
1161
|
-
`;
|
|
1162
|
-
domCode += ` _seen.add(_key);
|
|
1163
|
-
`;
|
|
1164
|
-
domCode += ` let _nodes = ${eid}_keyMap.get(_key);
|
|
1165
|
-
`;
|
|
1166
|
-
domCode += ` if (!_nodes) {
|
|
1167
|
-
`;
|
|
1168
|
-
domCode += ` const f = document.createDocumentFragment();
|
|
1169
|
-
`;
|
|
1304
|
+
domCode += ` let ${eid}_keyMap = new Map();\n`;
|
|
1305
|
+
domCode += ` onDestroy(() => { for (const _e of ${eid}_keyMap.values()) __flush_destroys(_e.ctx); ${eid}_keyMap.clear(); });\n`;
|
|
1306
|
+
domCode += ` createEffect(() => {\n`;
|
|
1307
|
+
domCode += ` const _items = ${arrayExpr} || [];\n`;
|
|
1308
|
+
domCode += ` const _parent = ${eid}_a.parentNode;\n`;
|
|
1309
|
+
domCode += ` const _seen = new Set();\n`;
|
|
1310
|
+
domCode += ` let _cursor = ${eid}_a.nextSibling;\n`;
|
|
1311
|
+
domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
|
|
1312
|
+
domCode += ` const ${node.attribs.item} = _items[_i];\n`;
|
|
1313
|
+
if (node.attribs.index) domCode += ` const ${node.attribs.index} = _i;\n`;
|
|
1314
|
+
domCode += ` const _key = String(${keyExpr});\n`;
|
|
1315
|
+
domCode += ` _seen.add(_key);\n`;
|
|
1316
|
+
domCode += ` let _entry = ${eid}_keyMap.get(_key);\n`;
|
|
1317
|
+
domCode += ` if (!_entry) {\n`;
|
|
1318
|
+
domCode += ` const _kctx = pushContext();\n`;
|
|
1319
|
+
domCode += ` try {\n`;
|
|
1320
|
+
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1170
1321
|
node.children.forEach(child => emitNode(child, 'f', eachLocals));
|
|
1171
|
-
domCode += `
|
|
1172
|
-
`;
|
|
1173
|
-
domCode += ` ${eid}_keyMap.set(_key,
|
|
1174
|
-
`;
|
|
1175
|
-
domCode += `
|
|
1176
|
-
`;
|
|
1177
|
-
domCode += `
|
|
1178
|
-
`;
|
|
1179
|
-
domCode += `
|
|
1180
|
-
`;
|
|
1181
|
-
domCode += `
|
|
1182
|
-
`;
|
|
1183
|
-
domCode += `
|
|
1184
|
-
`;
|
|
1185
|
-
domCode += ` }
|
|
1186
|
-
`;
|
|
1187
|
-
domCode += ` for (const [_k, _stale] of ${eid}_keyMap) {
|
|
1188
|
-
`;
|
|
1189
|
-
domCode += ` if (!_seen.has(_k)) { _stale.forEach(n => n.remove()); ${eid}_keyMap.delete(_k); }
|
|
1190
|
-
`;
|
|
1191
|
-
domCode += ` }
|
|
1192
|
-
`;
|
|
1193
|
-
domCode += ` });
|
|
1194
|
-
`;
|
|
1322
|
+
domCode += ` _entry = { nodes: Array.from(f.childNodes), ctx: _kctx };\n`;
|
|
1323
|
+
domCode += ` } finally { popContext(_kctx); }\n`;
|
|
1324
|
+
domCode += ` ${eid}_keyMap.set(_key, _entry);\n`;
|
|
1325
|
+
domCode += ` }\n`;
|
|
1326
|
+
domCode += ` for (const _n of _entry.nodes) {\n`;
|
|
1327
|
+
domCode += ` if (_n === _cursor) { _cursor = _cursor.nextSibling; }\n`;
|
|
1328
|
+
domCode += ` else { _parent.insertBefore(_n, _cursor); }\n`;
|
|
1329
|
+
domCode += ` }\n`;
|
|
1330
|
+
domCode += ` }\n`;
|
|
1331
|
+
domCode += ` for (const [_k, _stale] of ${eid}_keyMap) {\n`;
|
|
1332
|
+
domCode += ` if (!_seen.has(_k)) { __flush_destroys(_stale.ctx); _stale.nodes.forEach(n => n.remove()); ${eid}_keyMap.delete(_k); }\n`;
|
|
1333
|
+
domCode += ` }\n`;
|
|
1334
|
+
domCode += ` });\n`;
|
|
1195
1335
|
} else {
|
|
1196
1336
|
// Unkeyed: full teardown + rebuild
|
|
1197
1337
|
domCode += ` let ${eid}_els = [];\n`;
|
|
1338
|
+
domCode += ` let ${eid}_ctx = null;\n`;
|
|
1339
|
+
domCode += ` onDestroy(() => { if (${eid}_ctx) { __flush_destroys(${eid}_ctx); ${eid}_ctx = null; } });\n`;
|
|
1198
1340
|
domCode += ` createEffect(() => {\n`;
|
|
1341
|
+
domCode += ` if (${eid}_ctx) { __flush_destroys(${eid}_ctx); ${eid}_ctx = null; }\n`;
|
|
1199
1342
|
domCode += ` ${eid}_els.forEach(e => e.remove()); ${eid}_els = [];\n`;
|
|
1200
1343
|
domCode += ` const _items = ${arrayExpr} || [];\n`;
|
|
1344
|
+
domCode += ` ${eid}_ctx = pushContext();\n`;
|
|
1345
|
+
domCode += ` try {\n`;
|
|
1201
1346
|
domCode += ` const f = document.createDocumentFragment();\n`;
|
|
1202
1347
|
domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
|
|
1203
1348
|
domCode += ` const ${node.attribs.item} = _items[_i];\n`;
|
|
@@ -1206,6 +1351,7 @@ export function compile(source, options = {}) {
|
|
|
1206
1351
|
domCode += ` }\n`;
|
|
1207
1352
|
domCode += ` Array.from(f.childNodes).forEach(n => ${eid}_els.push(n));\n`;
|
|
1208
1353
|
domCode += ` ${eid}_a.parentNode.insertBefore(f, ${eid}_a.nextSibling);\n`;
|
|
1354
|
+
domCode += ` } finally { popContext(${eid}_ctx); }\n`;
|
|
1209
1355
|
domCode += ` });\n`;
|
|
1210
1356
|
}
|
|
1211
1357
|
return;
|
|
@@ -1273,24 +1419,39 @@ export function compile(source, options = {}) {
|
|
|
1273
1419
|
root.children.forEach(child => emitNode(child, '__target'));
|
|
1274
1420
|
|
|
1275
1421
|
// Assemble final output module
|
|
1276
|
-
const CORE_EXPORTS = '
|
|
1422
|
+
const CORE_EXPORTS = [...CORE_PROVIDED].join(', ');
|
|
1423
|
+
// The default bus is imported under an alias so a user variable named `bus`
|
|
1424
|
+
// cannot shadow what $on / $lastEvent compile to.
|
|
1425
|
+
const ESM_CORE_IMPORTS = CORE_EXPORTS + ', bus as __solaBus';
|
|
1426
|
+
const IIFE_CORE_PULL = CORE_EXPORTS + ', bus: __solaBus';
|
|
1277
1427
|
let output = '';
|
|
1278
1428
|
output += `// Compiled by @sola-air-ui/compiler v${COMPILER_VERSION}\n`;
|
|
1279
1429
|
|
|
1280
1430
|
if (target === 'iife') {
|
|
1281
|
-
//
|
|
1431
|
+
// No-bundler mode: pull from window.SolaCore global
|
|
1282
1432
|
output += `(function() {\n`;
|
|
1283
|
-
|
|
1433
|
+
const iifeExtras = coreImportExtras.map(e => (e.local === e.imported ? e.imported : `${e.imported}: ${e.local}`));
|
|
1434
|
+
output += `const { ${[IIFE_CORE_PULL, ...iifeExtras].join(', ')} } = window.SolaCore;\n`;
|
|
1284
1435
|
for (const imp of componentImports) {
|
|
1285
1436
|
output += `// Note: sub-component ${imp.localName} must be pre-loaded before this script\n`;
|
|
1286
1437
|
}
|
|
1438
|
+
for (const imp of otherImports) {
|
|
1439
|
+
output += `// Note: ${imp.locals.join(', ')} must be loaded before this script (no-bundler output cannot import)\n`;
|
|
1440
|
+
}
|
|
1287
1441
|
output += `\n`;
|
|
1288
1442
|
output += `window['${options.exportName || 'SolaComponent'}'] = function mount(__target, props = {}) {\n`;
|
|
1289
1443
|
} else {
|
|
1290
|
-
output += `import { ${
|
|
1444
|
+
output += `import { ${ESM_CORE_IMPORTS} } from '@sola-air-ui/core';\n`;
|
|
1445
|
+
if (coreImportExtras.length) {
|
|
1446
|
+
const names = coreImportExtras.map(e => (e.local === e.imported ? e.imported : `${e.imported} as ${e.local}`));
|
|
1447
|
+
output += `import { ${names.join(', ')} } from '@sola-air-ui/core';\n`;
|
|
1448
|
+
}
|
|
1291
1449
|
for (const imp of componentImports) {
|
|
1292
1450
|
output += `import ${imp.localName} from '${imp.path}';\n`;
|
|
1293
1451
|
}
|
|
1452
|
+
for (const imp of otherImports) {
|
|
1453
|
+
output += `${imp.code}\n`;
|
|
1454
|
+
}
|
|
1294
1455
|
output += `\n`;
|
|
1295
1456
|
output += `export default function mount(__target, props = {}) {\n`;
|
|
1296
1457
|
}
|
|
@@ -1336,6 +1497,13 @@ export function compile(source, options = {}) {
|
|
|
1336
1497
|
output += `};\n`;
|
|
1337
1498
|
if (target === 'iife') {
|
|
1338
1499
|
output += `})();\n`;
|
|
1500
|
+
} else if (options.exportStyles) {
|
|
1501
|
+
// Opt-in: the stylesheet and its scope class as module exports, for hosts
|
|
1502
|
+
// that mount into a Shadow Root and adopt the CSS themselves (e.g. via
|
|
1503
|
+
// adoptedStyleSheets). Off by default so existing build scripts that append
|
|
1504
|
+
// their own `css` / `scopeHash` exports, or strip exports, keep working.
|
|
1505
|
+
output += `\nexport const scopeHash = ${JSON.stringify(scopeHash)};\n`;
|
|
1506
|
+
output += `export const css = \`${escapeTemplateLiteral(scopedCss || '')}\`;\n`;
|
|
1339
1507
|
}
|
|
1340
1508
|
|
|
1341
1509
|
// The reactive rewrite can add or remove characters within a line but never
|
|
@@ -1355,6 +1523,41 @@ export function compile(source, options = {}) {
|
|
|
1355
1523
|
code: output,
|
|
1356
1524
|
scopeHash,
|
|
1357
1525
|
css: scopedCss,
|
|
1358
|
-
map
|
|
1526
|
+
map,
|
|
1527
|
+
dts: buildDeclarations(exportedProps, !!options.exportStyles)
|
|
1359
1528
|
};
|
|
1360
1529
|
}
|
|
1530
|
+
|
|
1531
|
+
// Best-effort TypeScript type for a prop, from the literal its default is
|
|
1532
|
+
// declared with. Anything it cannot read confidently is `unknown`, which stays
|
|
1533
|
+
// safe for callers rather than claiming a precision it does not have.
|
|
1534
|
+
function inferPropType(defaultSrc) {
|
|
1535
|
+
const d = String(defaultSrc ?? '').trim();
|
|
1536
|
+
if (/^(['"`])[\s\S]*\1$/.test(d)) return 'string';
|
|
1537
|
+
if (/^-?\d[\d_]*(\.\d+)?([eE][+-]?\d+)?$/.test(d)) return 'number';
|
|
1538
|
+
if (d === 'true' || d === 'false') return 'boolean';
|
|
1539
|
+
// A null or undefined default says nothing about the type. `unknown` would stop a
|
|
1540
|
+
// TypeScript host from reading or calling the prop; `any` lets it.
|
|
1541
|
+
if (d === 'null' || d === 'undefined') return 'any';
|
|
1542
|
+
if (d.startsWith('[')) return 'unknown[]';
|
|
1543
|
+
if (d.startsWith('{')) return 'Record<string, unknown>';
|
|
1544
|
+
if (/^(\(.*\)|\w+)\s*=>|^function\b/.test(d)) return '(...args: any[]) => any';
|
|
1545
|
+
return 'unknown';
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// A `.d.ts` for a compiled component, so a TypeScript host can import the
|
|
1549
|
+
// compiled module with its props checked. Every prop is optional because each
|
|
1550
|
+
// has a default.
|
|
1551
|
+
function buildDeclarations(props, exportStyles) {
|
|
1552
|
+
const fields = props
|
|
1553
|
+
.map(p => ` ${/^[A-Za-z_$][\w$]*$/.test(p.name) ? p.name : JSON.stringify(p.name)}?: ${inferPropType(p.default)};`)
|
|
1554
|
+
.join('\n');
|
|
1555
|
+
return [
|
|
1556
|
+
`export interface Props {${fields ? '\n' + fields + '\n' : ''}}`,
|
|
1557
|
+
`/** Mounts the component into \`target\` and returns a teardown function. */`,
|
|
1558
|
+
`declare function mount(target: Element | ShadowRoot, props?: Props): () => void;`,
|
|
1559
|
+
`export default mount;`,
|
|
1560
|
+
...(exportStyles ? [`export declare const scopeHash: string;`, `export declare const css: string;`] : []),
|
|
1561
|
+
``
|
|
1562
|
+
].join('\n');
|
|
1563
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface CompileOptions {
|
|
|
4
4
|
/**
|
|
5
5
|
* 'esm' (default) emits a module importing from @sola-air-ui/core.
|
|
6
6
|
* 'iife' emits a self-contained bundle reading from window.SolaCore, for
|
|
7
|
-
*
|
|
7
|
+
* enterprise portals and other no-bundler hosts.
|
|
8
8
|
*/
|
|
9
9
|
target?: 'esm' | 'iife';
|
|
10
10
|
/** Global name the IIFE build registers itself under. Default 'SolaComponent'. */
|