@kuratchi/js 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -68
- package/dist/cli.js +80 -47
- package/dist/compiler/api-route-pipeline.d.ts +8 -0
- package/dist/compiler/api-route-pipeline.js +23 -0
- package/dist/compiler/asset-pipeline.d.ts +7 -0
- package/dist/compiler/asset-pipeline.js +33 -0
- package/dist/compiler/client-module-pipeline.d.ts +25 -0
- package/dist/compiler/client-module-pipeline.js +257 -0
- package/dist/compiler/compiler-shared.d.ts +55 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +163 -0
- package/dist/compiler/config-reading.d.ts +11 -0
- package/dist/compiler/config-reading.js +323 -0
- package/dist/compiler/convention-discovery.d.ts +9 -0
- package/dist/compiler/convention-discovery.js +83 -0
- package/dist/compiler/durable-object-pipeline.d.ts +9 -0
- package/dist/compiler/durable-object-pipeline.js +255 -0
- package/dist/compiler/error-page-pipeline.d.ts +1 -0
- package/dist/compiler/error-page-pipeline.js +16 -0
- package/dist/compiler/import-linking.d.ts +36 -0
- package/dist/compiler/import-linking.js +139 -0
- package/dist/compiler/index.d.ts +3 -3
- package/dist/compiler/index.js +137 -3265
- package/dist/compiler/layout-pipeline.d.ts +31 -0
- package/dist/compiler/layout-pipeline.js +155 -0
- package/dist/compiler/page-route-pipeline.d.ts +16 -0
- package/dist/compiler/page-route-pipeline.js +62 -0
- package/dist/compiler/parser.d.ts +4 -0
- package/dist/compiler/parser.js +433 -51
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +517 -0
- package/dist/compiler/route-discovery.d.ts +7 -0
- package/dist/compiler/route-discovery.js +87 -0
- package/dist/compiler/route-pipeline.d.ts +57 -0
- package/dist/compiler/route-pipeline.js +296 -0
- package/dist/compiler/route-state-pipeline.d.ts +25 -0
- package/dist/compiler/route-state-pipeline.js +139 -0
- package/dist/compiler/routes-module-feature-blocks.d.ts +2 -0
- package/dist/compiler/routes-module-feature-blocks.js +330 -0
- package/dist/compiler/routes-module-pipeline.d.ts +2 -0
- package/dist/compiler/routes-module-pipeline.js +6 -0
- package/dist/compiler/routes-module-runtime-shell.d.ts +2 -0
- package/dist/compiler/routes-module-runtime-shell.js +81 -0
- package/dist/compiler/routes-module-types.d.ts +44 -0
- package/dist/compiler/routes-module-types.js +1 -0
- package/dist/compiler/script-transform.d.ts +16 -0
- package/dist/compiler/script-transform.js +218 -0
- package/dist/compiler/server-module-pipeline.d.ts +13 -0
- package/dist/compiler/server-module-pipeline.js +124 -0
- package/dist/compiler/template.d.ts +13 -1
- package/dist/compiler/template.js +323 -60
- package/dist/compiler/worker-output-pipeline.d.ts +13 -0
- package/dist/compiler/worker-output-pipeline.js +37 -0
- package/dist/compiler/wrangler-sync.d.ts +14 -0
- package/dist/compiler/wrangler-sync.js +185 -0
- package/dist/runtime/app.js +15 -3
- package/dist/runtime/generated-worker.d.ts +33 -0
- package/dist/runtime/generated-worker.js +412 -0
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/router.d.ts +2 -1
- package/dist/runtime/router.js +12 -3
- package/dist/runtime/types.d.ts +8 -2
- package/package.json +5 -1
|
@@ -37,13 +37,244 @@ const JS_CONTROL_PATTERNS = [
|
|
|
37
37
|
function isJsControlLine(line) {
|
|
38
38
|
return JS_CONTROL_PATTERNS.some(p => p.test(line));
|
|
39
39
|
}
|
|
40
|
+
const FRAGMENT_OPEN_MARKER = '<!--__KURATCHI_FRAGMENT_OPEN:';
|
|
41
|
+
const FRAGMENT_CLOSE_MARKER = '<!--__KURATCHI_FRAGMENT_CLOSE-->';
|
|
42
|
+
export function splitTemplateRenderSections(template) {
|
|
43
|
+
const bodyLines = [];
|
|
44
|
+
const headLines = [];
|
|
45
|
+
const lines = template.split('\n');
|
|
46
|
+
let inHead = false;
|
|
47
|
+
let inStyle = false;
|
|
48
|
+
let inScript = false;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
const opensStyle = /<style[\s>]/i.test(trimmed);
|
|
52
|
+
const closesStyle = /<\/style>/i.test(trimmed);
|
|
53
|
+
const opensScript = /<script[\s>]/i.test(trimmed);
|
|
54
|
+
const closesScript = /<\/script>/i.test(trimmed);
|
|
55
|
+
const inRawBlock = inStyle || inScript || opensStyle || opensScript;
|
|
56
|
+
if (inRawBlock) {
|
|
57
|
+
let remaining = line;
|
|
58
|
+
let bodyLine = '';
|
|
59
|
+
let headLine = '';
|
|
60
|
+
while (remaining.length > 0) {
|
|
61
|
+
if (!inHead) {
|
|
62
|
+
const openMatch = remaining.match(/<head(?:\s[^>]*)?>/i);
|
|
63
|
+
if (!openMatch || openMatch.index === undefined) {
|
|
64
|
+
bodyLine += remaining;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
bodyLine += remaining.slice(0, openMatch.index);
|
|
68
|
+
remaining = remaining.slice(openMatch.index + openMatch[0].length);
|
|
69
|
+
const closeMatch = remaining.match(/<\/head>/i);
|
|
70
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
71
|
+
headLine += remaining;
|
|
72
|
+
remaining = '';
|
|
73
|
+
inHead = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
headLine += remaining.slice(0, closeMatch.index);
|
|
77
|
+
remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const closeMatch = remaining.match(/<\/head>/i);
|
|
81
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
82
|
+
headLine += remaining;
|
|
83
|
+
remaining = '';
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
headLine += remaining.slice(0, closeMatch.index);
|
|
87
|
+
remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
|
|
88
|
+
inHead = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
bodyLines.push(bodyLine);
|
|
92
|
+
headLines.push(headLine);
|
|
93
|
+
if (opensStyle && !closesStyle)
|
|
94
|
+
inStyle = true;
|
|
95
|
+
if (closesStyle)
|
|
96
|
+
inStyle = false;
|
|
97
|
+
if (opensScript && !closesScript)
|
|
98
|
+
inScript = true;
|
|
99
|
+
if (closesScript)
|
|
100
|
+
inScript = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (isJsControlLine(trimmed) && !/<\/?head(?:\s|>)/i.test(line)) {
|
|
104
|
+
bodyLines.push(line);
|
|
105
|
+
headLines.push(line);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let remaining = line;
|
|
109
|
+
let bodyLine = '';
|
|
110
|
+
let headLine = '';
|
|
111
|
+
while (remaining.length > 0) {
|
|
112
|
+
if (!inHead) {
|
|
113
|
+
const openMatch = remaining.match(/<head(?:\s[^>]*)?>/i);
|
|
114
|
+
if (!openMatch || openMatch.index === undefined) {
|
|
115
|
+
bodyLine += remaining;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
bodyLine += remaining.slice(0, openMatch.index);
|
|
119
|
+
remaining = remaining.slice(openMatch.index + openMatch[0].length);
|
|
120
|
+
const closeMatch = remaining.match(/<\/head>/i);
|
|
121
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
122
|
+
headLine += remaining;
|
|
123
|
+
remaining = '';
|
|
124
|
+
inHead = true;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
headLine += remaining.slice(0, closeMatch.index);
|
|
128
|
+
remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const closeMatch = remaining.match(/<\/head>/i);
|
|
132
|
+
if (!closeMatch || closeMatch.index === undefined) {
|
|
133
|
+
headLine += remaining;
|
|
134
|
+
remaining = '';
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
headLine += remaining.slice(0, closeMatch.index);
|
|
138
|
+
remaining = remaining.slice(closeMatch.index + closeMatch[0].length);
|
|
139
|
+
inHead = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
bodyLines.push(bodyLine);
|
|
143
|
+
headLines.push(headLine);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
bodyTemplate: bodyLines.join('\n'),
|
|
147
|
+
headTemplate: headLines.join('\n'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function buildAppendStatement(expression, emitCall) {
|
|
151
|
+
return emitCall ? `${emitCall}(${expression});` : `__html += ${expression};`;
|
|
152
|
+
}
|
|
153
|
+
function extractPollFragmentExpr(tagText, rpcNameMap) {
|
|
154
|
+
const pollMatch = tagText.match(/\bdata-poll=\{([\s\S]*?)\}/);
|
|
155
|
+
if (!pollMatch)
|
|
156
|
+
return null;
|
|
157
|
+
const inner = pollMatch[1].trim();
|
|
158
|
+
const callMatch = inner.match(/^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/);
|
|
159
|
+
if (!callMatch)
|
|
160
|
+
return null;
|
|
161
|
+
const fnName = callMatch[1];
|
|
162
|
+
const rpcName = rpcNameMap?.get(fnName) || fnName;
|
|
163
|
+
const argsExpr = (callMatch[2] || '').trim();
|
|
164
|
+
if (!argsExpr)
|
|
165
|
+
return JSON.stringify(`__poll_${rpcName}`);
|
|
166
|
+
return `'__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_')`;
|
|
167
|
+
}
|
|
168
|
+
function findTagEnd(template, start) {
|
|
169
|
+
let quote = null;
|
|
170
|
+
let escaped = false;
|
|
171
|
+
let braceDepth = 0;
|
|
172
|
+
for (let i = start; i < template.length; i++) {
|
|
173
|
+
const ch = template[i];
|
|
174
|
+
if (quote) {
|
|
175
|
+
if (escaped) {
|
|
176
|
+
escaped = false;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (ch === '\\') {
|
|
180
|
+
escaped = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === quote)
|
|
184
|
+
quote = null;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
188
|
+
quote = ch;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === '{') {
|
|
192
|
+
braceDepth++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '}') {
|
|
196
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (ch === '>' && braceDepth === 0) {
|
|
200
|
+
return i;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return -1;
|
|
204
|
+
}
|
|
205
|
+
function instrumentPollFragments(template, rpcNameMap) {
|
|
206
|
+
const out = [];
|
|
207
|
+
const stack = [];
|
|
208
|
+
let cursor = 0;
|
|
209
|
+
while (cursor < template.length) {
|
|
210
|
+
const lt = template.indexOf('<', cursor);
|
|
211
|
+
if (lt === -1) {
|
|
212
|
+
out.push(template.slice(cursor));
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
out.push(template.slice(cursor, lt));
|
|
216
|
+
if (template.startsWith('<!--', lt)) {
|
|
217
|
+
const commentEnd = template.indexOf('-->', lt + 4);
|
|
218
|
+
if (commentEnd === -1) {
|
|
219
|
+
out.push(template.slice(lt));
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
out.push(template.slice(lt, commentEnd + 3));
|
|
223
|
+
cursor = commentEnd + 3;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const tagEnd = findTagEnd(template, lt + 1);
|
|
227
|
+
if (tagEnd === -1) {
|
|
228
|
+
out.push(template.slice(lt));
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const tagText = template.slice(lt, tagEnd + 1);
|
|
232
|
+
const closingMatch = tagText.match(/^<\s*\/\s*([A-Za-z][\w:-]*)\s*>$/);
|
|
233
|
+
if (closingMatch) {
|
|
234
|
+
const closingTag = closingMatch[1].toLowerCase();
|
|
235
|
+
const last = stack[stack.length - 1];
|
|
236
|
+
if (last && last.tagName === closingTag) {
|
|
237
|
+
if (last.fragmentExpr)
|
|
238
|
+
out.push(FRAGMENT_CLOSE_MARKER);
|
|
239
|
+
stack.pop();
|
|
240
|
+
}
|
|
241
|
+
out.push(tagText);
|
|
242
|
+
cursor = tagEnd + 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const openMatch = tagText.match(/^<\s*([A-Za-z][\w:-]*)\b/);
|
|
246
|
+
out.push(tagText);
|
|
247
|
+
if (!openMatch) {
|
|
248
|
+
cursor = tagEnd + 1;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const tagName = openMatch[1].toLowerCase();
|
|
252
|
+
const isVoidLike = /\/\s*>$/.test(tagText) || /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(tagName);
|
|
253
|
+
const fragmentExpr = extractPollFragmentExpr(tagText, rpcNameMap);
|
|
254
|
+
if (fragmentExpr) {
|
|
255
|
+
out.push(`${FRAGMENT_OPEN_MARKER}${encodeURIComponent(fragmentExpr)}-->`);
|
|
256
|
+
if (isVoidLike) {
|
|
257
|
+
out.push(FRAGMENT_CLOSE_MARKER);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!isVoidLike) {
|
|
261
|
+
stack.push({ tagName, fragmentExpr });
|
|
262
|
+
}
|
|
263
|
+
cursor = tagEnd + 1;
|
|
264
|
+
}
|
|
265
|
+
return out.join('');
|
|
266
|
+
}
|
|
40
267
|
/**
|
|
41
268
|
* Compile a template string into a JS render function body.
|
|
42
269
|
*
|
|
43
270
|
* The generated code expects `data` in scope (destructured load return)
|
|
44
271
|
* and an `__esc` helper for HTML-escaping.
|
|
45
272
|
*/
|
|
46
|
-
export function compileTemplate(template, componentNames, actionNames, rpcNameMap) {
|
|
273
|
+
export function compileTemplate(template, componentNames, actionNames, rpcNameMap, options = {}) {
|
|
274
|
+
const emitCall = options.emitCall;
|
|
275
|
+
if (options.enableFragmentManifest) {
|
|
276
|
+
template = instrumentPollFragments(template, rpcNameMap);
|
|
277
|
+
}
|
|
47
278
|
const out = ['let __html = "";'];
|
|
48
279
|
const lines = template.split('\n');
|
|
49
280
|
let inStyle = false;
|
|
@@ -58,7 +289,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
58
289
|
if (trimmed.match(/<style[\s>]/i))
|
|
59
290
|
inStyle = true;
|
|
60
291
|
if (inStyle) {
|
|
61
|
-
out.push(
|
|
292
|
+
out.push(buildAppendStatement(`\`${escapeLiteral(line)}\\n\``, emitCall));
|
|
62
293
|
if (trimmed.match(/<\/style>/i))
|
|
63
294
|
inStyle = false;
|
|
64
295
|
continue;
|
|
@@ -70,7 +301,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
70
301
|
if (trimmed.match(/<\/script>/i)) {
|
|
71
302
|
const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
|
|
72
303
|
for (const scriptLine of transformed.split('\n')) {
|
|
73
|
-
out.push(
|
|
304
|
+
out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
|
|
74
305
|
}
|
|
75
306
|
scriptBuffer = [];
|
|
76
307
|
inScript = false;
|
|
@@ -82,7 +313,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
82
313
|
if (trimmed.match(/<\/script>/i)) {
|
|
83
314
|
const transformed = transformClientScriptBlock(scriptBuffer.join('\n'));
|
|
84
315
|
for (const scriptLine of transformed.split('\n')) {
|
|
85
|
-
out.push(
|
|
316
|
+
out.push(buildAppendStatement(`\`${escapeLiteral(scriptLine)}\\n\``, emitCall));
|
|
86
317
|
}
|
|
87
318
|
scriptBuffer = [];
|
|
88
319
|
inScript = false;
|
|
@@ -92,7 +323,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
92
323
|
const startedInsideQuotedAttr = !!htmlAttrQuote;
|
|
93
324
|
const nextHtmlState = advanceHtmlTagState(line, inHtmlTag, htmlAttrQuote);
|
|
94
325
|
if (startedInsideQuotedAttr) {
|
|
95
|
-
out.push(
|
|
326
|
+
out.push(buildAppendStatement(`\`${escapeLiteral(line)}\n\``, emitCall));
|
|
96
327
|
inHtmlTag = nextHtmlState.inTag;
|
|
97
328
|
htmlAttrQuote = nextHtmlState.quote;
|
|
98
329
|
continue;
|
|
@@ -101,13 +332,13 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
101
332
|
htmlAttrQuote = nextHtmlState.quote;
|
|
102
333
|
// Skip empty lines
|
|
103
334
|
if (!trimmed) {
|
|
104
|
-
out.push(
|
|
335
|
+
out.push(buildAppendStatement(`"\\n"`, emitCall));
|
|
105
336
|
continue;
|
|
106
337
|
}
|
|
107
338
|
// One-line inline if/else with branch content:
|
|
108
339
|
// if (cond) { text/html } else { text/html }
|
|
109
340
|
// Compile branch content as template output instead of raw JS.
|
|
110
|
-
const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap);
|
|
341
|
+
const inlineIfElse = tryCompileInlineIfElseLine(trimmed, actionNames, rpcNameMap, options);
|
|
111
342
|
if (inlineIfElse) {
|
|
112
343
|
out.push(...inlineIfElse);
|
|
113
344
|
continue;
|
|
@@ -137,7 +368,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
137
368
|
}
|
|
138
369
|
}
|
|
139
370
|
// Self-closing: <Card attr="x" />
|
|
140
|
-
const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap);
|
|
371
|
+
const componentLine = tryCompileComponentTag(joinedTrimmed, componentNames, actionNames, rpcNameMap, options);
|
|
141
372
|
if (componentLine) {
|
|
142
373
|
i += joinedExtra;
|
|
143
374
|
out.push(componentLine);
|
|
@@ -167,10 +398,12 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
167
398
|
}
|
|
168
399
|
// Compile children into a sub-render block
|
|
169
400
|
const childTemplate = childLines.join('\n');
|
|
170
|
-
const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap
|
|
401
|
+
const childBody = compileTemplate(childTemplate, componentNames, actionNames, rpcNameMap, {
|
|
402
|
+
clientRouteRegistry: options.clientRouteRegistry,
|
|
403
|
+
});
|
|
171
404
|
// Wrap in an IIFE that returns the children HTML
|
|
172
405
|
const childrenExpr = `(function() { ${childBody}; return __html; })()`;
|
|
173
|
-
out.push(
|
|
406
|
+
out.push(buildAppendStatement(`${openResult.funcName}({ ${openResult.propsStr}${openResult.propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, emitCall));
|
|
174
407
|
continue;
|
|
175
408
|
}
|
|
176
409
|
}
|
|
@@ -187,7 +420,7 @@ export function compileTemplate(template, componentNames, actionNames, rpcNameMa
|
|
|
187
420
|
}
|
|
188
421
|
i += extraLines;
|
|
189
422
|
}
|
|
190
|
-
out.push(
|
|
423
|
+
out.push(...compileHtmlLineStatements(htmlLine, actionNames, rpcNameMap, options));
|
|
191
424
|
}
|
|
192
425
|
return out.join('\n');
|
|
193
426
|
}
|
|
@@ -441,14 +674,13 @@ function findMatching(src, openPos, openChar, closeChar) {
|
|
|
441
674
|
}
|
|
442
675
|
return -1;
|
|
443
676
|
}
|
|
444
|
-
function compileInlineBranchContent(content, actionNames, rpcNameMap) {
|
|
677
|
+
function compileInlineBranchContent(content, actionNames, rpcNameMap, options = {}) {
|
|
445
678
|
const c = content.trim();
|
|
446
679
|
if (!c)
|
|
447
680
|
return [];
|
|
448
|
-
|
|
449
|
-
return [compiled.replace(/\\n`;/, '`;')];
|
|
681
|
+
return compileHtmlLineStatements(c, actionNames, rpcNameMap, { ...options, appendNewline: false });
|
|
450
682
|
}
|
|
451
|
-
function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
|
|
683
|
+
function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap, options = {}) {
|
|
452
684
|
if (!line.startsWith('if'))
|
|
453
685
|
return null;
|
|
454
686
|
const ifMatch = line.match(/^if\s*\(/);
|
|
@@ -489,9 +721,9 @@ function tryCompileInlineIfElseLine(line, actionNames, rpcNameMap) {
|
|
|
489
721
|
const elseContent = line.slice(elseOpen + 1, elseClose);
|
|
490
722
|
const out = [];
|
|
491
723
|
out.push(`if (${condition}) {`);
|
|
492
|
-
out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap));
|
|
724
|
+
out.push(...compileInlineBranchContent(thenContent, actionNames, rpcNameMap, options));
|
|
493
725
|
out.push(`} else {`);
|
|
494
|
-
out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap));
|
|
726
|
+
out.push(...compileInlineBranchContent(elseContent, actionNames, rpcNameMap, options));
|
|
495
727
|
out.push(`}`);
|
|
496
728
|
return out;
|
|
497
729
|
}
|
|
@@ -575,7 +807,7 @@ function parseComponentAttrs(attrsStr, actionNames) {
|
|
|
575
807
|
* Matches: <StatCard attr="literal" attr={expr} />
|
|
576
808
|
* Generates: __html += __c_stat_card({ attr: "literal", attr: (expr) }, __esc);
|
|
577
809
|
*/
|
|
578
|
-
function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
|
|
810
|
+
function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap, options = {}) {
|
|
579
811
|
// Match self-closing tags: <TagName ...attrs... />
|
|
580
812
|
const selfCloseMatch = line.match(/^<([A-Z]\w*)\s*(.*?)\s*\/>\s*$/);
|
|
581
813
|
// Match open+close on same line with no content: <TagName ...attrs...></TagName>
|
|
@@ -590,7 +822,7 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
|
|
|
590
822
|
return null;
|
|
591
823
|
const funcName = componentFuncName(fileName);
|
|
592
824
|
const propsStr = parseComponentAttrs(match[2].trim(), actionNames);
|
|
593
|
-
return
|
|
825
|
+
return buildAppendStatement(`${funcName}({ ${propsStr} }, __esc)`, options.emitCall);
|
|
594
826
|
}
|
|
595
827
|
if (inlinePairMatch) {
|
|
596
828
|
const tagName = inlinePairMatch[1];
|
|
@@ -601,9 +833,11 @@ function tryCompileComponentTag(line, componentNames, actionNames, rpcNameMap) {
|
|
|
601
833
|
const propsStr = parseComponentAttrs(inlinePairMatch[2].trim(), actionNames);
|
|
602
834
|
const innerContent = inlinePairMatch[3];
|
|
603
835
|
// Compile the inline content as a mini-template to handle {expr} interpolation
|
|
604
|
-
const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap
|
|
836
|
+
const childBody = compileTemplate(innerContent, componentNames, actionNames, rpcNameMap, {
|
|
837
|
+
clientRouteRegistry: options.clientRouteRegistry,
|
|
838
|
+
});
|
|
605
839
|
const childrenExpr = `(function() { ${childBody}; return __html; })()`;
|
|
606
|
-
return
|
|
840
|
+
return buildAppendStatement(`${funcName}({ ${propsStr}${propsStr ? ', ' : ''}children: ${childrenExpr} }, __esc)`, options.emitCall);
|
|
607
841
|
}
|
|
608
842
|
return null;
|
|
609
843
|
}
|
|
@@ -642,17 +876,47 @@ function expandShorthands(line) {
|
|
|
642
876
|
});
|
|
643
877
|
return line;
|
|
644
878
|
}
|
|
879
|
+
function compileHtmlLineStatements(line, actionNames, rpcNameMap, options = {}) {
|
|
880
|
+
const markerRegex = /<!--__KURATCHI_FRAGMENT_(OPEN:([\s\S]*?)|CLOSE)-->/g;
|
|
881
|
+
const statements = [];
|
|
882
|
+
let cursor = 0;
|
|
883
|
+
let sawLiteral = false;
|
|
884
|
+
let match;
|
|
885
|
+
while ((match = markerRegex.exec(line)) !== null) {
|
|
886
|
+
const literal = line.slice(cursor, match.index);
|
|
887
|
+
if (literal) {
|
|
888
|
+
statements.push(compileHtmlSegment(literal, actionNames, rpcNameMap, { ...options, appendNewline: false }));
|
|
889
|
+
sawLiteral = true;
|
|
890
|
+
}
|
|
891
|
+
if (match[1].startsWith('OPEN:')) {
|
|
892
|
+
const encodedExpr = match[2] || '';
|
|
893
|
+
statements.push(`__pushFragment(${decodeURIComponent(encodedExpr)});`);
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
statements.push(`__popFragment();`);
|
|
897
|
+
}
|
|
898
|
+
cursor = match.index + match[0].length;
|
|
899
|
+
}
|
|
900
|
+
const tail = line.slice(cursor);
|
|
901
|
+
if (tail || sawLiteral || options.appendNewline !== false) {
|
|
902
|
+
const appendNewline = options.appendNewline !== false;
|
|
903
|
+
if (tail || appendNewline) {
|
|
904
|
+
statements.push(compileHtmlSegment(tail, actionNames, rpcNameMap, { ...options, appendNewline }));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return statements.filter(Boolean);
|
|
908
|
+
}
|
|
645
909
|
/**
|
|
646
|
-
* Compile a single HTML
|
|
910
|
+
* Compile a single HTML segment, replacing {expr} with escaped output,
|
|
647
911
|
* {@html expr} with sanitized HTML, and {@raw expr} with raw output.
|
|
648
912
|
* Handles attribute values like value={x}.
|
|
649
913
|
*/
|
|
650
|
-
function
|
|
914
|
+
function compileHtmlSegment(line, actionNames, rpcNameMap, options = {}) {
|
|
651
915
|
// Expand shorthand syntax before main compilation
|
|
652
916
|
line = expandShorthands(line);
|
|
653
917
|
let result = '';
|
|
654
918
|
let pos = 0;
|
|
655
|
-
let
|
|
919
|
+
let pendingActionHiddenInput = null;
|
|
656
920
|
while (pos < line.length) {
|
|
657
921
|
const braceIdx = findNextTemplateBrace(line, pos);
|
|
658
922
|
if (braceIdx === -1) {
|
|
@@ -671,19 +935,16 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
671
935
|
if (inner.startsWith('@html ')) {
|
|
672
936
|
const expr = inner.slice(6).trim();
|
|
673
937
|
result += `\${__sanitizeHtml(${expr})}`;
|
|
674
|
-
hasExpr = true;
|
|
675
938
|
}
|
|
676
939
|
else if (inner.startsWith('@raw ')) {
|
|
677
940
|
// Unsafe raw HTML: {@raw expr}
|
|
678
941
|
const expr = inner.slice(5).trim();
|
|
679
942
|
result += `\${__rawHtml(${expr})}`;
|
|
680
|
-
hasExpr = true;
|
|
681
943
|
}
|
|
682
944
|
else if (inner.startsWith('=html ')) {
|
|
683
945
|
// Legacy alias for raw HTML: {=html expr}
|
|
684
946
|
const expr = inner.slice(6).trim();
|
|
685
947
|
result += `\${__rawHtml(${expr})}`;
|
|
686
|
-
hasExpr = true;
|
|
687
948
|
}
|
|
688
949
|
else {
|
|
689
950
|
// Check if this is an attribute value: attr={expr}
|
|
@@ -697,7 +958,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
697
958
|
if (attrName === 'data-dialog-data') {
|
|
698
959
|
// data-dialog-data={expr} → data-dialog-data="JSON.stringify(expr)" (HTML-escaped)
|
|
699
960
|
result += `"\${__esc(JSON.stringify(${inner}))}"`;
|
|
700
|
-
hasExpr = true;
|
|
701
961
|
pos = closeIdx + 1;
|
|
702
962
|
continue;
|
|
703
963
|
}
|
|
@@ -719,7 +979,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
719
979
|
else {
|
|
720
980
|
result += ` data-refresh="\${__esc(${inner})}"`;
|
|
721
981
|
}
|
|
722
|
-
hasExpr = true;
|
|
723
982
|
pos = closeIdx + 1;
|
|
724
983
|
continue;
|
|
725
984
|
}
|
|
@@ -737,7 +996,6 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
737
996
|
else {
|
|
738
997
|
result += ` ${attrName}="${escapeLiteral(inner)}"`;
|
|
739
998
|
}
|
|
740
|
-
hasExpr = true;
|
|
741
999
|
pos = closeIdx + 1;
|
|
742
1000
|
continue;
|
|
743
1001
|
}
|
|
@@ -759,12 +1017,11 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
759
1017
|
// Non-call expression mode (e.g., data-get={someUrl})
|
|
760
1018
|
result += ` ${attrName}="\${__esc(${inner})}"`;
|
|
761
1019
|
}
|
|
762
|
-
hasExpr = true;
|
|
763
1020
|
pos = closeIdx + 1;
|
|
764
1021
|
continue;
|
|
765
1022
|
}
|
|
766
1023
|
else if (attrName === 'data-poll') {
|
|
767
|
-
// data-poll={fn(args)}
|
|
1024
|
+
// data-poll={fn(args)} → data-poll="fnName" data-poll-args="[serialized]" data-poll-id="stable-id"
|
|
768
1025
|
const pollCallMatch = inner.match(/^(\w+)\((.*)\)$/);
|
|
769
1026
|
if (pollCallMatch) {
|
|
770
1027
|
const fnName = pollCallMatch[1];
|
|
@@ -772,13 +1029,18 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
772
1029
|
const argsExpr = pollCallMatch[2].trim();
|
|
773
1030
|
// Remove the trailing "data-poll=" we already appended
|
|
774
1031
|
result = result.replace(/\s*data-poll=$/, '');
|
|
775
|
-
// Emit data-poll and data-poll-args
|
|
1032
|
+
// Emit data-poll, data-poll-args, and stable data-poll-id (based on fn + args expression)
|
|
776
1033
|
result += ` data-poll="${rpcName}"`;
|
|
777
1034
|
if (argsExpr) {
|
|
778
1035
|
result += ` data-poll-args="\${__esc(JSON.stringify([${argsExpr}]))}"`;
|
|
1036
|
+
// Stable ID based on args so same data produces same ID across renders
|
|
1037
|
+
result += ` data-poll-id="\${__esc('__poll_' + String(${argsExpr}).replace(/[^a-zA-Z0-9]/g, '_'))}"`;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
// No args - use function name as ID
|
|
1041
|
+
result += ` data-poll-id="__poll_${rpcName}"`;
|
|
779
1042
|
}
|
|
780
1043
|
}
|
|
781
|
-
hasExpr = true;
|
|
782
1044
|
pos = closeIdx + 1;
|
|
783
1045
|
continue;
|
|
784
1046
|
}
|
|
@@ -794,23 +1056,14 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
794
1056
|
}
|
|
795
1057
|
// Remove trailing `action=` from output and inject _action hidden field.
|
|
796
1058
|
result = result.replace(/\s*action=$/, '');
|
|
1059
|
+
const actionValue = actionNames === undefined
|
|
1060
|
+
? `\${__esc(${inner})}`
|
|
1061
|
+
: inner;
|
|
1062
|
+
pendingActionHiddenInput = `\\n<input type="hidden" name="_action" value="${actionValue}">`;
|
|
797
1063
|
pos = closeIdx + 1;
|
|
798
|
-
const tagEnd = line.indexOf('>', pos);
|
|
799
|
-
if (tagEnd !== -1) {
|
|
800
|
-
result += escapeLiteral(line.slice(pos, tagEnd + 1));
|
|
801
|
-
// In a route context, inner is the literal action function name (a string key).
|
|
802
|
-
// In a component context (actionNames === undefined), inner is a prop name whose
|
|
803
|
-
// value at runtime is the action name string — emit it as a dynamic expression.
|
|
804
|
-
const actionValue = actionNames === undefined
|
|
805
|
-
? `\${__esc(${inner})}`
|
|
806
|
-
: inner;
|
|
807
|
-
result += `\\n<input type="hidden" name="_action" value="${actionValue}">`;
|
|
808
|
-
pos = tagEnd + 1;
|
|
809
|
-
}
|
|
810
|
-
hasExpr = true;
|
|
811
1064
|
continue;
|
|
812
1065
|
}
|
|
813
|
-
else if (/^on[A-Za-z]
|
|
1066
|
+
else if (/^on[A-Za-z]+$/i.test(attrName)) {
|
|
814
1067
|
// onClick/onChange/...={expr} — check if it's a server action or plain client JS
|
|
815
1068
|
const eventName = attrName.slice(2).toLowerCase();
|
|
816
1069
|
const actionCallMatch = inner.match(/^(\w+)\((.*)\)$/);
|
|
@@ -825,12 +1078,25 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
825
1078
|
result += ` data-action="${fnName}" data-args="\${__esc(JSON.stringify([${argsExpr}]))}" data-action-event="${eventName}"`;
|
|
826
1079
|
}
|
|
827
1080
|
else {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1081
|
+
const clientRegistration = options.clientRouteRegistry?.registerEventHandler(eventName, inner) ?? null;
|
|
1082
|
+
if (clientRegistration) {
|
|
1083
|
+
result = result.replace(new RegExp(`\\s*${attrName}=$`), '');
|
|
1084
|
+
result += ` data-client-route="${clientRegistration.routeId}" data-client-handler="${clientRegistration.handlerId}" data-client-event="${eventName}"`;
|
|
1085
|
+
if (clientRegistration.argsExpr) {
|
|
1086
|
+
result += ` data-client-args="\${__esc(JSON.stringify([${clientRegistration.argsExpr}]))}"`;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
else if (options.clientRouteRegistry?.hasBindingReference(inner)) {
|
|
1090
|
+
throw new Error(`Unsupported client handler expression: "${inner}". ` +
|
|
1091
|
+
`Top-level $client/$shared event handlers must be a direct function reference or call, like onClick={openDialog()} or onClick={helpers.openDialog()}.`);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
// Plain client-side event handler: onClick={myFn()}
|
|
1095
|
+
// Emit as native inline handler (lowercased event attribute).
|
|
1096
|
+
result = result.replace(new RegExp(`\\s*${attrName}=$`), ` on${eventName}=`);
|
|
1097
|
+
result += `"${escapeLiteral(inner)}"`;
|
|
1098
|
+
}
|
|
832
1099
|
}
|
|
833
|
-
hasExpr = true;
|
|
834
1100
|
pos = closeIdx + 1;
|
|
835
1101
|
continue;
|
|
836
1102
|
}
|
|
@@ -846,17 +1112,14 @@ function compileHtmlLine(line, actionNames, rpcNameMap) {
|
|
|
846
1112
|
else {
|
|
847
1113
|
result += `\${__esc(${inner})}`;
|
|
848
1114
|
}
|
|
849
|
-
hasExpr = true;
|
|
850
1115
|
}
|
|
851
1116
|
pos = closeIdx + 1;
|
|
852
1117
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
else {
|
|
858
|
-
return `__html += \`${result}\\n\`;`;
|
|
1118
|
+
if (pendingActionHiddenInput && result.includes('>')) {
|
|
1119
|
+
const gtIndex = result.indexOf('>');
|
|
1120
|
+
result = result.slice(0, gtIndex + 1) + pendingActionHiddenInput + result.slice(gtIndex + 1);
|
|
859
1121
|
}
|
|
1122
|
+
return buildAppendStatement(`\`${result}${options.appendNewline === false ? '' : '\\n'}\``, options.emitCall);
|
|
860
1123
|
}
|
|
861
1124
|
/** Escape characters that would break a JS template literal. */
|
|
862
1125
|
function escapeLiteral(text) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface WorkerClassExportEntry {
|
|
2
|
+
className: string;
|
|
3
|
+
exportKind: 'named' | 'default';
|
|
4
|
+
file: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveRuntimeImportPath(projectDir: string): string | null;
|
|
7
|
+
export declare function toWorkerImportPath(projectDir: string, outDir: string, filePath: string): string;
|
|
8
|
+
export declare function buildWorkerEntrypointSource(opts: {
|
|
9
|
+
projectDir: string;
|
|
10
|
+
outDir: string;
|
|
11
|
+
doClassNames: string[];
|
|
12
|
+
workerClassEntries: WorkerClassExportEntry[];
|
|
13
|
+
}): string;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function resolveRuntimeImportPath(projectDir) {
|
|
4
|
+
const candidates = [
|
|
5
|
+
{ file: 'src/server/runtime.hook.ts', importPath: '../src/server/runtime.hook' },
|
|
6
|
+
];
|
|
7
|
+
for (const candidate of candidates) {
|
|
8
|
+
if (fs.existsSync(path.join(projectDir, candidate.file))) {
|
|
9
|
+
return candidate.importPath;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
export function toWorkerImportPath(projectDir, outDir, filePath) {
|
|
15
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath);
|
|
16
|
+
let rel = path.relative(outDir, absPath).replace(/\\/g, '/');
|
|
17
|
+
if (!rel.startsWith('.'))
|
|
18
|
+
rel = `./${rel}`;
|
|
19
|
+
return rel.replace(/\.(ts|js|mjs|cjs)$/, '');
|
|
20
|
+
}
|
|
21
|
+
export function buildWorkerEntrypointSource(opts) {
|
|
22
|
+
const workerClassExports = opts.workerClassEntries
|
|
23
|
+
.map((entry) => {
|
|
24
|
+
const importPath = toWorkerImportPath(opts.projectDir, opts.outDir, entry.file);
|
|
25
|
+
if (entry.exportKind === 'default') {
|
|
26
|
+
return `export { default as ${entry.className} } from '${importPath}';`;
|
|
27
|
+
}
|
|
28
|
+
return `export { ${entry.className} } from '${importPath}';`;
|
|
29
|
+
});
|
|
30
|
+
return [
|
|
31
|
+
'// Auto-generated by kuratchi — do not edit.',
|
|
32
|
+
"export { default } from './routes.js';",
|
|
33
|
+
...opts.doClassNames.map((className) => `export { ${className} } from './routes.js';`),
|
|
34
|
+
...workerClassExports,
|
|
35
|
+
'',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface WranglerSyncEntry {
|
|
2
|
+
binding: string;
|
|
3
|
+
className: string;
|
|
4
|
+
}
|
|
5
|
+
export interface WranglerSyncConfig {
|
|
6
|
+
workflows: WranglerSyncEntry[];
|
|
7
|
+
containers: WranglerSyncEntry[];
|
|
8
|
+
durableObjects: WranglerSyncEntry[];
|
|
9
|
+
}
|
|
10
|
+
export declare function syncWranglerConfig(opts: {
|
|
11
|
+
projectDir: string;
|
|
12
|
+
config: WranglerSyncConfig;
|
|
13
|
+
writeFile: (filePath: string, content: string) => void;
|
|
14
|
+
}): void;
|