@meekojs/meeko 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/check.d.ts +3 -0
- package/dist/check.js +130 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/compiler/mapping.d.ts +16 -0
- package/dist/compiler/mapping.js +48 -0
- package/dist/compiler/sfc.d.ts +43 -0
- package/dist/compiler/sfc.js +70 -0
- package/dist/compiler/template.d.ts +31 -0
- package/dist/compiler/template.js +44 -0
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +404 -0
- package/dist/dom.d.ts +43 -0
- package/dist/dom.js +315 -0
- package/dist/hmr.d.ts +24 -0
- package/dist/hmr.js +137 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -0
- package/dist/reactivity.d.ts +22 -0
- package/dist/reactivity.js +151 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +79 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +38 -0
- package/dist/vite.d.ts +3 -0
- package/dist/vite.js +47 -0
- package/package.json +69 -4
package/dist/compiler.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import ts from "@typescript/typescript6";
|
|
4
|
+
import postcss from "postcss";
|
|
5
|
+
import selectorParser from "postcss-selector-parser";
|
|
6
|
+
import { extractSourceMappings, markSource } from "./compiler/mapping.js";
|
|
7
|
+
import { compileError, locate, parseSfc } from "./compiler/sfc.js";
|
|
8
|
+
import { parseTemplate } from "./compiler/template.js";
|
|
9
|
+
function emptyBindings() {
|
|
10
|
+
return { texts: [], events: [], attributes: [], components: [], regions: [] };
|
|
11
|
+
}
|
|
12
|
+
function quote(value) {
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
function validateExpression(expression, source, filename, offset) {
|
|
16
|
+
const prefix = "const __check = (";
|
|
17
|
+
const parsed = ts.createSourceFile(filename, `${prefix}${expression});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
18
|
+
const error = parsed.parseDiagnostics[0];
|
|
19
|
+
if (error) {
|
|
20
|
+
const position = Math.max(0, Math.min(expression.length, (error.start ?? prefix.length) - prefix.length));
|
|
21
|
+
throw compileError(source, filename, "MEEK020", ts.flattenDiagnosticMessageText(error.messageText, "\n"), offset + position);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function attributeValueOffset(node, name, context) {
|
|
25
|
+
const position = node.sourceCodeLocation?.attrs?.[name];
|
|
26
|
+
if (!position)
|
|
27
|
+
return context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0);
|
|
28
|
+
const raw = context.source.slice(context.templateOffset + position.startOffset, context.templateOffset + position.endOffset);
|
|
29
|
+
const equals = raw.indexOf("=");
|
|
30
|
+
if (equals < 0)
|
|
31
|
+
return context.templateOffset + position.startOffset;
|
|
32
|
+
const after = raw.slice(equals + 1);
|
|
33
|
+
const whitespace = after.length - after.trimStart().length;
|
|
34
|
+
const quoteStart = equals + 1 + whitespace;
|
|
35
|
+
const quoted = raw[quoteStart] === '"' || raw[quoteStart] === "'";
|
|
36
|
+
return context.templateOffset + position.startOffset + quoteStart + (quoted ? 1 : 0);
|
|
37
|
+
}
|
|
38
|
+
function rewriteLocals(expression, locals) {
|
|
39
|
+
if (locals.size === 0)
|
|
40
|
+
return expression;
|
|
41
|
+
const prefix = "const __value = (";
|
|
42
|
+
const file = ts.createSourceFile("template-expression.ts", `${prefix}${expression});`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
43
|
+
const changes = [];
|
|
44
|
+
function isShadowed(node) {
|
|
45
|
+
for (let parent = node.parent; parent; parent = parent.parent) {
|
|
46
|
+
if ((ts.isArrowFunction(parent) || ts.isFunctionExpression(parent)) && parent.parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === node.text))
|
|
47
|
+
return true;
|
|
48
|
+
if (ts.isSourceFile(parent))
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function visit(node) {
|
|
54
|
+
if (ts.isIdentifier(node)) {
|
|
55
|
+
const replacement = locals.get(node.text);
|
|
56
|
+
const parent = node.parent;
|
|
57
|
+
if (replacement && parent && !isShadowed(node)
|
|
58
|
+
&& !(ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
59
|
+
&& !(ts.isPropertyAssignment(parent) && parent.name === node)
|
|
60
|
+
&& !(ts.isParameter(parent) && parent.name === node)
|
|
61
|
+
&& !(ts.isVariableDeclaration(parent) && parent.name === node)) {
|
|
62
|
+
const shorthand = ts.isShorthandPropertyAssignment(parent) && parent.name === node;
|
|
63
|
+
changes.push({ start: node.getStart(file) - prefix.length, end: node.getEnd() - prefix.length, text: shorthand ? `${node.text}: (${replacement})` : `(${replacement})` });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ts.forEachChild(node, visit);
|
|
67
|
+
}
|
|
68
|
+
visit(file);
|
|
69
|
+
let result = expression;
|
|
70
|
+
for (const change of changes.reverse()) {
|
|
71
|
+
if (change.start >= 0 && change.end <= expression.length)
|
|
72
|
+
result = result.slice(0, change.start) + change.text + result.slice(change.end);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function textParts(value, bindings, context, offset) {
|
|
77
|
+
const parts = [];
|
|
78
|
+
const pattern = /\{([^{}]+)\}/g;
|
|
79
|
+
let cursor = 0;
|
|
80
|
+
for (const match of value.matchAll(pattern)) {
|
|
81
|
+
const index = match.index;
|
|
82
|
+
const staticPart = value.slice(cursor, index);
|
|
83
|
+
if (staticPart)
|
|
84
|
+
parts.push(`__meeko.escapeHtml(${quote(staticPart)})`);
|
|
85
|
+
const rawExpression = match[1] ?? "";
|
|
86
|
+
const expression = rawExpression.trim();
|
|
87
|
+
if (!expression)
|
|
88
|
+
throw compileError(context.source, context.filename, "MEEK009", "Empty template expression", offset + index);
|
|
89
|
+
validateExpression(expression, context.source, context.filename, offset + index + 1 + rawExpression.indexOf(expression));
|
|
90
|
+
const id = context.nextText++;
|
|
91
|
+
const expressionOffset = offset + index + 1 + rawExpression.indexOf(expression);
|
|
92
|
+
bindings.texts.push({ id, expression, offset: expressionOffset });
|
|
93
|
+
parts.push(`${quote(`<!--meeko:text:${id}-->`)} + __meeko.escapeHtml(${markSource(expression, expressionOffset)}) + ${quote(`<!--/meeko:text:${id}-->`)}`);
|
|
94
|
+
cursor = index + match[0].length;
|
|
95
|
+
}
|
|
96
|
+
const tail = value.slice(cursor);
|
|
97
|
+
if (tail.includes("{"))
|
|
98
|
+
throw compileError(context.source, context.filename, "MEEK010", "Unclosed template expression", offset + cursor + tail.indexOf("{"));
|
|
99
|
+
if (tail)
|
|
100
|
+
parts.push(`__meeko.escapeHtml(${quote(tail)})`);
|
|
101
|
+
return parts;
|
|
102
|
+
}
|
|
103
|
+
function renderNode(node, bindings, context) {
|
|
104
|
+
if (node.nodeName === "#text") {
|
|
105
|
+
return textParts(node.value ?? "", bindings, context, context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0)).join(" + ") || quote("");
|
|
106
|
+
}
|
|
107
|
+
if (node.nodeName === "#comment")
|
|
108
|
+
return quote(`<!--${node.value ?? ""}-->`);
|
|
109
|
+
if (!node.tagName)
|
|
110
|
+
return (node.childNodes ?? []).map((child) => renderNode(child, bindings, context)).join(" + ") || quote("");
|
|
111
|
+
const attrsList = node.attrs ?? [];
|
|
112
|
+
const position = context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0);
|
|
113
|
+
const condition = attrsList.find((attribute) => attribute.name === "if:when");
|
|
114
|
+
const list = attrsList.find((attribute) => attribute.name === "each:of");
|
|
115
|
+
if (condition && list)
|
|
116
|
+
throw compileError(context.source, context.filename, "MEEK021", "A node cannot have both if:when and each:of", position);
|
|
117
|
+
if (condition || list) {
|
|
118
|
+
const inner = emptyBindings();
|
|
119
|
+
const excluded = new Set(condition ? ["if:when"] : ["each:of", "each:as", "each:key", "each:index"]);
|
|
120
|
+
const clone = { ...node, attrs: attrsList.filter((attribute) => !excluded.has(attribute.name)) };
|
|
121
|
+
const html = renderNodeContent(clone, inner, context);
|
|
122
|
+
const id = context.nextRegion++;
|
|
123
|
+
if (condition) {
|
|
124
|
+
const offset = attributeValueOffset(node, "if:when", context);
|
|
125
|
+
validateExpression(condition.value, context.source, context.filename, offset);
|
|
126
|
+
bindings.regions.push({ kind: "condition", id, expression: condition.value, offset, render: html, bindings: inner });
|
|
127
|
+
return `__meeko.renderIf(${id}, ${markSource(condition.value, offset)}, () => ${html})`;
|
|
128
|
+
}
|
|
129
|
+
const itemName = attrsList.find((attribute) => attribute.name === "each:as")?.value;
|
|
130
|
+
const key = attrsList.find((attribute) => attribute.name === "each:key")?.value;
|
|
131
|
+
const indexName = attrsList.find((attribute) => attribute.name === "each:index")?.value ?? "index";
|
|
132
|
+
if (!itemName || !/^[a-zA-Z_$][\w$]*$/.test(itemName) || itemName.startsWith("__") || !/^[a-zA-Z_$][\w$]*$/.test(indexName) || indexName.startsWith("__") || itemName === indexName) {
|
|
133
|
+
throw compileError(context.source, context.filename, "MEEK022", "A list needs valid, distinct each:as and each:index identifiers", position);
|
|
134
|
+
}
|
|
135
|
+
if (!key)
|
|
136
|
+
throw compileError(context.source, context.filename, "MEEK023", "A list requires each:key", position);
|
|
137
|
+
const offset = attributeValueOffset(node, "each:of", context);
|
|
138
|
+
const keyOffset = attributeValueOffset(node, "each:key", context);
|
|
139
|
+
validateExpression(list.value, context.source, context.filename, offset);
|
|
140
|
+
validateExpression(key, context.source, context.filename, keyOffset);
|
|
141
|
+
bindings.regions.push({ kind: "list", id, expression: list.value, offset, key, keyOffset, itemName, indexName, render: html, bindings: inner });
|
|
142
|
+
return `__meeko.renderFor(${id}, ${markSource(list.value, offset)}, (${itemName}, ${indexName}) => ${markSource(key, keyOffset)}, (${itemName}, ${indexName}) => ${html})`;
|
|
143
|
+
}
|
|
144
|
+
if (attrsList.some((attribute) => attribute.name.startsWith("each:") || attribute.name.startsWith("if:"))) {
|
|
145
|
+
throw compileError(context.source, context.filename, "MEEK024", "Incomplete template directive", position);
|
|
146
|
+
}
|
|
147
|
+
return renderNodeContent(node, bindings, context);
|
|
148
|
+
}
|
|
149
|
+
function renderNodeContent(node, bindings, context) {
|
|
150
|
+
if (node.tagName === "template") {
|
|
151
|
+
return (node.content?.childNodes ?? []).map((child) => renderNode(child, bindings, context)).join(" + ") || quote("");
|
|
152
|
+
}
|
|
153
|
+
if (node.tagName === "component") {
|
|
154
|
+
const position = node.start;
|
|
155
|
+
const referenceAttribute = node.attrs?.find((attribute) => attribute.name === "is");
|
|
156
|
+
const reference = referenceAttribute?.value ?? "";
|
|
157
|
+
if (!/^[a-zA-Z_$][\w$]*$/.test(reference) || reference.startsWith("__")) {
|
|
158
|
+
throw compileError(context.source, context.filename, "MEEK030", "A component requires is=\"ImportedComponent\"", position);
|
|
159
|
+
}
|
|
160
|
+
if ((node.childNodes ?? []).some((child) => child.nodeName !== "#text" || Boolean(child.value?.trim()))) {
|
|
161
|
+
throw compileError(context.source, context.filename, "MEEK031", "Component children are not supported; close the tag without content", position);
|
|
162
|
+
}
|
|
163
|
+
const props = [];
|
|
164
|
+
for (const attribute of node.attrs ?? []) {
|
|
165
|
+
if (attribute.name === "is")
|
|
166
|
+
continue;
|
|
167
|
+
const rawName = /^[^\s=]+/.exec(context.source.slice(attribute.start, attribute.end))?.[0] ?? attribute.name;
|
|
168
|
+
if (!rawName.startsWith("bind:") || !/^[a-zA-Z_$][\w$]*$/.test(rawName.slice(5))) {
|
|
169
|
+
throw compileError(context.source, context.filename, "MEEK032", "Component props must use bind:prop=\"expression\"", attribute.start);
|
|
170
|
+
}
|
|
171
|
+
const name = rawName.slice(5);
|
|
172
|
+
const offset = attributeValueOffset(node, attribute.name, context);
|
|
173
|
+
validateExpression(attribute.value, context.source, context.filename, offset);
|
|
174
|
+
props.push({ name, nameOffset: attribute.start + 5, expression: attribute.value, offset });
|
|
175
|
+
}
|
|
176
|
+
const id = context.nextRegion++;
|
|
177
|
+
const referenceOffset = attributeValueOffset(node, "is", context);
|
|
178
|
+
bindings.components.push({ id, reference, referenceOffset, props, offset: position });
|
|
179
|
+
const mappedProps = props.map((prop) => `${markSource(quote(prop.name), prop.nameOffset, prop.name.length)}: ${markSource(prop.expression, prop.offset)}`);
|
|
180
|
+
const object = markSource(`{ ${mappedProps.join(", ")} }`, position, node.end - position);
|
|
181
|
+
return `__meeko.renderComponent(${id}, ${markSource(reference, referenceOffset)}, ${object}, __meeko_transfer)`;
|
|
182
|
+
}
|
|
183
|
+
const tag = node.tagName;
|
|
184
|
+
const attrs = [];
|
|
185
|
+
for (const attr of node.attrs ?? []) {
|
|
186
|
+
if (attr.name.startsWith("on:")) {
|
|
187
|
+
const name = attr.name.slice(3);
|
|
188
|
+
if (!/^[a-z]+$/.test(name))
|
|
189
|
+
throw compileError(context.source, context.filename, "MEEK011", `Invalid event name: ${name}`, context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0));
|
|
190
|
+
const id = context.nextEvent++;
|
|
191
|
+
const offset = attributeValueOffset(node, attr.name, context);
|
|
192
|
+
validateExpression(attr.value, context.source, context.filename, offset);
|
|
193
|
+
bindings.events.push({ id, name, expression: attr.value, offset });
|
|
194
|
+
attrs.push({ kind: "static", value: ` data-meeko-e-${id}=""` });
|
|
195
|
+
}
|
|
196
|
+
else if (attr.name.startsWith("bind:")) {
|
|
197
|
+
const name = attr.name.slice(5);
|
|
198
|
+
if (!/^[a-z][a-z0-9:.-]*$/.test(name) || name.startsWith("on")) {
|
|
199
|
+
throw compileError(context.source, context.filename, "MEEK012", `Invalid dynamic attribute: ${name}`, context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0));
|
|
200
|
+
}
|
|
201
|
+
const offset = attributeValueOffset(node, attr.name, context);
|
|
202
|
+
validateExpression(attr.value, context.source, context.filename, offset);
|
|
203
|
+
const id = context.nextAttribute++;
|
|
204
|
+
bindings.attributes.push({ id, name, expression: attr.value, offset });
|
|
205
|
+
attrs.push({ kind: "static", value: ` data-meeko-a-${id}=""` });
|
|
206
|
+
attrs.push({ kind: "dynamic", expression: `__meeko.renderAttribute(${quote(name)}, ${markSource(attr.value, offset)})` });
|
|
207
|
+
}
|
|
208
|
+
else if (attr.name.startsWith(":")) {
|
|
209
|
+
throw compileError(context.source, context.filename, "MEEK012", `Unsupported template directive: ${attr.name}`, context.templateOffset + (node.sourceCodeLocation?.startOffset ?? 0));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
attrs.push({ kind: "static", value: ` ${attr.name}="${escapeAttribute(attr.value)}"` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (context.scopeAttribute)
|
|
216
|
+
attrs.push({ kind: "static", value: ` ${context.scopeAttribute}=""` });
|
|
217
|
+
const openingParts = [quote(`<${tag}`)];
|
|
218
|
+
for (const attr of attrs) {
|
|
219
|
+
if (attr.kind === "dynamic")
|
|
220
|
+
openingParts.push(attr.expression);
|
|
221
|
+
else
|
|
222
|
+
openingParts.push(quote(attr.value));
|
|
223
|
+
}
|
|
224
|
+
openingParts.push(quote(">"));
|
|
225
|
+
const opening = openingParts.join(" + ");
|
|
226
|
+
const voidElements = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
227
|
+
if (voidElements.has(tag))
|
|
228
|
+
return opening;
|
|
229
|
+
const children = (node.childNodes ?? []).map((child) => renderNode(child, bindings, context));
|
|
230
|
+
return [opening, ...children, quote(`</${tag}>`)].join(" + ");
|
|
231
|
+
}
|
|
232
|
+
function escapeAttribute(value) {
|
|
233
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<");
|
|
234
|
+
}
|
|
235
|
+
function scopeCss(css, scopeAttribute) {
|
|
236
|
+
const root = postcss.parse(css);
|
|
237
|
+
root.walkRules((rule) => {
|
|
238
|
+
let parent = rule.parent;
|
|
239
|
+
while (parent) {
|
|
240
|
+
if (parent.type === "atrule" && /keyframes$/i.test(parent.name))
|
|
241
|
+
return;
|
|
242
|
+
parent = parent.parent;
|
|
243
|
+
}
|
|
244
|
+
if (rule.selector.includes(":global"))
|
|
245
|
+
throw rule.error(":global is not supported in scoped styles yet", { index: rule.selector.indexOf(":global") });
|
|
246
|
+
rule.selector = selectorParser((selectors) => {
|
|
247
|
+
selectors.each((selector) => {
|
|
248
|
+
const pseudoElement = selector.nodes.find((node) => node.type === "pseudo" && node.value.startsWith("::"));
|
|
249
|
+
const attribute = selectorParser.attribute({ attribute: scopeAttribute, value: undefined, raws: {} });
|
|
250
|
+
if (pseudoElement)
|
|
251
|
+
selector.insertBefore(pseudoElement, attribute);
|
|
252
|
+
else
|
|
253
|
+
selector.append(attribute);
|
|
254
|
+
});
|
|
255
|
+
}).processSync(rule.selector);
|
|
256
|
+
});
|
|
257
|
+
return root.toString();
|
|
258
|
+
}
|
|
259
|
+
function compileScript(section, source, filename) {
|
|
260
|
+
const file = ts.createSourceFile(filename, section.content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
261
|
+
const syntaxError = file.parseDiagnostics[0];
|
|
262
|
+
if (syntaxError) {
|
|
263
|
+
const message = ts.flattenDiagnosticMessageText(syntaxError.messageText, "\n");
|
|
264
|
+
throw compileError(source, filename, "MEEK013", message, section.contentStart + (syntaxError.start ?? 0));
|
|
265
|
+
}
|
|
266
|
+
const imports = [];
|
|
267
|
+
const types = [];
|
|
268
|
+
const setup = [];
|
|
269
|
+
const names = [];
|
|
270
|
+
let propsType;
|
|
271
|
+
let propsTypeOffset;
|
|
272
|
+
for (const statement of file.statements) {
|
|
273
|
+
if (ts.isImportDeclaration(statement)) {
|
|
274
|
+
imports.push(markSource(statement.getText(file), section.contentStart + statement.getStart(file)));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) {
|
|
278
|
+
types.push(markSource(statement.getText(file), section.contentStart + statement.getStart(file)));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (ts.isVariableStatement(statement)) {
|
|
282
|
+
if (statement.declarationList.declarations.length !== 1)
|
|
283
|
+
throw compileError(source, filename, "MEEK014", "Declare one variable per statement in .meeko scripts", section.contentStart + statement.getStart(file));
|
|
284
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
285
|
+
if (!ts.isIdentifier(declaration.name))
|
|
286
|
+
throw compileError(source, filename, "MEEK015", "Only simple variable declarations are supported in .meeko scripts", section.contentStart + declaration.getStart(file));
|
|
287
|
+
const name = declaration.name.text;
|
|
288
|
+
names.push(name);
|
|
289
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && declaration.initializer.expression.getText(file) === "defineProps") {
|
|
290
|
+
if (propsType)
|
|
291
|
+
throw compileError(source, filename, "MEEK016", "defineProps must appear once in its own declaration", section.contentStart + declaration.getStart(file));
|
|
292
|
+
const typeArgument = declaration.initializer.typeArguments?.[0];
|
|
293
|
+
propsType = typeArgument?.getText(file);
|
|
294
|
+
if (!propsType)
|
|
295
|
+
throw compileError(source, filename, "MEEK017", "defineProps requires a type argument", section.contentStart + declaration.getStart(file));
|
|
296
|
+
propsTypeOffset = section.contentStart + typeArgument.getStart(file);
|
|
297
|
+
setup.push(`const ${name}: ${markSource(propsType, propsTypeOffset)} = __props;`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
setup.push(markSource(statement.getText(file), section.contentStart + statement.getStart(file)));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
306
|
+
names.push(statement.name.text);
|
|
307
|
+
setup.push(markSource(statement.getText(file), section.contentStart + statement.getStart(file)));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
throw compileError(source, filename, "MEEK018", `Unsupported top-level script statement: ${ts.SyntaxKind[statement.kind]}`, section.contentStart + statement.getStart(file));
|
|
311
|
+
}
|
|
312
|
+
return { imports, types, setup, names, propsType: propsType ?? "Record<string, never>", propsTypeOffset };
|
|
313
|
+
}
|
|
314
|
+
function hydrationBody(bindings, context, locals = new Map()) {
|
|
315
|
+
const statements = [];
|
|
316
|
+
const mappedExpression = (expression, offset, expressionLocals = locals) => markSource(rewriteLocals(expression, expressionLocals), offset, expression.length);
|
|
317
|
+
for (const { id, expression, offset } of bindings.texts) {
|
|
318
|
+
const point = locate(context.source, offset);
|
|
319
|
+
const location = `${basename(context.filename)}:${point.line}:${point.column} (text ${id})`;
|
|
320
|
+
statements.push(`cleanups.push(__meeko.bindText(__scope, ${id}, () => ${mappedExpression(expression, offset)}, ${quote(location)}));`);
|
|
321
|
+
}
|
|
322
|
+
for (const { id, name, expression, offset } of bindings.attributes) {
|
|
323
|
+
const point = locate(context.source, offset);
|
|
324
|
+
const location = `${basename(context.filename)}:${point.line}:${point.column} (attribute ${name})`;
|
|
325
|
+
statements.push(`cleanups.push(__meeko.bindAttribute(__scope, ${id}, ${quote(name)}, () => ${mappedExpression(expression, offset)}, ${quote(location)}));`);
|
|
326
|
+
}
|
|
327
|
+
for (const { id, name, expression, offset } of bindings.events) {
|
|
328
|
+
const eventType = ["click", "input", "change", "submit", "keydown", "keyup", "focus", "blur"].includes(name)
|
|
329
|
+
? `GlobalEventHandlersEventMap[${quote(name)}]` : "Event";
|
|
330
|
+
const handler = markSource(`((__meeko_handler: (event: ${eventType}) => void) => __meeko_handler(event as ${eventType}))(${mappedExpression(expression, offset)});`, offset, expression.length);
|
|
331
|
+
statements.push(`{
|
|
332
|
+
cleanups.push(__meeko.bindEvent(__scope, ${id}, ${quote(name)}, (event: Event) => {
|
|
333
|
+
${handler}
|
|
334
|
+
}));
|
|
335
|
+
}`);
|
|
336
|
+
}
|
|
337
|
+
const componentStatements = [];
|
|
338
|
+
for (const entry of bindings.components) {
|
|
339
|
+
const point = locate(context.source, entry.offset);
|
|
340
|
+
const location = `${basename(context.filename)}:${point.line}:${point.column} (component ${entry.reference})`;
|
|
341
|
+
const props = entry.props.map((prop) => `${markSource(quote(prop.name), prop.nameOffset, prop.name.length)}: ${mappedExpression(prop.expression, prop.offset)}`);
|
|
342
|
+
const object = markSource(`{ ${props.join(", ")} }`, entry.offset, 1);
|
|
343
|
+
componentStatements.push(`cleanups.push(__meeko.bindComponent(__scope, ${entry.id}, ${mappedExpression(entry.reference, entry.referenceOffset)}, () => (${object}), ${quote(location)}));`);
|
|
344
|
+
}
|
|
345
|
+
statements.unshift(...componentStatements);
|
|
346
|
+
for (const entry of bindings.regions) {
|
|
347
|
+
if (entry.kind === "condition") {
|
|
348
|
+
statements.push(`cleanups.push(__meeko.bindIf(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, () => ${rewriteLocals(entry.render, locals)}, (__scope) => { ${hydrationBody(entry.bindings, context, locals)} }));`);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const outerLocals = new Map(locals);
|
|
352
|
+
outerLocals.delete(entry.itemName);
|
|
353
|
+
outerLocals.delete(entry.indexName);
|
|
354
|
+
const innerLocals = new Map(locals);
|
|
355
|
+
innerLocals.set(entry.itemName, "__meeko_item.value");
|
|
356
|
+
innerLocals.set(entry.indexName, "__meeko_index.value");
|
|
357
|
+
statements.push(`cleanups.push(__meeko.bindFor(__scope, ${entry.id}, () => ${mappedExpression(entry.expression, entry.offset)}, (${entry.itemName}, ${entry.indexName}) => ${mappedExpression(entry.key, entry.keyOffset, outerLocals)}, (${entry.itemName}, ${entry.indexName}) => ${rewriteLocals(entry.render, outerLocals)}, (__meeko_item, __meeko_index, __scope) => { ${hydrationBody(entry.bindings, context, innerLocals)} }));`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return `const cleanups: Array<() => void> = [];
|
|
361
|
+
try {
|
|
362
|
+
${statements.join("\n")}
|
|
363
|
+
return () => { for (const cleanup of cleanups.reverse()) cleanup(); };
|
|
364
|
+
} catch (error) { for (const cleanup of cleanups.reverse()) cleanup(); throw error; }`;
|
|
365
|
+
}
|
|
366
|
+
/** Compila una parte tipada de .meeko para SSR e hidratación del HTML existente. */
|
|
367
|
+
export function compile(source, filename = "anonymous.meeko", options = {}) {
|
|
368
|
+
const { script, template, style } = parseSfc(source, filename);
|
|
369
|
+
const componentId = createHash("sha256").update(filename).digest("hex").slice(0, 12);
|
|
370
|
+
const scopeAttribute = style?.attributes.some((attribute) => attribute.name === "scoped")
|
|
371
|
+
? `data-meeko-s-${createHash("sha256").update(filename).digest("hex").slice(0, 8)}`
|
|
372
|
+
: undefined;
|
|
373
|
+
let css;
|
|
374
|
+
try {
|
|
375
|
+
css = scopeAttribute ? scopeCss(style?.content ?? "", scopeAttribute) : style?.content ?? "";
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
if (error instanceof postcss.CssSyntaxError && style && error.line !== undefined && error.column !== undefined) {
|
|
379
|
+
const lines = style.content.split(/\r\n|\r|\n/);
|
|
380
|
+
const prior = lines.slice(0, error.line - 1).reduce((total, line) => total + line.length + 1, 0);
|
|
381
|
+
throw compileError(source, filename, "MEEK019", error.reason, style.contentStart + prior + error.column - 1);
|
|
382
|
+
}
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
const parsed = parseTemplate(source, template, filename);
|
|
386
|
+
const bindingsForTemplate = emptyBindings();
|
|
387
|
+
const context = { source, filename, templateOffset: template.contentStart, scopeAttribute, nextText: 0, nextEvent: 0, nextAttribute: 0, nextRegion: 0 };
|
|
388
|
+
const html = renderNode(parsed, bindingsForTemplate, context);
|
|
389
|
+
const scriptParts = compileScript(script, source, filename);
|
|
390
|
+
const bindings = scriptParts.names.join(", ");
|
|
391
|
+
const hot = options.hot === true;
|
|
392
|
+
const setupCode = `const __bindings = __setup(__inputProps);\n__meeko.restoreSignals(__bindings, __state);\nconst { ${bindings} } = __bindings;`;
|
|
393
|
+
const releaseCode = hot
|
|
394
|
+
? `const __cleanup = () => { __release(); dispose(); };\nconst __component = { renderToString, hydrate, hydrateScope, css };\nconst __snapshot = () => __meeko.snapshotSignals(__bindings);\nconst __stop = __hotRoot ? __meeko.registerHotInstance(__hotId, __hotRoot, __inputProps, __component, __snapshot, __cleanup) : __scope.start ? __meeko.registerHotScope(__hotId, __scope, __inputProps, __component, __snapshot, __cleanup) : __cleanup;\nreturn __meeko.withStateSnapshot(__stop, __snapshot);`
|
|
395
|
+
: `return __meeko.withStateSnapshot(() => { __release(); dispose(); }, () => __meeko.snapshotSignals(__bindings));`;
|
|
396
|
+
const hotIdExport = hot ? `export const __hotId = ${quote(componentId)};\n` : "";
|
|
397
|
+
const stateParam = ", __state?: __meeko.HotState";
|
|
398
|
+
const scopeHotRootParam = hot ? ", __hotRoot?: Element" : "";
|
|
399
|
+
const scopeForward = hot ? "__scope, __inputProps, __state, root" : "__scope, __inputProps, __state";
|
|
400
|
+
const propsType = scriptParts.propsTypeOffset === undefined ? scriptParts.propsType : markSource(scriptParts.propsType, scriptParts.propsTypeOffset);
|
|
401
|
+
const markedCode = `// Generated by Meeko. Edit the .meeko source instead.\nimport * as __meeko from "@meekojs/meeko";\n${scriptParts.imports.join("\n")}\n${scriptParts.types.join("\n")}\nfunction __setup(__props: ${propsType}) {\n${scriptParts.setup.join("\n")}\nreturn { ${bindings} };\n}\nexport type ComponentProps = ${propsType};\nexport const css = ${quote(css)};\n${hotIdExport}export function renderWithState(__inputProps: ComponentProps): { html: string; state: __meeko.HotState } {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = true;\nconst __bindings = __setup(__inputProps);\nconst { ${bindings} } = __bindings;\ntry {\nconst html = ${html};\nconst state = __meeko.snapshotSignals(__bindings);\n__meeko.serializeJson(state);\nreturn { html, state };\n} finally { dispose(); }\n});\n}\nexport function renderToString(__inputProps: ComponentProps${stateParam}): string {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\ntry { return ${html}; } finally { dispose(); }\n});\n}\nexport function hydrate(root: Element, __inputProps: ComponentProps${stateParam}): () => void {\nconst __scope: __meeko.DomScope = { root };\nreturn hydrateScope(${scopeForward});\n}\nexport function hydrateScope(__scope: __meeko.DomScope, __inputProps: ComponentProps${stateParam}${scopeHotRootParam}): () => void {\nreturn __meeko.createRoot((dispose) => {\nconst __meeko_transfer = false;\n${setupCode}\nconst __release = (() => { ${hydrationBody(bindingsForTemplate, context)} })();\n${releaseCode}\n});\n}\n`;
|
|
402
|
+
const { code, mappings } = extractSourceMappings(markedCode);
|
|
403
|
+
return { code, css, mappings };
|
|
404
|
+
}
|
package/dist/dom.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Signal } from "./reactivity.js";
|
|
2
|
+
/** Fragmento de DOM que pertenece a una instancia o a una región dinámica. */
|
|
3
|
+
export interface DomScope {
|
|
4
|
+
root: Element;
|
|
5
|
+
start?: Comment;
|
|
6
|
+
end?: Comment;
|
|
7
|
+
}
|
|
8
|
+
/** Contrato de un módulo de componente compilado para composición sin wrapper. */
|
|
9
|
+
export interface ComponentModule<Props extends object> {
|
|
10
|
+
renderToString(props: Props, state?: Record<string, unknown>): string;
|
|
11
|
+
renderWithState?(props: Props): {
|
|
12
|
+
html: string;
|
|
13
|
+
state: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
hydrateScope(scope: DomScope, props: Props, state?: Record<string, unknown>): ComponentRelease;
|
|
16
|
+
}
|
|
17
|
+
/** Función de limpieza que puede exponer el estado editable de su instancia. */
|
|
18
|
+
export interface ComponentRelease {
|
|
19
|
+
(): void;
|
|
20
|
+
snapshot?: () => Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
/** Añade una lectura de estado a la limpieza de un componente compilado. */
|
|
23
|
+
export declare function withStateSnapshot(release: () => void, snapshot: () => Record<string, unknown>): ComponentRelease;
|
|
24
|
+
/** Renderiza un hijo entre comentarios de ámbito sin añadir un elemento visual. */
|
|
25
|
+
export declare function renderComponent<Props extends object>(id: number, component: ComponentModule<Props>, props: Props, transferState?: boolean): string;
|
|
26
|
+
/** Hidrata un hijo dentro de su rango y actualiza props reactivas con limpieza previa. */
|
|
27
|
+
export declare function bindComponent<Props extends object>(scope: DomScope, id: number, component: ComponentModule<Props>, read: () => Props, location: string): () => void;
|
|
28
|
+
/** Conecta un texto reactivo al marcador SSR y comprueba su valor inicial. */
|
|
29
|
+
export declare function bindText(scope: DomScope, id: number, read: () => unknown, location: string): () => void;
|
|
30
|
+
/** Conecta un atributo dinámico al elemento SSR y comprueba su valor inicial. */
|
|
31
|
+
export declare function bindAttribute(scope: DomScope, id: number, name: string, read: () => unknown, location: string): () => void;
|
|
32
|
+
/** Conecta un evento al elemento SSR y entrega su función de limpieza. */
|
|
33
|
+
export declare function bindEvent(scope: DomScope, id: number, name: string, handler: (event: Event) => void): () => void;
|
|
34
|
+
/** Genera los límites estables de una condición en SSR. */
|
|
35
|
+
export declare function renderIf(id: number, condition: unknown, render: () => string): string;
|
|
36
|
+
/** Hidrata una condición y conserva los nodos mientras sigue siendo verdadera. */
|
|
37
|
+
export declare function bindIf(scope: DomScope, id: number, read: () => unknown, render: () => string, hydrate: (inner: DomScope) => () => void): () => void;
|
|
38
|
+
/** Convierte una clave de lista en un token seguro para comentarios HTML. */
|
|
39
|
+
export declare function keyToken(key: unknown): string;
|
|
40
|
+
/** Renderiza una lista con límites identificados por claves únicas. */
|
|
41
|
+
export declare function renderFor<T>(id: number, items: Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string): string;
|
|
42
|
+
/** Hidrata una lista y reordena regiones por clave sin reemplazar nodos conservados. */
|
|
43
|
+
export declare function bindFor<T>(scope: DomScope, id: number, read: () => Iterable<T>, keyOf: (item: T, index: number) => string | number, render: (item: T, index: number) => string, hydrate: (item: Signal<T>, index: Signal<number>, inner: DomScope) => () => void): () => void;
|