@kuratchi/js 0.0.17 → 0.0.19
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 +4 -4
- package/dist/compiler/import-linking.js +1 -1
- package/dist/compiler/index.js +4 -2
- package/dist/compiler/root-layout-pipeline.js +11 -1
- package/dist/compiler/route-discovery.js +5 -5
- package/dist/compiler/route-pipeline.js +9 -1
- package/dist/compiler/routes-module-feature-blocks.js +6 -0
- package/dist/compiler/worker-output-pipeline.d.ts +1 -0
- package/dist/compiler/worker-output-pipeline.js +8 -0
- package/dist/create.js +7 -7
- package/dist/runtime/security.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,9 +42,9 @@ For the framework's internal compiler/runtime orchestration and tracked implemen
|
|
|
42
42
|
Place `.html` files inside `src/routes/`. The file path becomes the URL pattern.
|
|
43
43
|
|
|
44
44
|
```
|
|
45
|
-
src/routes/
|
|
46
|
-
src/routes/items/
|
|
47
|
-
src/routes/blog/[slug]/
|
|
45
|
+
src/routes/index.html → /
|
|
46
|
+
src/routes/items/index.html → /items
|
|
47
|
+
src/routes/blog/[slug]/index.html → /blog/:slug
|
|
48
48
|
src/routes/layout.html → shared layout wrapping all routes
|
|
49
49
|
```
|
|
50
50
|
|
|
@@ -277,7 +277,7 @@ Throw `PageError` from a route's load scope to return the correct HTTP error pag
|
|
|
277
277
|
```ts
|
|
278
278
|
import { PageError } from '@kuratchi/js';
|
|
279
279
|
|
|
280
|
-
// In src/routes/posts/[id]/
|
|
280
|
+
// In src/routes/posts/[id]/index.html <script> block:
|
|
281
281
|
const post = await db.posts.findOne({ id: params.id });
|
|
282
282
|
if (!post) throw new PageError(404);
|
|
283
283
|
if (!post.isPublished && !currentUser?.isAdmin) throw new PageError(403);
|
|
@@ -120,7 +120,7 @@ export function linkRouteServerImports(opts) {
|
|
|
120
120
|
continue;
|
|
121
121
|
}
|
|
122
122
|
fnToModule[binding.local] = moduleId;
|
|
123
|
-
if (!routeImportDeclMap.has(binding.local)
|
|
123
|
+
if (!routeImportDeclMap.has(binding.local)) {
|
|
124
124
|
const accessExpr = binding.imported === 'default' ? `${moduleId}.default` : `${moduleId}.${binding.imported}`;
|
|
125
125
|
routeImportDeclMap.set(binding.local, `const ${binding.local} = ${accessExpr};`);
|
|
126
126
|
}
|
package/dist/compiler/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { prepareRootLayoutSource } from './root-layout-pipeline.js';
|
|
|
18
18
|
import { generateRoutesModule as generateRoutesModulePipeline } from './routes-module-pipeline.js';
|
|
19
19
|
import { assembleRouteState } from './route-state-pipeline.js';
|
|
20
20
|
import { createServerModuleCompiler } from './server-module-pipeline.js';
|
|
21
|
-
import { buildWorkerEntrypointSource, resolveRuntimeImportPath as resolveRuntimeImportPathPipeline, } from './worker-output-pipeline.js';
|
|
21
|
+
import { buildCompatEntrypointSource, buildWorkerEntrypointSource, resolveRuntimeImportPath as resolveRuntimeImportPathPipeline, } from './worker-output-pipeline.js';
|
|
22
22
|
import { syncWranglerConfig as syncWranglerConfigPipeline } from './wrangler-sync.js';
|
|
23
23
|
import { filePathToPattern } from '../runtime/router.js';
|
|
24
24
|
import * as fs from 'node:fs';
|
|
@@ -220,7 +220,7 @@ export async function compile(options) {
|
|
|
220
220
|
}));
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
|
-
// -- Page route (
|
|
223
|
+
// -- Page route (index.html) --
|
|
224
224
|
const source = fileContents.get(fullPath) ?? fs.readFileSync(fullPath, 'utf-8');
|
|
225
225
|
const parsed = parseFile(source, { kind: 'route', filePath: fullPath });
|
|
226
226
|
const routeState = assembleRouteState({
|
|
@@ -294,6 +294,7 @@ export async function compile(options) {
|
|
|
294
294
|
fs.mkdirSync(outDir, { recursive: true });
|
|
295
295
|
}
|
|
296
296
|
writeIfChanged(outFile, output);
|
|
297
|
+
writeIfChanged(path.join(outDir, 'routes.js'), buildCompatEntrypointSource('./routes.ts'));
|
|
297
298
|
// Generate .kuratchi/worker.ts — the stable wrangler entry point.
|
|
298
299
|
// routes.ts already exports the default fetch handler and all named DO classes;
|
|
299
300
|
// worker.ts explicitly re-exports them so wrangler.jsonc can reference a
|
|
@@ -305,6 +306,7 @@ export async function compile(options) {
|
|
|
305
306
|
doClassNames: doConfig.map((entry) => entry.className),
|
|
306
307
|
workerClassEntries: [...agentConfig, ...containerConfig, ...workflowConfig],
|
|
307
308
|
}));
|
|
309
|
+
writeIfChanged(path.join(outDir, 'worker.js'), buildCompatEntrypointSource('./worker.ts'));
|
|
308
310
|
// Auto-sync wrangler.jsonc with workflow/container/DO config from kuratchi.config.ts
|
|
309
311
|
syncWranglerConfigPipeline({
|
|
310
312
|
projectDir,
|
|
@@ -354,8 +354,18 @@ const BRIDGE_SOURCE = `(function(){
|
|
|
354
354
|
(function tick(){
|
|
355
355
|
setTimeout(function(){
|
|
356
356
|
fetch(location.pathname + location.search, { headers: { 'x-kuratchi-fragment': pollId } })
|
|
357
|
-
.then(function(r){
|
|
357
|
+
.then(function(r){
|
|
358
|
+
if(r.status === 404){
|
|
359
|
+
// Fragment no longer exists (e.g., item completed and removed from list)
|
|
360
|
+
// Remove the element and stop polling
|
|
361
|
+
el.remove();
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
if(!r.ok) return null;
|
|
365
|
+
return r.text();
|
|
366
|
+
})
|
|
358
367
|
.then(function(html){
|
|
368
|
+
if(html === null) return;
|
|
359
369
|
if(prevHtml !== html){
|
|
360
370
|
el.innerHTML = html;
|
|
361
371
|
prevHtml = html;
|
|
@@ -26,9 +26,9 @@ export function discoverRoutes(routesDir) {
|
|
|
26
26
|
for (const entry of entries) {
|
|
27
27
|
if (entry.isDirectory()) {
|
|
28
28
|
const childPrefix = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
29
|
-
const pageFile = path.join(dir, entry.name, '
|
|
29
|
+
const pageFile = path.join(dir, entry.name, 'index.html');
|
|
30
30
|
if (fs.existsSync(pageFile)) {
|
|
31
|
-
const routeFile = `${childPrefix}/
|
|
31
|
+
const routeFile = `${childPrefix}/index.html`;
|
|
32
32
|
if (!registered.has(routeFile)) {
|
|
33
33
|
registered.add(routeFile);
|
|
34
34
|
results.push({ file: routeFile, name: childPrefix, layouts: getLayoutsForPrefix(childPrefix), type: 'page' });
|
|
@@ -56,15 +56,15 @@ export function discoverRoutes(routesDir) {
|
|
|
56
56
|
}
|
|
57
57
|
continue;
|
|
58
58
|
}
|
|
59
|
-
if (entry.name === '
|
|
60
|
-
const routeFile = prefix ? `${prefix}/
|
|
59
|
+
if (entry.name === 'index.html') {
|
|
60
|
+
const routeFile = prefix ? `${prefix}/index.html` : 'index.html';
|
|
61
61
|
if (!registered.has(routeFile)) {
|
|
62
62
|
registered.add(routeFile);
|
|
63
63
|
results.push({ file: routeFile, name: prefix || 'index', layouts: getLayoutsForPrefix(prefix), type: 'page' });
|
|
64
64
|
}
|
|
65
65
|
continue;
|
|
66
66
|
}
|
|
67
|
-
if (entry.name.endsWith('.html') && entry.name !== '
|
|
67
|
+
if (entry.name.endsWith('.html') && entry.name !== 'index.html') {
|
|
68
68
|
const name = prefix
|
|
69
69
|
? `${prefix}/${entry.name.replace('.html', '')}`
|
|
70
70
|
: entry.name.replace('.html', '');
|
|
@@ -94,10 +94,18 @@ export function analyzeRouteBuild(opts) {
|
|
|
94
94
|
const routeImportDecls = routeImportDeclLines.join('\n');
|
|
95
95
|
const importedBindingNames = new Set(Object.keys(fnToModule));
|
|
96
96
|
const renderScopeActionNames = new Set(parsed.actionFunctions);
|
|
97
|
+
// Filter out params/breadcrumbs from render prelude since they come from data destructuring
|
|
98
|
+
const reservedRenderVars = new Set(['params', 'breadcrumbs']);
|
|
97
99
|
const renderImportPrelude = routeImportDeclLines
|
|
98
100
|
.filter((statement) => {
|
|
99
101
|
const declaredName = extractDeclaredConstName(statement);
|
|
100
|
-
|
|
102
|
+
if (!declaredName)
|
|
103
|
+
return true;
|
|
104
|
+
if (renderScopeActionNames.has(declaredName))
|
|
105
|
+
return false;
|
|
106
|
+
if (reservedRenderVars.has(declaredName))
|
|
107
|
+
return false;
|
|
108
|
+
return true;
|
|
101
109
|
})
|
|
102
110
|
.join('\n');
|
|
103
111
|
let scriptBody = '';
|
|
@@ -311,10 +311,12 @@ function buildWorkflowStatusRpc(opts) {
|
|
|
311
311
|
const rpcLines = [];
|
|
312
312
|
rpcLines.push(`\n// Workflow Status RPCs (auto-generated)`);
|
|
313
313
|
rpcLines.push(`const __workflowStatusRpc = {`);
|
|
314
|
+
const rpcNames = [];
|
|
314
315
|
for (const workflow of opts.workflowConfig) {
|
|
315
316
|
const baseName = workflow.file.split('/').pop()?.replace(/\.workflow\.ts$/, '') || '';
|
|
316
317
|
const camelName = baseName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
317
318
|
const rpcName = `${camelName}WorkflowStatus`;
|
|
319
|
+
rpcNames.push(rpcName);
|
|
318
320
|
rpcLines.push(` '${rpcName}': async (instanceId) => {`);
|
|
319
321
|
rpcLines.push(` if (!instanceId) return { status: 'unknown', error: { name: 'Error', message: 'Missing instanceId' } };`);
|
|
320
322
|
rpcLines.push(` try {`);
|
|
@@ -326,5 +328,9 @@ function buildWorkflowStatusRpc(opts) {
|
|
|
326
328
|
rpcLines.push(` },`);
|
|
327
329
|
}
|
|
328
330
|
rpcLines.push(`};`);
|
|
331
|
+
// Export individual functions for use in route templates
|
|
332
|
+
for (const rpcName of rpcNames) {
|
|
333
|
+
rpcLines.push(`const ${rpcName} = __workflowStatusRpc['${rpcName}'];`);
|
|
334
|
+
}
|
|
329
335
|
return rpcLines.join('\n');
|
|
330
336
|
}
|
|
@@ -35,3 +35,11 @@ export function buildWorkerEntrypointSource(opts) {
|
|
|
35
35
|
'',
|
|
36
36
|
].join('\n');
|
|
37
37
|
}
|
|
38
|
+
export function buildCompatEntrypointSource(targetFile) {
|
|
39
|
+
return [
|
|
40
|
+
'// Auto-generated by kuratchi — do not edit.',
|
|
41
|
+
`export { default } from '${targetFile}';`,
|
|
42
|
+
`export * from '${targetFile}';`,
|
|
43
|
+
'',
|
|
44
|
+
].join('\n');
|
|
45
|
+
}
|
package/dist/create.js
CHANGED
|
@@ -211,25 +211,25 @@ function scaffold(dir, opts) {
|
|
|
211
211
|
write(dir, 'tsconfig.json', genTsConfig());
|
|
212
212
|
write(dir, '.gitignore', genGitIgnore());
|
|
213
213
|
write(dir, 'src/routes/layout.html', genLayout(opts));
|
|
214
|
-
write(dir, 'src/routes/
|
|
214
|
+
write(dir, 'src/routes/index.html', genLandingPage(opts));
|
|
215
215
|
if (orm) {
|
|
216
216
|
write(dir, 'src/schemas/app.ts', genSchema(opts));
|
|
217
217
|
write(dir, 'src/database/items.ts', genItemsCrud());
|
|
218
|
-
write(dir, 'src/routes/items/
|
|
218
|
+
write(dir, 'src/routes/items/index.html', genItemsPage());
|
|
219
219
|
}
|
|
220
220
|
if (enableDO) {
|
|
221
221
|
write(dir, 'src/schemas/notes.ts', genNotesSchema());
|
|
222
222
|
write(dir, 'src/server/notes.do.ts', genNotesDoHandler());
|
|
223
223
|
write(dir, 'src/database/notes.ts', genNotesDb());
|
|
224
|
-
write(dir, 'src/routes/notes/
|
|
224
|
+
write(dir, 'src/routes/notes/index.html', genNotesPage());
|
|
225
225
|
}
|
|
226
226
|
if (auth) {
|
|
227
227
|
write(dir, '.dev.vars', genDevVars());
|
|
228
228
|
write(dir, 'src/database/auth.ts', genAuthFunctions());
|
|
229
229
|
write(dir, 'src/database/admin.ts', genAdminLoader());
|
|
230
|
-
write(dir, 'src/routes/auth/login/
|
|
231
|
-
write(dir, 'src/routes/auth/signup/
|
|
232
|
-
write(dir, 'src/routes/admin/
|
|
230
|
+
write(dir, 'src/routes/auth/login/index.html', genLoginPage());
|
|
231
|
+
write(dir, 'src/routes/auth/signup/index.html', genSignupPage());
|
|
232
|
+
write(dir, 'src/routes/admin/index.html', genAdminPage());
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
function write(dir, filePath, content) {
|
|
@@ -278,7 +278,7 @@ function genPackageJson(opts) {
|
|
|
278
278
|
function genWrangler(opts) {
|
|
279
279
|
const config = {
|
|
280
280
|
name: opts.name,
|
|
281
|
-
main: '.kuratchi/worker.
|
|
281
|
+
main: '.kuratchi/worker.ts',
|
|
282
282
|
compatibility_date: new Date().toISOString().split('T')[0],
|
|
283
283
|
compatibility_flags: ['nodejs_compat'],
|
|
284
284
|
};
|
package/dist/runtime/security.js
CHANGED
|
@@ -32,6 +32,7 @@ export function initCsrf(request, cookieName = CSRF_COOKIE_NAME) {
|
|
|
32
32
|
}
|
|
33
33
|
__setLocal('__csrfToken', token);
|
|
34
34
|
__setLocal('__csrfCookieName', cookieName);
|
|
35
|
+
__setLocal('__csrfCookieSecure', shouldUseSecureCookie(request));
|
|
35
36
|
return token;
|
|
36
37
|
}
|
|
37
38
|
/**
|
|
@@ -94,9 +95,10 @@ export function getCsrfCookieHeader() {
|
|
|
94
95
|
}
|
|
95
96
|
const token = locals.__csrfToken;
|
|
96
97
|
const cookieName = locals.__csrfCookieName || CSRF_COOKIE_NAME;
|
|
98
|
+
const secure = locals.__csrfCookieSecure ? '; Secure' : '';
|
|
97
99
|
// SameSite=Lax allows the cookie to be sent on top-level navigations
|
|
98
100
|
// HttpOnly=false so client JS can read it for fetch requests
|
|
99
|
-
return `${cookieName}=${token}; Path=/; SameSite=Lax
|
|
101
|
+
return `${cookieName}=${token}; Path=/; SameSite=Lax${secure}`;
|
|
100
102
|
}
|
|
101
103
|
// ── RPC Security ───────────────────────────────────────────────────
|
|
102
104
|
const RPC_NONCE_LENGTH = 16;
|
|
@@ -191,6 +193,18 @@ function parseCookies(header) {
|
|
|
191
193
|
}
|
|
192
194
|
return map;
|
|
193
195
|
}
|
|
196
|
+
function shouldUseSecureCookie(request) {
|
|
197
|
+
const forwardedProto = request.headers.get('x-forwarded-proto');
|
|
198
|
+
if (forwardedProto) {
|
|
199
|
+
return forwardedProto.split(',')[0].trim().toLowerCase() === 'https';
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return new URL(request.url).protocol === 'https:';
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
194
208
|
function isSameOrigin(request, url) {
|
|
195
209
|
const fetchSite = request.headers.get('sec-fetch-site');
|
|
196
210
|
if (fetchSite && fetchSite !== 'same-origin' && fetchSite !== 'same-site' && fetchSite !== 'none') {
|