@kuratchi/js 0.0.15 → 0.0.17
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 +160 -1
- package/dist/cli.js +78 -45
- 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 +73 -0
- package/dist/compiler/compiler-shared.js +4 -0
- package/dist/compiler/component-pipeline.d.ts +15 -0
- package/dist/compiler/component-pipeline.js +158 -0
- package/dist/compiler/config-reading.d.ts +12 -0
- package/dist/compiler/config-reading.js +380 -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 +140 -0
- package/dist/compiler/index.d.ts +7 -7
- package/dist/compiler/index.js +181 -3321
- 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 +436 -55
- package/dist/compiler/root-layout-pipeline.d.ts +10 -0
- package/dist/compiler/root-layout-pipeline.js +532 -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 +291 -0
- package/dist/compiler/route-state-pipeline.d.ts +26 -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 +91 -0
- package/dist/compiler/routes-module-types.d.ts +45 -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 +337 -71
- 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/context.d.ts +4 -0
- package/dist/runtime/context.js +40 -2
- package/dist/runtime/do.js +21 -6
- package/dist/runtime/generated-worker.d.ts +55 -0
- package/dist/runtime/generated-worker.js +543 -0
- package/dist/runtime/index.d.ts +4 -1
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/router.d.ts +6 -1
- package/dist/runtime/router.js +125 -31
- package/dist/runtime/security.d.ts +101 -0
- package/dist/runtime/security.js +298 -0
- package/dist/runtime/types.d.ts +29 -2
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ bun run dev
|
|
|
28
28
|
|
|
29
29
|
Point wrangler at the entry and you're done. **No `src/index.ts` needed.**
|
|
30
30
|
|
|
31
|
+
For the framework's internal compiler/runtime orchestration and tracked implementation roadmap, see [ARCHITECTURE.md](./ARCHITECTURE.md).
|
|
32
|
+
|
|
31
33
|
```jsonc
|
|
32
34
|
// wrangler.jsonc
|
|
33
35
|
{
|
|
@@ -764,9 +766,166 @@ Kuratchi also exposes a framework build-mode flag:
|
|
|
764
766
|
- `dev` is compile-time framework state, not a generic process env var
|
|
765
767
|
- `@kuratchi/js/environment` is intended for server route code, not client `$:` scripts
|
|
766
768
|
|
|
769
|
+
## Security
|
|
770
|
+
|
|
771
|
+
Kuratchi includes built-in security features that are enabled by default or configurable via `kuratchi.config.ts`.
|
|
772
|
+
|
|
773
|
+
### Default Security Headers
|
|
774
|
+
|
|
775
|
+
All responses include these headers automatically:
|
|
776
|
+
|
|
777
|
+
- `X-Content-Type-Options: nosniff`
|
|
778
|
+
- `X-Frame-Options: DENY`
|
|
779
|
+
- `Referrer-Policy: strict-origin-when-cross-origin`
|
|
780
|
+
|
|
781
|
+
### CSRF Protection
|
|
782
|
+
|
|
783
|
+
CSRF protection is **enabled by default** for all form actions and RPC calls.
|
|
784
|
+
|
|
785
|
+
**How it works:**
|
|
786
|
+
1. A cryptographically random token is generated per session and stored in a cookie
|
|
787
|
+
2. The compiler auto-injects a hidden `_csrf` field into forms with `action={fn}`
|
|
788
|
+
3. The client bridge includes the CSRF token header in fetch action requests
|
|
789
|
+
4. Server validates the token using timing-safe comparison
|
|
790
|
+
|
|
791
|
+
No configuration required — it just works.
|
|
792
|
+
|
|
793
|
+
```html
|
|
794
|
+
<!-- CSRF token is auto-injected -->
|
|
795
|
+
<form action={submitForm}>
|
|
796
|
+
<input type="text" name="email" />
|
|
797
|
+
<button type="submit">Submit</button>
|
|
798
|
+
</form>
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Authentication Enforcement
|
|
802
|
+
|
|
803
|
+
Optionally require authentication for all RPC calls or form actions:
|
|
804
|
+
|
|
805
|
+
```ts
|
|
806
|
+
// kuratchi.config.ts
|
|
807
|
+
export default defineConfig({
|
|
808
|
+
security: {
|
|
809
|
+
rpcRequireAuth: true, // Require auth for all RPC calls (default: false)
|
|
810
|
+
actionRequireAuth: true, // Require auth for all form actions (default: false)
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
When enabled, unauthenticated requests return `401 Authentication required`. The check looks for `locals.user` or `locals.session.user`, which is populated by `@kuratchi/auth`.
|
|
816
|
+
|
|
817
|
+
For per-function control, use guards in individual functions instead:
|
|
818
|
+
|
|
819
|
+
```ts
|
|
820
|
+
import { requireAuth } from '@kuratchi/auth';
|
|
821
|
+
|
|
822
|
+
export async function deleteItem(formData: FormData) {
|
|
823
|
+
await requireAuth(); // Throws 401 if not authenticated
|
|
824
|
+
// ... action logic
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Configurable Security Headers
|
|
829
|
+
|
|
830
|
+
Add CSP, HSTS, and Permissions-Policy headers:
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
// kuratchi.config.ts
|
|
834
|
+
export default defineConfig({
|
|
835
|
+
security: {
|
|
836
|
+
contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'",
|
|
837
|
+
strictTransportSecurity: "max-age=31536000; includeSubDomains",
|
|
838
|
+
permissionsPolicy: "camera=(), microphone=(), geolocation=()",
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### HTML Sanitization
|
|
844
|
+
|
|
845
|
+
The `{@html}` directive automatically sanitizes output to prevent XSS:
|
|
846
|
+
|
|
847
|
+
- Removes dangerous elements: `<script>`, `<iframe>`, `<object>`, `<embed>`, `<style>`, `<template>`, etc.
|
|
848
|
+
- Strips all `on*` event handlers
|
|
849
|
+
- Neutralizes `javascript:` and `vbscript:` URLs
|
|
850
|
+
- Removes `data:` URLs from `src` attributes
|
|
851
|
+
|
|
852
|
+
For user-generated HTML, we recommend using DOMPurify on the client side for maximum security.
|
|
853
|
+
|
|
854
|
+
### Fragment Refresh Security
|
|
855
|
+
|
|
856
|
+
Fragment IDs used for `data-poll` are automatically signed to prevent attackers from probing for data:
|
|
857
|
+
|
|
858
|
+
- Fragment IDs are signed at render time with the session's CSRF token
|
|
859
|
+
- Server validates signatures before returning fragment content
|
|
860
|
+
- Invalid or unsigned fragments return 403 when CSRF is enabled
|
|
861
|
+
|
|
862
|
+
This is automatic — no configuration required.
|
|
863
|
+
|
|
864
|
+
### Query Override Protection
|
|
865
|
+
|
|
866
|
+
Query function calls via `x-kuratchi-query-fn` headers are validated against a whitelist:
|
|
867
|
+
|
|
868
|
+
- Only query functions registered for the current route can be called
|
|
869
|
+
- Prevents attackers from invoking arbitrary RPC functions
|
|
870
|
+
- Returns 403 for unauthorized query function calls
|
|
871
|
+
|
|
872
|
+
This is automatic — no configuration required.
|
|
873
|
+
|
|
874
|
+
### Client Bridge Security
|
|
875
|
+
|
|
876
|
+
Client-side handler invocation is protected against injection attacks:
|
|
877
|
+
|
|
878
|
+
- Route and handler IDs are validated against safe patterns
|
|
879
|
+
- Prototype pollution attempts are blocked (`__proto__`, `constructor`, `prototype`)
|
|
880
|
+
- Uses `hasOwnProperty` checks to prevent prototype chain traversal
|
|
881
|
+
|
|
882
|
+
This is automatic — no configuration required.
|
|
883
|
+
|
|
884
|
+
### Error Information Protection
|
|
885
|
+
|
|
886
|
+
Error messages are sanitized to prevent information leakage in production:
|
|
887
|
+
|
|
888
|
+
- Generic errors show full details in dev mode only
|
|
889
|
+
- Production uses safe fallback messages ("Internal Server Error", "Action failed")
|
|
890
|
+
- `ActionError` and `PageError` messages are always shown (developer-controlled)
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
// Safe to show - developer-controlled message
|
|
894
|
+
throw new ActionError('Invalid email format');
|
|
895
|
+
|
|
896
|
+
// In production: "Internal Server Error" (details hidden)
|
|
897
|
+
// In dev mode: Full error message for debugging
|
|
898
|
+
throw new Error('Database connection failed at line 42');
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
### Full Security Configuration
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
// kuratchi.config.ts
|
|
905
|
+
export default defineConfig({
|
|
906
|
+
security: {
|
|
907
|
+
// CSRF Protection (enabled by default)
|
|
908
|
+
csrfEnabled: true,
|
|
909
|
+
csrfCookieName: '__kuratchi_csrf',
|
|
910
|
+
csrfHeaderName: 'x-kuratchi-csrf',
|
|
911
|
+
|
|
912
|
+
// Authentication Enforcement
|
|
913
|
+
rpcRequireAuth: false, // Require auth for RPC calls
|
|
914
|
+
actionRequireAuth: false, // Require auth for form actions
|
|
915
|
+
|
|
916
|
+
// Security Headers
|
|
917
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
918
|
+
strictTransportSecurity: "max-age=31536000; includeSubDomains",
|
|
919
|
+
permissionsPolicy: "camera=(), microphone=()",
|
|
920
|
+
},
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
For a comprehensive security analysis and roadmap, see [SECURITY.md](./SECURITY.md).
|
|
925
|
+
|
|
767
926
|
## `kuratchi.config.ts`
|
|
768
927
|
|
|
769
|
-
Optional. Required only when using framework integrations (ORM, auth, UI).
|
|
928
|
+
Optional. Required only when using framework integrations (ORM, auth, UI, security).
|
|
770
929
|
|
|
771
930
|
**Durable Objects are auto-discovered** — no config needed unless you need `stubId` for auth integration.
|
|
772
931
|
|
package/dist/cli.js
CHANGED
|
@@ -6,25 +6,31 @@ import { compile } from './compiler/index.js';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as net from 'node:net';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
12
|
const command = args[0];
|
|
12
13
|
const projectDir = process.cwd();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
void main().catch((err) => {
|
|
15
|
+
console.error(`[kuratchi] ${err?.message ?? err}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
|
18
|
+
async function main() {
|
|
19
|
+
switch (command) {
|
|
20
|
+
case 'build':
|
|
21
|
+
runBuild();
|
|
22
|
+
return;
|
|
23
|
+
case 'watch':
|
|
24
|
+
await runWatch(false);
|
|
25
|
+
return;
|
|
26
|
+
case 'dev':
|
|
27
|
+
await runWatch(true);
|
|
28
|
+
return;
|
|
29
|
+
case 'create':
|
|
30
|
+
await runCreate();
|
|
31
|
+
return;
|
|
32
|
+
default:
|
|
33
|
+
console.log(`
|
|
28
34
|
KuratchiJS CLI
|
|
29
35
|
|
|
30
36
|
Usage:
|
|
@@ -33,7 +39,8 @@ Usage:
|
|
|
33
39
|
kuratchi dev Compile, watch for changes, and start wrangler dev server
|
|
34
40
|
kuratchi watch Compile + watch only (no wrangler — for custom setups)
|
|
35
41
|
`);
|
|
36
|
-
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
async function runCreate() {
|
|
39
46
|
const { create } = await import('./create.js');
|
|
@@ -42,10 +49,10 @@ async function runCreate() {
|
|
|
42
49
|
const positional = remaining.filter(a => !a.startsWith('-'));
|
|
43
50
|
await create(positional[0], flags);
|
|
44
51
|
}
|
|
45
|
-
function runBuild(isDev = false) {
|
|
52
|
+
async function runBuild(isDev = false) {
|
|
46
53
|
console.log('[kuratchi] Compiling...');
|
|
47
54
|
try {
|
|
48
|
-
const outFile = compile({ projectDir, isDev });
|
|
55
|
+
const outFile = await compile({ projectDir, isDev });
|
|
49
56
|
console.log(`[kuratchi] Built → ${path.relative(projectDir, outFile)}`);
|
|
50
57
|
}
|
|
51
58
|
catch (err) {
|
|
@@ -53,8 +60,8 @@ function runBuild(isDev = false) {
|
|
|
53
60
|
process.exit(1);
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
|
-
function runWatch(withWrangler = false) {
|
|
57
|
-
runBuild(true);
|
|
63
|
+
async function runWatch(withWrangler = false) {
|
|
64
|
+
await runBuild(true);
|
|
58
65
|
const routesDir = path.join(projectDir, 'src', 'routes');
|
|
59
66
|
const serverDir = path.join(projectDir, 'src', 'server');
|
|
60
67
|
const watchDirs = [routesDir, serverDir].filter(d => fs.existsSync(d));
|
|
@@ -62,10 +69,10 @@ function runWatch(withWrangler = false) {
|
|
|
62
69
|
const triggerRebuild = () => {
|
|
63
70
|
if (rebuildTimeout)
|
|
64
71
|
clearTimeout(rebuildTimeout);
|
|
65
|
-
rebuildTimeout = setTimeout(() => {
|
|
72
|
+
rebuildTimeout = setTimeout(async () => {
|
|
66
73
|
console.log('[kuratchi] File changed, rebuilding...');
|
|
67
74
|
try {
|
|
68
|
-
compile({ projectDir, isDev: true });
|
|
75
|
+
await compile({ projectDir, isDev: true });
|
|
69
76
|
console.log('[kuratchi] Rebuilt.');
|
|
70
77
|
}
|
|
71
78
|
catch (err) {
|
|
@@ -80,11 +87,10 @@ function runWatch(withWrangler = false) {
|
|
|
80
87
|
// `kuratchi dev` also starts the wrangler dev server.
|
|
81
88
|
// `kuratchi watch` is the compiler-only mode for custom setups.
|
|
82
89
|
if (withWrangler) {
|
|
83
|
-
startWranglerDev()
|
|
84
|
-
|
|
85
|
-
process.exit(1);
|
|
86
|
-
});
|
|
90
|
+
await startWranglerDev();
|
|
91
|
+
return;
|
|
87
92
|
}
|
|
93
|
+
await new Promise(() => { });
|
|
88
94
|
}
|
|
89
95
|
function hasPortFlag(inputArgs) {
|
|
90
96
|
for (let i = 0; i < inputArgs.length; i++) {
|
|
@@ -117,25 +123,13 @@ async function findOpenPort(start = 8787, end = 8899) {
|
|
|
117
123
|
}
|
|
118
124
|
async function startWranglerDev() {
|
|
119
125
|
const passthroughArgs = args.slice(1);
|
|
120
|
-
const wranglerArgs = ['
|
|
126
|
+
const wranglerArgs = ['dev', ...passthroughArgs];
|
|
121
127
|
if (!hasPortFlag(passthroughArgs)) {
|
|
122
128
|
const port = await findOpenPort();
|
|
123
129
|
wranglerArgs.push('--port', String(port));
|
|
124
130
|
console.log(`[kuratchi] Starting wrangler dev on port ${port}`);
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
// prematurely when launched via a script runner (e.g. `bun run dev`).
|
|
128
|
-
const isWin = process.platform === 'win32';
|
|
129
|
-
const wrangler = isWin
|
|
130
|
-
? spawn('npx ' + wranglerArgs.join(' '), {
|
|
131
|
-
cwd: projectDir,
|
|
132
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
133
|
-
shell: true,
|
|
134
|
-
})
|
|
135
|
-
: spawn('npx', wranglerArgs, {
|
|
136
|
-
cwd: projectDir,
|
|
137
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
138
|
-
});
|
|
132
|
+
const wrangler = spawnWranglerProcess(wranglerArgs);
|
|
139
133
|
const cleanup = () => {
|
|
140
134
|
if (!wrangler.killed)
|
|
141
135
|
wrangler.kill();
|
|
@@ -143,10 +137,49 @@ async function startWranglerDev() {
|
|
|
143
137
|
process.on('exit', cleanup);
|
|
144
138
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
145
139
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
await new Promise((resolve, reject) => {
|
|
141
|
+
wrangler.on('exit', (code) => {
|
|
142
|
+
if (code !== 0 && code !== null) {
|
|
143
|
+
reject(new Error(`wrangler exited with code ${code}`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
resolve();
|
|
147
|
+
});
|
|
148
|
+
wrangler.on('error', (err) => {
|
|
149
|
+
reject(err);
|
|
150
|
+
});
|
|
151
|
+
}).catch((err) => {
|
|
152
|
+
console.error(`[kuratchi] Failed to start wrangler dev: ${err?.message ?? err}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
function resolveWranglerBin() {
|
|
157
|
+
try {
|
|
158
|
+
const projectPackageJson = path.join(projectDir, 'package.json');
|
|
159
|
+
const projectRequire = createRequire(projectPackageJson);
|
|
160
|
+
return projectRequire.resolve('wrangler/bin/wrangler.js');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function getNodeExecutable() {
|
|
167
|
+
if (!process.versions.bun)
|
|
168
|
+
return process.execPath;
|
|
169
|
+
return 'node';
|
|
170
|
+
}
|
|
171
|
+
function spawnWranglerProcess(wranglerArgs) {
|
|
172
|
+
const localWranglerBin = resolveWranglerBin();
|
|
173
|
+
const stdio = ['pipe', 'inherit', 'inherit'];
|
|
174
|
+
if (localWranglerBin) {
|
|
175
|
+
return spawn(getNodeExecutable(), [localWranglerBin, ...wranglerArgs], {
|
|
176
|
+
cwd: projectDir,
|
|
177
|
+
stdio,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const fallbackCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
181
|
+
return spawn(fallbackCommand, ['wrangler', ...wranglerArgs], {
|
|
182
|
+
cwd: projectDir,
|
|
183
|
+
stdio,
|
|
151
184
|
});
|
|
152
185
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const ALL_API_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
4
|
+
export function compileApiRoute(opts) {
|
|
5
|
+
const outFileDir = path.join(opts.projectDir, '.kuratchi');
|
|
6
|
+
const absRoutePath = opts.transformModule(opts.fullPath);
|
|
7
|
+
let importPath = path.relative(outFileDir, absRoutePath).replace(/\\/g, '/');
|
|
8
|
+
if (!importPath.startsWith('.'))
|
|
9
|
+
importPath = './' + importPath;
|
|
10
|
+
const moduleId = opts.allocateModuleId();
|
|
11
|
+
opts.pushImport(`import * as ${moduleId} from '${importPath}';`);
|
|
12
|
+
const apiSource = fs.readFileSync(opts.fullPath, 'utf-8');
|
|
13
|
+
const exportedMethods = ALL_API_METHODS.filter((method) => {
|
|
14
|
+
const fnPattern = new RegExp(`export\\s+(async\\s+)?function\\s+${method}\\b`);
|
|
15
|
+
const reExportPattern = new RegExp(`export\\s*\\{[^}]*\\b\\w+\\s+as\\s+${method}\\b`);
|
|
16
|
+
const namedExportPattern = new RegExp(`export\\s*\\{[^}]*\\b${method}\\b`);
|
|
17
|
+
return fnPattern.test(apiSource) || reExportPattern.test(apiSource) || namedExportPattern.test(apiSource);
|
|
18
|
+
});
|
|
19
|
+
const methodEntries = exportedMethods
|
|
20
|
+
.map((method) => `${method}: ${moduleId}.${method}`)
|
|
21
|
+
.join(', ');
|
|
22
|
+
return `{ pattern: '${opts.pattern}', __api: true, ${methodEntries} }`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
const MIME_TYPES = {
|
|
5
|
+
'.css': 'text/css; charset=utf-8',
|
|
6
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
7
|
+
'.json': 'application/json; charset=utf-8',
|
|
8
|
+
'.svg': 'image/svg+xml',
|
|
9
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
10
|
+
};
|
|
11
|
+
export function compileAssets(assetsDir) {
|
|
12
|
+
const compiledAssets = [];
|
|
13
|
+
if (!fs.existsSync(assetsDir))
|
|
14
|
+
return compiledAssets;
|
|
15
|
+
const scanAssets = (dir, prefix) => {
|
|
16
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
scanAssets(path.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
22
|
+
const mime = MIME_TYPES[ext];
|
|
23
|
+
if (!mime)
|
|
24
|
+
continue;
|
|
25
|
+
const content = fs.readFileSync(path.join(dir, entry.name), 'utf-8');
|
|
26
|
+
const etag = '"' + crypto.createHash('md5').update(content).digest('hex').slice(0, 12) + '"';
|
|
27
|
+
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
28
|
+
compiledAssets.push({ name, content, mime, etag });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
scanAssets(assetsDir, '');
|
|
32
|
+
return compiledAssets;
|
|
33
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CompiledAsset } from './asset-pipeline.js';
|
|
2
|
+
import { type RouteImportEntry } from './import-linking.js';
|
|
3
|
+
export interface ClientEventRegistration {
|
|
4
|
+
routeId: string;
|
|
5
|
+
handlerId: string;
|
|
6
|
+
argsExpr: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface ClientModuleCompiler {
|
|
9
|
+
createRegistry(scopeId: string, importEntries: RouteImportEntry[]): ClientRouteRegistry;
|
|
10
|
+
createRouteRegistry(routeIndex: number, importEntries: RouteImportEntry[]): ClientRouteRegistry;
|
|
11
|
+
getCompiledAssets(): CompiledAsset[];
|
|
12
|
+
}
|
|
13
|
+
export interface ClientRouteRegistry {
|
|
14
|
+
hasBindings(): boolean;
|
|
15
|
+
hasBindingReference(expression: string): boolean;
|
|
16
|
+
registerEventHandler(eventName: string, expression: string): ClientEventRegistration | null;
|
|
17
|
+
buildEntryAsset(): {
|
|
18
|
+
assetName: string;
|
|
19
|
+
asset: CompiledAsset;
|
|
20
|
+
} | null;
|
|
21
|
+
}
|
|
22
|
+
export declare function createClientModuleCompiler(opts: {
|
|
23
|
+
projectDir: string;
|
|
24
|
+
srcDir: string;
|
|
25
|
+
}): ClientModuleCompiler;
|