@fabrika/app 0.9.0-dev.1 → 0.9.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +27 -26
- package/skill/SKILL.md +1 -1
- package/skill/references/app-setup.md +8 -8
- package/src/dev/changes.ts +5 -43
- package/src/dev/contract-digest.ts +17 -0
- package/src/dev/index.ts +1 -1
- package/src/dev/launcher.ts +116 -31
- package/src/dev/wire.ts +11 -1
- package/types/src/agent-skill.d.ts +29 -0
- package/types/src/build.d.ts +11 -0
- package/types/src/bun.d.ts +66 -0
- package/types/src/client/caller.d.ts +19 -0
- package/types/src/client/contract.d.ts +131 -0
- package/types/src/client/error-reporting.d.ts +66 -0
- package/types/src/client/index.d.ts +4 -0
- package/types/src/client/rpc.d.ts +14 -0
- package/types/src/client/share.d.ts +2 -0
- package/types/src/client/transport.d.ts +42 -0
- package/types/src/cloudflare.d.ts +49 -0
- package/types/src/contract/artifact-schema.d.ts +8 -0
- package/types/src/contract/attachment.d.ts +41 -0
- package/types/src/contract/build.d.ts +12 -0
- package/types/src/contract/compile.d.ts +11 -0
- package/types/src/contract/connect.d.ts +51 -0
- package/types/src/contract/context.d.ts +501 -0
- package/types/src/contract/declaration.d.ts +303 -0
- package/types/src/contract/defaults.d.ts +208 -0
- package/types/src/contract/define.d.ts +359 -0
- package/types/src/contract/file.d.ts +44 -0
- package/types/src/contract/group.d.ts +274 -0
- package/types/src/contract/handled.d.ts +18 -0
- package/types/src/contract/index.d.ts +19 -0
- package/types/src/contract/kit.d.ts +210 -0
- package/types/src/contract/platform.d.ts +9 -0
- package/types/src/contract/problems.d.ts +14 -0
- package/types/src/contract/redirect.d.ts +25 -0
- package/types/src/contract/schema-from-shape.d.ts +10 -0
- package/types/src/contract/schema-shape.d.ts +5 -0
- package/types/src/contract/snapshot-source.d.ts +15 -0
- package/types/src/contract/token.d.ts +58 -0
- package/types/src/contract/vocabulary.d.ts +30 -0
- package/types/src/contract/webhook-binding.d.ts +24 -0
- package/types/src/contract/webhook-scheme.d.ts +23 -0
- package/types/src/dev/changes.d.ts +7 -0
- package/types/src/dev/contract-digest.d.ts +1 -0
- package/types/src/dev/index.d.ts +2 -0
- package/types/src/dev/launcher.d.ts +32 -0
- package/types/src/dev/output.d.ts +22 -0
- package/types/src/dev/wire.d.ts +126 -0
- package/types/src/disk-assets.d.ts +3 -0
- package/types/src/errors.d.ts +46 -0
- package/types/src/fetch.d.ts +7 -0
- package/types/src/files/archive.d.ts +26 -0
- package/types/src/files/clamscan.d.ts +32 -0
- package/types/src/files/handle.d.ts +106 -0
- package/types/src/files/kinds.d.ts +105 -0
- package/types/src/files/scan.d.ts +80 -0
- package/types/src/files/sniff.d.ts +7 -0
- package/types/src/files/streams.d.ts +17 -0
- package/types/src/files/treatment.d.ts +18 -0
- package/types/src/from-row.d.ts +20 -0
- package/types/src/honeypot.d.ts +2 -0
- package/types/src/implement/anonymous.d.ts +26 -0
- package/types/src/implement/attachment.d.ts +14 -0
- package/types/src/implement/boot.d.ts +109 -0
- package/types/src/implement/bounded.d.ts +22 -0
- package/types/src/implement/browser-config.d.ts +19 -0
- package/types/src/implement/capability.d.ts +140 -0
- package/types/src/implement/challenge.d.ts +75 -0
- package/types/src/implement/client-address.d.ts +11 -0
- package/types/src/implement/connect.d.ts +67 -0
- package/types/src/implement/crashes.d.ts +31 -0
- package/types/src/implement/dispatch.d.ts +218 -0
- package/types/src/implement/egress.d.ts +14 -0
- package/types/src/implement/environment.d.ts +14 -0
- package/types/src/implement/failures.d.ts +34 -0
- package/types/src/implement/faults.d.ts +44 -0
- package/types/src/implement/headers.d.ts +7 -0
- package/types/src/implement/implement.d.ts +58 -0
- package/types/src/implement/index.d.ts +24 -0
- package/types/src/implement/invocation.d.ts +120 -0
- package/types/src/implement/membership.d.ts +201 -0
- package/types/src/implement/multipart.d.ts +36 -0
- package/types/src/implement/params.d.ts +7 -0
- package/types/src/implement/principal.d.ts +52 -0
- package/types/src/implement/project.d.ts +11 -0
- package/types/src/implement/public.d.ts +61 -0
- package/types/src/implement/serve.d.ts +25 -0
- package/types/src/implement/tokens.d.ts +49 -0
- package/types/src/implement/types.d.ts +243 -0
- package/types/src/implement/urls.d.ts +15 -0
- package/types/src/implement/verifiers.d.ts +19 -0
- package/types/src/implement/webhook.d.ts +55 -0
- package/types/src/implement/workload.d.ts +56 -0
- package/types/src/implement/xlsx.d.ts +6 -0
- package/types/src/index.d.ts +12 -0
- package/types/src/jobs/exception.d.ts +40 -0
- package/types/src/jobs/mail.d.ts +67 -0
- package/types/src/jobs/report.d.ts +56 -0
- package/types/src/jobs/runner.d.ts +145 -0
- package/types/src/jobs/schedule.d.ts +15 -0
- package/types/src/jobs/scrub.d.ts +12 -0
- package/types/src/jobs/types.d.ts +92 -0
- package/types/src/react/caller.d.ts +30 -0
- package/types/src/react/errors.d.ts +40 -0
- package/types/src/react/hooks.d.ts +110 -0
- package/types/src/react/index.d.ts +5 -0
- package/types/src/react/provider.d.ts +58 -0
- package/types/src/react/store.d.ts +68 -0
- package/types/src/react/testing.d.ts +28 -0
- package/types/src/react/types.d.ts +33 -0
- package/types/src/rpc/builder.d.ts +52 -0
- package/types/src/rpc/contract.d.ts +23 -0
- package/types/src/rpc/dispatcher.d.ts +11 -0
- package/types/src/rpc/types.d.ts +62 -0
- package/types/src/service/define.d.ts +42 -0
- package/types/src/service/index.d.ts +3 -0
- package/types/src/service/serve.d.ts +28 -0
- package/types/src/standard-schema.d.ts +55 -0
- package/types/src/testing/caller.d.ts +95 -0
- package/types/src/testing/challenge.d.ts +11 -0
- package/types/src/testing/dev.d.ts +97 -0
- package/types/src/testing/endpoint-access.d.ts +99 -0
- package/types/src/testing/index.d.ts +12 -0
- package/types/src/testing/isolation.d.ts +3 -0
- package/types/src/testing/kit.d.ts +182 -0
- package/types/src/testing/membership.d.ts +132 -0
- package/types/src/testing/policy-isolation.d.ts +113 -0
- package/types/src/testing/preload.d.ts +1 -0
- package/types/src/testing/proxy.d.ts +40 -0
- package/types/src/testing/runner.d.ts +8 -0
- package/types/src/testing/sample-file.d.ts +5 -0
- package/types/src/testing/status.d.ts +4 -0
- package/types/src/undumpable.d.ts +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fabrika/app",
|
|
3
|
-
"version": "0.9.0-dev.
|
|
3
|
+
"version": "0.9.0-dev.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,87 +14,88 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"src",
|
|
16
16
|
"skill",
|
|
17
|
-
"!src/**/__tests__/**"
|
|
17
|
+
"!src/**/__tests__/**",
|
|
18
|
+
"types"
|
|
18
19
|
],
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
21
22
|
"bun": "./src/index.ts",
|
|
22
|
-
"types": "./src/index.ts",
|
|
23
|
+
"types": "./types/src/index.d.ts",
|
|
23
24
|
"default": "./src/index.ts"
|
|
24
25
|
},
|
|
25
26
|
"./bun": {
|
|
26
27
|
"bun": "./src/bun.ts",
|
|
27
|
-
"types": "./src/bun.ts",
|
|
28
|
+
"types": "./types/src/bun.d.ts",
|
|
28
29
|
"default": "./src/bun.ts"
|
|
29
30
|
},
|
|
30
31
|
"./cloudflare": {
|
|
31
32
|
"bun": "./src/cloudflare.ts",
|
|
32
|
-
"types": "./src/cloudflare.ts",
|
|
33
|
+
"types": "./types/src/cloudflare.d.ts",
|
|
33
34
|
"default": "./src/cloudflare.ts"
|
|
34
35
|
},
|
|
35
36
|
"./contract": {
|
|
36
37
|
"bun": "./src/contract/index.ts",
|
|
37
|
-
"types": "./src/contract/index.ts",
|
|
38
|
+
"types": "./types/src/contract/index.d.ts",
|
|
38
39
|
"default": "./src/contract/index.ts"
|
|
39
40
|
},
|
|
40
41
|
"./client": {
|
|
41
42
|
"bun": "./src/client/index.ts",
|
|
42
|
-
"types": "./src/client/index.ts",
|
|
43
|
+
"types": "./types/src/client/index.d.ts",
|
|
43
44
|
"default": "./src/client/index.ts"
|
|
44
45
|
},
|
|
45
46
|
"./react": {
|
|
46
47
|
"bun": "./src/react/index.ts",
|
|
47
|
-
"types": "./src/react/index.ts",
|
|
48
|
+
"types": "./types/src/react/index.d.ts",
|
|
48
49
|
"default": "./src/react/index.ts"
|
|
49
50
|
},
|
|
50
51
|
"./react/testing": {
|
|
51
52
|
"bun": "./src/react/testing.ts",
|
|
52
|
-
"types": "./src/react/testing.ts",
|
|
53
|
+
"types": "./types/src/react/testing.d.ts",
|
|
53
54
|
"default": "./src/react/testing.ts"
|
|
54
55
|
},
|
|
55
56
|
"./implement": {
|
|
56
57
|
"bun": "./src/implement/index.ts",
|
|
57
|
-
"types": "./src/implement/index.ts",
|
|
58
|
+
"types": "./types/src/implement/index.d.ts",
|
|
58
59
|
"default": "./src/implement/index.ts"
|
|
59
60
|
},
|
|
60
61
|
"./service": {
|
|
61
62
|
"bun": "./src/service/index.ts",
|
|
62
|
-
"types": "./src/service/index.ts",
|
|
63
|
+
"types": "./types/src/service/index.d.ts",
|
|
63
64
|
"default": "./src/service/index.ts"
|
|
64
65
|
},
|
|
65
66
|
"./build": {
|
|
66
67
|
"bun": "./src/build.ts",
|
|
67
|
-
"types": "./src/build.ts",
|
|
68
|
+
"types": "./types/src/build.d.ts",
|
|
68
69
|
"default": "./src/build.ts"
|
|
69
70
|
},
|
|
70
71
|
"./testing": {
|
|
71
72
|
"bun": "./src/testing/index.ts",
|
|
72
|
-
"types": "./src/testing/index.ts",
|
|
73
|
+
"types": "./types/src/testing/index.d.ts",
|
|
73
74
|
"default": "./src/testing/index.ts"
|
|
74
75
|
},
|
|
75
76
|
"./testing/isolation": {
|
|
76
77
|
"bun": "./src/testing/isolation.ts",
|
|
77
|
-
"types": "./src/testing/isolation.ts",
|
|
78
|
+
"types": "./types/src/testing/isolation.d.ts",
|
|
78
79
|
"default": "./src/testing/isolation.ts"
|
|
79
80
|
},
|
|
80
81
|
"./testing/preload": {
|
|
81
82
|
"bun": "./src/testing/preload.ts",
|
|
82
|
-
"types": "./src/testing/preload.ts",
|
|
83
|
+
"types": "./types/src/testing/preload.d.ts",
|
|
83
84
|
"default": "./src/testing/preload.ts"
|
|
84
85
|
},
|
|
85
86
|
"./dev": {
|
|
86
87
|
"bun": "./src/dev/index.ts",
|
|
87
|
-
"types": "./src/dev/index.ts",
|
|
88
|
+
"types": "./types/src/dev/index.d.ts",
|
|
88
89
|
"default": "./src/dev/index.ts"
|
|
89
90
|
},
|
|
90
91
|
"./dev/wire": {
|
|
91
92
|
"bun": "./src/dev/wire.ts",
|
|
92
|
-
"types": "./src/dev/wire.ts",
|
|
93
|
+
"types": "./types/src/dev/wire.d.ts",
|
|
93
94
|
"default": "./src/dev/wire.ts"
|
|
94
95
|
},
|
|
95
96
|
"./agent-skill": {
|
|
96
97
|
"bun": "./src/agent-skill.ts",
|
|
97
|
-
"types": "./src/agent-skill.ts",
|
|
98
|
+
"types": "./types/src/agent-skill.d.ts",
|
|
98
99
|
"default": "./src/agent-skill.ts"
|
|
99
100
|
}
|
|
100
101
|
},
|
|
@@ -103,14 +104,14 @@
|
|
|
103
104
|
"test": "bun test --path-ignore-patterns='src/react/**' && bun test --isolate src/react"
|
|
104
105
|
},
|
|
105
106
|
"dependencies": {
|
|
106
|
-
"@fabrika/auth": "0.9.0-dev.
|
|
107
|
-
"@fabrika/auth-core": "0.9.0-dev.
|
|
108
|
-
"@fabrika/db": "0.9.0-dev.
|
|
109
|
-
"@fabrika/email": "0.9.0-dev.
|
|
110
|
-
"@fabrika/jwks": "0.9.0-dev.
|
|
111
|
-
"@fabrika/platform": "0.9.0-dev.
|
|
112
|
-
"@fabrika/platform-node": "0.9.0-dev.
|
|
113
|
-
"@fabrika/proxy-core": "0.9.0-dev.
|
|
107
|
+
"@fabrika/auth": "0.9.0-dev.3",
|
|
108
|
+
"@fabrika/auth-core": "0.9.0-dev.3",
|
|
109
|
+
"@fabrika/db": "0.9.0-dev.3",
|
|
110
|
+
"@fabrika/email": "0.9.0-dev.3",
|
|
111
|
+
"@fabrika/jwks": "0.9.0-dev.3",
|
|
112
|
+
"@fabrika/platform": "0.9.0-dev.3",
|
|
113
|
+
"@fabrika/platform-node": "0.9.0-dev.3",
|
|
114
|
+
"@fabrika/proxy-core": "0.9.0-dev.3",
|
|
114
115
|
"altcha-lib": "2.6.0",
|
|
115
116
|
"jose": "^5.9.0"
|
|
116
117
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabrika-app
|
|
3
|
-
version: 0.9.0-dev.
|
|
3
|
+
version: 0.9.0-dev.3
|
|
4
4
|
description: >
|
|
5
5
|
How to write and change a Fabrika application: src/vocabulary.ts, db/migrations/, src/db/tables.ts, the
|
|
6
6
|
slices' endpoints.ts with their handlers, fabrika.contract.ts, jobs, files, webhooks, share links, connectors
|
|
@@ -19,20 +19,20 @@ the SPA is added with it.
|
|
|
19
19
|
"test": "bun test --isolate"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@fabrika/app": "0.9.0-dev.
|
|
23
|
-
"@fabrika/db": "0.9.0-dev.
|
|
24
|
-
"@fabrika/provider-zerops": "0.9.0-dev.
|
|
25
|
-
"@fabrika/proxy-core": "0.9.0-dev.
|
|
26
|
-
"@fabrika/ui": "0.9.0-dev.
|
|
22
|
+
"@fabrika/app": "0.9.0-dev.3",
|
|
23
|
+
"@fabrika/db": "0.9.0-dev.3",
|
|
24
|
+
"@fabrika/provider-zerops": "0.9.0-dev.3",
|
|
25
|
+
"@fabrika/proxy-core": "0.9.0-dev.3",
|
|
26
|
+
"@fabrika/ui": "0.9.0-dev.3",
|
|
27
27
|
"kysely": "0.29.6",
|
|
28
28
|
"react": "^19.0.0",
|
|
29
29
|
"react-dom": "^19.0.0",
|
|
30
30
|
"zod": "^3.25.76"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@fabrika/check": "0.9.0-dev.
|
|
34
|
-
"@fabrika/cli": "0.9.0-dev.
|
|
35
|
-
"@fabrika/platform": "0.9.0-dev.
|
|
33
|
+
"@fabrika/check": "0.9.0-dev.3",
|
|
34
|
+
"@fabrika/cli": "0.9.0-dev.3",
|
|
35
|
+
"@fabrika/platform": "0.9.0-dev.3",
|
|
36
36
|
"@happy-dom/global-registrator": "^20.11.2",
|
|
37
37
|
"@types/bun": "latest",
|
|
38
38
|
"@types/react": "^19.0.0",
|
package/src/dev/changes.ts
CHANGED
|
@@ -1,64 +1,26 @@
|
|
|
1
1
|
// What a sync changes, read off its paths: whether the build owns one (then the commit needs a bootstrap run), whether
|
|
2
|
-
// the migrations changed, and whether a web source did.
|
|
2
|
+
// the migrations changed, and whether a web source did. Whether the contract changed is the digest's to say, not a path's.
|
|
3
3
|
|
|
4
|
-
import { posix } from 'node:path'
|
|
5
4
|
import { CONTRACT_FILE } from '../build.js'
|
|
6
5
|
|
|
7
6
|
/** The application's migrations, as `fabrika db migrate` reads them. */
|
|
8
7
|
export const MIGRATIONS_DIRECTORY = 'db/migrations/'
|
|
9
8
|
|
|
10
|
-
/** Files whose change the build turns into something else: an install, the
|
|
9
|
+
/** Files whose change the build turns into something else: an install, or the generated run and build descriptors. */
|
|
11
10
|
const BUILD_OWNED_FILES: ReadonlySet<string> = new Set(['package.json', 'bun.lock', 'bun.lockb', 'bunfig.toml', 'zerops.yaml', 'zerops.yml'])
|
|
12
11
|
const BUILD_OWNED_DIRECTORIES = ['fabrika/', 'node_modules/'] as const
|
|
13
12
|
|
|
14
13
|
/** Server code and the database: a change only there leaves the SPA as it is. */
|
|
15
14
|
const SERVER_DIRECTORIES = ['src/', 'db/'] as const
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
const RELATIVE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)['"](\.{1,2}\/[^'"]+)['"]/g
|
|
21
|
-
|
|
22
|
-
/** The repository paths a relative specifier of `from` may name, in the order Bun resolves a TypeScript import. */
|
|
23
|
-
function candidates(from: string, specifier: string): string[] {
|
|
24
|
-
const base = posix.normalize(posix.join(posix.dirname(from), specifier))
|
|
25
|
-
if (base.startsWith('../')) return []
|
|
26
|
-
const withoutJs = base.endsWith('.js') ? base.slice(0, -3) : base
|
|
27
|
-
return [base, `${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}/index.ts`]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** `fabrika.contract.ts` and every module it reaches by a relative import, read through `read`. */
|
|
31
|
-
export function contractModules(read: SourceReader): Set<string> {
|
|
32
|
-
const modules = new Set<string>()
|
|
33
|
-
const pending = [CONTRACT_FILE]
|
|
34
|
-
while (pending.length > 0) {
|
|
35
|
-
const path = pending.pop() ?? ''
|
|
36
|
-
if (modules.has(path)) continue
|
|
37
|
-
const text = read(path)
|
|
38
|
-
if (text === null) continue
|
|
39
|
-
modules.add(path)
|
|
40
|
-
for (const match of text.matchAll(RELATIVE_IMPORT)) {
|
|
41
|
-
const resolved = candidates(path, match[1] ?? '').find((candidate) => read(candidate) !== null)
|
|
42
|
-
if (resolved !== undefined) pending.push(resolved)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return modules
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Why the change needs a bootstrap run, in one line, or null when the launcher may apply it. `before` reads the
|
|
50
|
-
* container's files, `after` the files at the target: a module the contract imports at either counts.
|
|
51
|
-
*/
|
|
52
|
-
export function bootstrapReason(paths: readonly string[], before: SourceReader, after: SourceReader): string | null {
|
|
53
|
-
const contract = new Set([...contractModules(before), ...contractModules(after)])
|
|
16
|
+
/** Why the change needs a bootstrap run, in one line, or null when the launcher may apply it. */
|
|
17
|
+
export function bootstrapReason(paths: readonly string[]): string | null {
|
|
54
18
|
for (const path of paths) {
|
|
55
19
|
const why = BUILD_OWNED_FILES.has(path)
|
|
56
20
|
? 'the dependencies, the lockfile or the build descriptor'
|
|
57
21
|
: BUILD_OWNED_DIRECTORIES.some((directory) => path.startsWith(directory))
|
|
58
22
|
? 'the generated files or the installed packages'
|
|
59
|
-
:
|
|
60
|
-
? 'the contract or a module it imports (its digest and the proxy table change)'
|
|
61
|
-
: path.startsWith('fabrika.') && !path.includes('/')
|
|
23
|
+
: path !== CONTRACT_FILE && path.startsWith('fabrika.') && !path.includes('/')
|
|
62
24
|
? 'a fabrika.* file'
|
|
63
25
|
: null
|
|
64
26
|
if (why !== null) return `${path} changed, which is ${why}; that needs a bootstrap run: call bind_dev again`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// The launcher's child that answers whether a commit changed the contract (ADR-0049). It runs in the application root
|
|
2
|
+
// with no credential in its environment, loads the application module the launcher itself never loads, and prints the
|
|
3
|
+
// digest the application's boot compares with `FABRIKA_CONTRACT_DIGEST`.
|
|
4
|
+
|
|
5
|
+
import { contractDigest } from '@fabrika/auth-core'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { IMPLEMENTATION_FILE } from '../build.js'
|
|
8
|
+
import { compileContract } from '../contract/compile.js'
|
|
9
|
+
import { isContractImplementation } from '../implement/implement.js'
|
|
10
|
+
|
|
11
|
+
const module: unknown = await import(join(process.cwd(), IMPLEMENTATION_FILE))
|
|
12
|
+
const app = typeof module === 'object' && module !== null ? Reflect.get(module, 'default') : undefined
|
|
13
|
+
if (!isContractImplementation(app)) {
|
|
14
|
+
console.error(`${IMPLEMENTATION_FILE} does not default-export serve(contract, { … })`)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
console.log(contractDigest(compileContract(app.contract, app.database?.tables ?? {})))
|
package/src/dev/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @fabrika/app/dev — the launcher of a `dev` environment, which the generated dev entrypoint runs (Bun only).
|
|
2
2
|
|
|
3
|
-
export { bootstrapReason,
|
|
3
|
+
export { bootstrapReason, migrationsChanged, webSourceChanged } from './changes.js'
|
|
4
4
|
export {
|
|
5
5
|
type DevLauncher,
|
|
6
6
|
type DevLauncherOptions,
|
package/src/dev/launcher.ts
CHANGED
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
// files, which it writes, then it migrates when `db/migrations/` changed, runs the SPA commands when a web source
|
|
5
5
|
// changed, and respawns the application. A change the build owns is refused with one line: it needs a bootstrap run.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEV_LAUNCHER_CONFIG_FILE,
|
|
9
|
+
DEV_LAUNCHER_STATE_FILE,
|
|
10
|
+
type DevLauncherConfig,
|
|
11
|
+
FABRIKA_CONTRACT_DIGEST,
|
|
12
|
+
platformTransportVariables,
|
|
13
|
+
} from '@fabrika/auth-core'
|
|
8
14
|
import { applicationRuntimeUrl, dropApplicationDatabase } from '@fabrika/platform-node'
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
10
15
|
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
11
16
|
import { dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
12
18
|
import { z } from 'zod/v4'
|
|
13
19
|
import { makeProcessUndumpable } from '../undumpable.js'
|
|
14
|
-
import { bootstrapReason, migrationsChanged,
|
|
20
|
+
import { bootstrapReason, migrationsChanged, webSourceChanged } from './changes.js'
|
|
15
21
|
import { OutputRing, type OutputSink, processOutput, readLines } from './output.js'
|
|
16
22
|
import {
|
|
17
23
|
DEV_SOURCE_REPORT_PATH,
|
|
@@ -41,6 +47,9 @@ const STOP_GRACE_MS = 3_000
|
|
|
41
47
|
const CHECK_TIMEOUT_MS = 10 * 60_000
|
|
42
48
|
const CRASH_BACKOFF_MS = [1_000, 2_000, 5_000, 10_000] as const
|
|
43
49
|
const FORWARDED_SIGNALS = ['SIGTERM', 'SIGINT', 'SIGHUP'] as const
|
|
50
|
+
const DIGEST_TIMEOUT_MS = 60_000
|
|
51
|
+
const CONTRACT_DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
52
|
+
const CONTRACT_DIGEST_CHILD = fileURLToPath(new URL('./contract-digest.ts', import.meta.url))
|
|
44
53
|
|
|
45
54
|
const commitSha = z.string().regex(/^[0-9a-f]{40}$/)
|
|
46
55
|
/** A literal path of unreserved characters, as the provider's readiness probe requests it. */
|
|
@@ -118,9 +127,14 @@ interface CommandResult {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
type SyncOutcome =
|
|
121
|
-
| { readonly kind: 'applied'; readonly migrations: boolean; readonly
|
|
130
|
+
| { readonly kind: 'applied'; readonly migrations: boolean; readonly spaFailure: string | null }
|
|
122
131
|
| { readonly kind: 'refused'; readonly reason: string }
|
|
123
132
|
|
|
133
|
+
/** Control answers 409 to the container of a bootstrap run that a newer one replaced. */
|
|
134
|
+
class SupersededError extends Error {}
|
|
135
|
+
|
|
136
|
+
const SUPERSEDED = 'a newer bootstrap run replaced this container; it stops following Control and serves until it is stopped'
|
|
137
|
+
|
|
124
138
|
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))
|
|
125
139
|
|
|
126
140
|
/** Every value of the environment a log line must never show: the database credentials and every key, secret or token. */
|
|
@@ -167,6 +181,8 @@ class Launcher {
|
|
|
167
181
|
private crashes = 0
|
|
168
182
|
private stopped = false
|
|
169
183
|
private reportFailing = false
|
|
184
|
+
/** A newer bootstrap run replaced this container: it keeps serving until the platform stops it, and asks nothing. */
|
|
185
|
+
private superseded = false
|
|
170
186
|
|
|
171
187
|
constructor(private readonly options: DevLauncherOptions) {
|
|
172
188
|
this.root = options.root
|
|
@@ -216,7 +232,12 @@ class Launcher {
|
|
|
216
232
|
while (!this.stopped) {
|
|
217
233
|
try {
|
|
218
234
|
return await this.wait(false)
|
|
219
|
-
} catch {
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof SupersededError) {
|
|
237
|
+
this.say(SUPERSEDED, 'stderr')
|
|
238
|
+
this.superseded = true
|
|
239
|
+
return null
|
|
240
|
+
}
|
|
220
241
|
if (Date.now() >= deadline) {
|
|
221
242
|
this.say('Control did not answer at boot; serving the files this container holds', 'stderr')
|
|
222
243
|
return null
|
|
@@ -229,13 +250,18 @@ class Launcher {
|
|
|
229
250
|
|
|
230
251
|
private async follow(): Promise<void> {
|
|
231
252
|
let backoff = 1_000
|
|
232
|
-
while (!this.stopped) {
|
|
253
|
+
while (!this.stopped && !this.superseded) {
|
|
233
254
|
let answer: DevWaitAnswer
|
|
234
255
|
try {
|
|
235
256
|
answer = await this.wait(true)
|
|
236
257
|
backoff = 1_000
|
|
237
|
-
} catch {
|
|
258
|
+
} catch (error) {
|
|
238
259
|
if (this.stopped) return
|
|
260
|
+
if (error instanceof SupersededError) {
|
|
261
|
+
this.say(SUPERSEDED, 'stderr')
|
|
262
|
+
this.superseded = true
|
|
263
|
+
return
|
|
264
|
+
}
|
|
239
265
|
this.say(`the long poll on Control failed; retrying in ${backoff / 1000} s`, 'stderr')
|
|
240
266
|
await sleep(backoff)
|
|
241
267
|
backoff = Math.min(backoff * 2, 15_000)
|
|
@@ -246,9 +272,19 @@ class Launcher {
|
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
private async wait(hold: boolean): Promise<DevWaitAnswer> {
|
|
249
|
-
const body = encodeDevWaitRequest({
|
|
275
|
+
const body = encodeDevWaitRequest({
|
|
276
|
+
bootstrap: this.config.head,
|
|
277
|
+
head: this.state.head,
|
|
278
|
+
reset: this.state.reset,
|
|
279
|
+
checks: this.state.checks,
|
|
280
|
+
hold,
|
|
281
|
+
})
|
|
250
282
|
const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(DEV_SOURCE_WAIT_MS + 15_000)])
|
|
251
283
|
const response = await this.fetch(this.request(DEV_SOURCE_WAIT_PATH, body, signal))
|
|
284
|
+
if (response.status === 409) {
|
|
285
|
+
await response.body?.cancel()
|
|
286
|
+
throw new SupersededError()
|
|
287
|
+
}
|
|
252
288
|
if (!response.ok) throw new Error(`the source receiver answered ${response.status}`)
|
|
253
289
|
const parsed = parseDevWaitAnswer(await response.json())
|
|
254
290
|
if (!parsed.ok) throw new Error('the source receiver answered a body this release does not read')
|
|
@@ -265,7 +301,9 @@ class Launcher {
|
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
private async report(): Promise<void> {
|
|
304
|
+
if (this.superseded) return
|
|
268
305
|
const body = encodeDevReport({
|
|
306
|
+
bootstrap: this.config.head,
|
|
269
307
|
head: this.state.head,
|
|
270
308
|
status: this.status,
|
|
271
309
|
target: this.target,
|
|
@@ -290,7 +328,7 @@ class Launcher {
|
|
|
290
328
|
if (this.state.checks === null) this.state = { ...this.state, checks: answer.checks }
|
|
291
329
|
let changed = boot
|
|
292
330
|
let migrate = boot
|
|
293
|
-
let
|
|
331
|
+
let spaFailure: string | null = null
|
|
294
332
|
if (answer.sync !== null) {
|
|
295
333
|
const outcome = await this.writeSync(answer.sync)
|
|
296
334
|
if (outcome.kind === 'refused') {
|
|
@@ -302,28 +340,28 @@ class Launcher {
|
|
|
302
340
|
} else {
|
|
303
341
|
changed = true
|
|
304
342
|
migrate ||= outcome.migrations
|
|
305
|
-
|
|
343
|
+
spaFailure = outcome.spaFailure
|
|
306
344
|
}
|
|
307
345
|
}
|
|
308
346
|
const reset = answer.reset > (this.state.reset ?? answer.reset)
|
|
309
|
-
if (changed || reset) await this.converge({ migrate: migrate || reset, reset,
|
|
347
|
+
if (changed || reset) await this.converge({ migrate: migrate || reset, reset, spaFailure, resetTo: answer.reset })
|
|
310
348
|
if (answer.checks > (this.state.checks ?? answer.checks)) await this.check(answer.checks)
|
|
311
349
|
}
|
|
312
350
|
|
|
313
351
|
/** Bring the database, the SPA and the running application to the files on disk, then report. */
|
|
314
|
-
private async converge(steps: { migrate: boolean; reset: boolean;
|
|
352
|
+
private async converge(steps: { migrate: boolean; reset: boolean; spaFailure: string | null; resetTo: number }): Promise<void> {
|
|
315
353
|
this.status = steps.reset ? 'resetting' : 'syncing'
|
|
316
354
|
void this.report()
|
|
317
355
|
let failure: string | null = null
|
|
318
356
|
// A migration's ALTER would queue behind the live version's locks, and a reset drops the role it connects as.
|
|
319
357
|
if (this.provisioningUrl !== null && steps.migrate) await this.stopApplication()
|
|
320
358
|
if (steps.reset) {
|
|
321
|
-
failure = await this.resetDatabase()
|
|
359
|
+
failure = await this.timed('the reset', () => this.resetDatabase())
|
|
322
360
|
this.state = { ...this.state, reset: steps.resetTo }
|
|
323
361
|
}
|
|
324
|
-
if (failure === null && steps.migrate) failure = await this.migrate()
|
|
325
|
-
if (failure === null
|
|
326
|
-
const served = await this.restartApplication()
|
|
362
|
+
if (failure === null && steps.migrate) failure = await this.timed('the migration', () => this.migrate())
|
|
363
|
+
if (failure === null) failure = steps.spaFailure
|
|
364
|
+
const served = await this.timed('the restart until healthy', () => this.restartApplication())
|
|
327
365
|
failure ??= served
|
|
328
366
|
this.status = failure === null ? 'serving' : 'failed'
|
|
329
367
|
this.target = null
|
|
@@ -332,17 +370,44 @@ class Launcher {
|
|
|
332
370
|
await this.report()
|
|
333
371
|
}
|
|
334
372
|
|
|
373
|
+
/** Run one step of a sync and log how long it took: what a slow `apply_changes` is read from. */
|
|
374
|
+
private async timed(step: string, run: () => Promise<string | null>): Promise<string | null> {
|
|
375
|
+
const started = performance.now()
|
|
376
|
+
const failure = await run()
|
|
377
|
+
this.say(`${step} ${failure === null ? 'took' : 'failed after'} ${Math.round(performance.now() - started)} ms`)
|
|
378
|
+
return failure
|
|
379
|
+
}
|
|
380
|
+
|
|
335
381
|
private async writeSync(sync: DevSync): Promise<SyncOutcome> {
|
|
336
382
|
if (sync.from !== this.state.head) {
|
|
337
383
|
return { kind: 'refused', reason: `Control sent changes from ${sync.from}, and this container holds ${this.state.head}; call bind_dev again` }
|
|
338
384
|
}
|
|
339
385
|
const paths = sync.files.map((file) => file.path)
|
|
340
|
-
const
|
|
341
|
-
const before: SourceReader = (path) => this.readLocal(path)
|
|
342
|
-
const after: SourceReader = (path) => incoming.has(path) ? incoming.get(path) ?? null : this.readLocal(path)
|
|
343
|
-
const reason = bootstrapReason(paths, before, after)
|
|
386
|
+
const reason = bootstrapReason(paths)
|
|
344
387
|
if (reason !== null) return { kind: 'refused', reason }
|
|
345
|
-
|
|
388
|
+
const previous = await Promise.all(
|
|
389
|
+
sync.files.map(async (file) => ({ path: file.path, content: await readFile(join(this.root, file.path)).catch(() => null) })),
|
|
390
|
+
)
|
|
391
|
+
await this.writeFiles(sync.files)
|
|
392
|
+
const web = this.config.spaBuild.length > 0 && webSourceChanged(paths)
|
|
393
|
+
// The SPA build runs beside the digest: a refusal is rare, and then the build runs again over the restored files.
|
|
394
|
+
const [changedContract, spaFailure] = await Promise.all([this.contractChange(), web ? this.spaBuild() : null])
|
|
395
|
+
if (changedContract !== null) {
|
|
396
|
+
await this.writeFiles(previous)
|
|
397
|
+
if (web) await this.spaBuild()
|
|
398
|
+
return { kind: 'refused', reason: changedContract }
|
|
399
|
+
}
|
|
400
|
+
this.state = { ...this.state, head: sync.to }
|
|
401
|
+
this.say(`synced ${sync.files.length} files to ${sync.to}`)
|
|
402
|
+
return { kind: 'applied', migrations: migrationsChanged(paths), spaFailure }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private spaBuild(): Promise<string | null> {
|
|
406
|
+
return this.timed('the SPA build', () => this.buildSpa())
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async writeFiles(files: readonly { readonly path: string; readonly content: string | Uint8Array | null }[]): Promise<void> {
|
|
410
|
+
for (const file of files) {
|
|
346
411
|
const path = join(this.root, file.path)
|
|
347
412
|
if (file.content === null) {
|
|
348
413
|
await rm(path, { force: true })
|
|
@@ -352,19 +417,37 @@ class Launcher {
|
|
|
352
417
|
await writeFile(`${path}.fabrika-dev`, file.content)
|
|
353
418
|
await rename(`${path}.fabrika-dev`, path)
|
|
354
419
|
}
|
|
355
|
-
this.state = { ...this.state, head: sync.to }
|
|
356
|
-
this.say(`synced ${sync.files.length} files to ${sync.to}`)
|
|
357
|
-
return { kind: 'applied', migrations: migrationsChanged(paths), web: this.config.spaBuild.length > 0 && webSourceChanged(paths) }
|
|
358
420
|
}
|
|
359
421
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Why the files on disk need a bootstrap run because their contract digest is not the one the bootstrap run deployed,
|
|
424
|
+
* or null. A child computes it: it loads the application module, which the launcher never does, with no credential.
|
|
425
|
+
* A digest it cannot compute is no reason: the respawned application then fails to boot and reports why.
|
|
426
|
+
*/
|
|
427
|
+
private async contractChange(): Promise<string | null> {
|
|
428
|
+
const deployed = this.options.environment[FABRIKA_CONTRACT_DIGEST] ?? ''
|
|
429
|
+
if (deployed === '') return null
|
|
430
|
+
const started = performance.now()
|
|
431
|
+
const child = Bun.spawn(['bun', CONTRACT_DIGEST_CHILD], {
|
|
432
|
+
cwd: this.root,
|
|
433
|
+
env: { PATH: this.options.environment['PATH'] ?? '', HOME: this.options.environment['HOME'] ?? '' },
|
|
434
|
+
stdin: 'ignore',
|
|
435
|
+
stdout: 'pipe',
|
|
436
|
+
stderr: 'pipe',
|
|
437
|
+
})
|
|
438
|
+
const timer = setTimeout(() => child.kill('SIGKILL'), DIGEST_TIMEOUT_MS)
|
|
439
|
+
const [stdout, stderr] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited])
|
|
440
|
+
clearTimeout(timer)
|
|
441
|
+
const digest = stdout.trim()
|
|
442
|
+
if (child.exitCode !== 0 || !CONTRACT_DIGEST.test(digest)) {
|
|
443
|
+
for (const line of stderr.split('\n').filter((text) => text.trim() !== '').slice(-5)) this.ring.push('stderr', line)
|
|
444
|
+
this.say('the contract digest could not be computed; the application is respawned and reports why', 'stderr')
|
|
366
445
|
return null
|
|
367
446
|
}
|
|
447
|
+
this.say(`contract digest computed in ${Math.round(performance.now() - started)} ms`)
|
|
448
|
+
if (digest === deployed) return null
|
|
449
|
+
return `the change alters the contract (its digest is ${digest}, and the bootstrap run deployed ${deployed}): the proxy table and `
|
|
450
|
+
+ 'the approval follow the contract, so that needs a bootstrap run: call bind_dev again'
|
|
368
451
|
}
|
|
369
452
|
|
|
370
453
|
private async persist(): Promise<void> {
|
|
@@ -413,15 +496,17 @@ class Launcher {
|
|
|
413
496
|
const results: DevCheckResult[] = []
|
|
414
497
|
for (const check of this.config.checks) {
|
|
415
498
|
this.say(`check ${check.name}: ${check.command}`)
|
|
499
|
+
const started = performance.now()
|
|
416
500
|
const result = await this.run(['sh', '-c', check.command], this.commandEnvironment, CHECK_TIMEOUT_MS)
|
|
417
501
|
results.push({
|
|
418
502
|
name: check.name,
|
|
419
503
|
ok: result.exitCode === 0,
|
|
420
504
|
exitCode: result.exitCode,
|
|
505
|
+
durationMs: Math.round(performance.now() - started),
|
|
421
506
|
output: result.lines.slice(-CHECK_OUTPUT_LINES).map((line) => line.slice(0, 2_000)),
|
|
422
507
|
})
|
|
423
508
|
}
|
|
424
|
-
this.checkResults = { request, results }
|
|
509
|
+
this.checkResults = { request, head: this.state.head, results }
|
|
425
510
|
this.state = { ...this.state, checks: request }
|
|
426
511
|
this.status = before.status
|
|
427
512
|
this.message = before.message
|
package/src/dev/wire.ts
CHANGED
|
@@ -32,7 +32,14 @@ export function isSourcePath(path: string): boolean {
|
|
|
32
32
|
|
|
33
33
|
const sourcePath = z.string().refine(isSourcePath, 'not a repository-relative path')
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The commit the container's bootstrap run built (`DevLauncherConfig.head`). Control answers only the container of the
|
|
37
|
+
* newest bootstrap run: a container a newer bootstrap replaces still runs for a while, and must not sync or report.
|
|
38
|
+
*/
|
|
39
|
+
const bootstrap = commitSha
|
|
40
|
+
|
|
35
41
|
export const devWaitRequestSchema = z.object({
|
|
42
|
+
bootstrap,
|
|
36
43
|
/** The commit whose files the container holds now. */
|
|
37
44
|
head: commitSha,
|
|
38
45
|
/** The reset generation it applied last; null for a container with no record of one, which adopts the answer's. */
|
|
@@ -81,6 +88,7 @@ export const devCheckResultSchema = z.object({
|
|
|
81
88
|
name: z.string().min(1).max(64),
|
|
82
89
|
ok: z.boolean(),
|
|
83
90
|
exitCode: z.number().int().nullable(),
|
|
91
|
+
durationMs: z.number().int().min(0),
|
|
84
92
|
/** The last lines of its output. */
|
|
85
93
|
output: z.array(z.string().max(DEV_REPORT_MAX_LINE_CHARACTERS)).max(DEV_REPORT_MAX_LINES),
|
|
86
94
|
}).strict()
|
|
@@ -88,6 +96,7 @@ export const devCheckResultSchema = z.object({
|
|
|
88
96
|
export type DevCheckResult = z.infer<typeof devCheckResultSchema>
|
|
89
97
|
|
|
90
98
|
export const devReportSchema = z.object({
|
|
99
|
+
bootstrap,
|
|
91
100
|
/** The commit whose files the container holds now. */
|
|
92
101
|
head: commitSha,
|
|
93
102
|
status: z.enum(DEV_STATUSES),
|
|
@@ -98,7 +107,8 @@ export const devReportSchema = z.object({
|
|
|
98
107
|
/** The newest lines of the application's and the launcher's output, oldest first. */
|
|
99
108
|
log: z.array(devLogLineSchema).max(DEV_REPORT_MAX_LINES),
|
|
100
109
|
reset: generation.nullable(),
|
|
101
|
-
|
|
110
|
+
/** The newest check run: the request it answers, the commit whose files it checked, and each check's result. */
|
|
111
|
+
checks: z.object({ request: generation, head: commitSha, results: z.array(devCheckResultSchema).max(8) }).strict().nullable(),
|
|
102
112
|
}).strict()
|
|
103
113
|
|
|
104
114
|
export type DevReport = z.infer<typeof devReportSchema>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const APP_AGENT_SKILL_NAME = "fabrika-app";
|
|
2
|
+
export interface AppAgentSkillReference {
|
|
3
|
+
/** The file's path in the skill directory, `references/<topic>.md`: the path `SKILL.md` names it by. */
|
|
4
|
+
readonly path: string;
|
|
5
|
+
/** The topic `get_app_rules` answers it under: its file name without `.md`. */
|
|
6
|
+
readonly topic: string;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
}
|
|
9
|
+
export interface AppAgentSkill {
|
|
10
|
+
/** The `version` of the frontmatter: the version of the `@fabrika/app` that ships it. */
|
|
11
|
+
readonly version: string;
|
|
12
|
+
/** The whole `SKILL.md`, frontmatter included: what a skill directory holds. */
|
|
13
|
+
readonly text: string;
|
|
14
|
+
/** `SKILL.md` without the frontmatter. */
|
|
15
|
+
readonly body: string;
|
|
16
|
+
/** The reference files, by path. */
|
|
17
|
+
readonly references: readonly AppAgentSkillReference[];
|
|
18
|
+
}
|
|
19
|
+
/** The skill of this package. */
|
|
20
|
+
export declare function readAppAgentSkill(): AppAgentSkill;
|
|
21
|
+
/** The skill in `skillDir`, the `skill/` directory of an installed `@fabrika/app`. */
|
|
22
|
+
export declare function readAppAgentSkillAt(skillDir: string): AppAgentSkill;
|
|
23
|
+
/**
|
|
24
|
+
* The skill of the `@fabrika/app` an application installed: the nearest `node_modules/@fabrika/app` from `appRoot`
|
|
25
|
+
* up, as Bun resolves the package. `null` when the application has not installed it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function installedAppAgentSkill(appRoot: string): AppAgentSkill | null;
|
|
28
|
+
/** The `AGENTS.md` of an application repository: the skill's body, then each reference file. */
|
|
29
|
+
export declare function renderAgentsFile(skill: AppAgentSkill): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** An application root with this file is a contract application. The paths equal `@fabrika/check`'s layout. */
|
|
2
|
+
export declare const CONTRACT_FILE = "fabrika.contract.ts";
|
|
3
|
+
/** Relative to the application root. */
|
|
4
|
+
export declare const GENERATED_ENTRY_FILE = "fabrika/entry.generated.ts";
|
|
5
|
+
/** The module whose default export is `serve(contract, …)`, relative to the application root. */
|
|
6
|
+
export declare const IMPLEMENTATION_FILE = "src/app.ts";
|
|
7
|
+
export type EntryRuntime = 'bun' | 'cloudflare';
|
|
8
|
+
/** The entrypoint source for one runtime. */
|
|
9
|
+
export declare function generatedEntry(runtime: EntryRuntime): string;
|
|
10
|
+
/** Writes the entrypoint under `root` and returns its path. */
|
|
11
|
+
export declare function writeGeneratedEntry(root: string, runtime: EntryRuntime): Promise<string>;
|