@frontera-sdk/cli 1.52.38 → 1.52.40

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.
Files changed (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
@@ -0,0 +1,775 @@
1
+ import { posix } from 'node:path'
2
+
3
+ import { fromOutsidePath } from '../function-outside-imports'
4
+
5
+ /**
6
+ * Modules a pulled Function imports that its source archive lacks, recovered
7
+ * from the bundle the version runs.
8
+ *
9
+ * A Function deployed from its folder before deploys kept the files it
10
+ * imports from beside it (`../../shared/eligibility`) has a source archive
11
+ * without them, so the pull cannot build it. Its bundle still holds their
12
+ * code: Bun scope-hoists every module into one file and marks where each
13
+ * begins with its path (`// ../shared/eligibility.ts`, relative to wherever
14
+ * the author built it). This cuts those modules back out, as JavaScript, at
15
+ * the path the import expects.
16
+ *
17
+ * What comes back is the bundled JavaScript, not the original TypeScript:
18
+ * types are gone, and so is anything the Function did not use (the bundler
19
+ * drops it). Each module is written as `<path>.js` beside a `<path>.d.ts`
20
+ * typing every export `any`: Bun picks the `.js`, so the Function builds and
21
+ * runs; TypeScript picks the `.d.ts` and never checks the `.js`. A `.ts` with
22
+ * `// @ts-nocheck` was the alternative, and it typechecks worse: a parameter
23
+ * the source marked optional (`opts?: …`) comes back required, and an
24
+ * object's inferred shape is narrower than its declared type.
25
+ *
26
+ * Neither gives the types back. A caller that leaned on them (an unannotated
27
+ * callback over a recovered function's result, an arrow assigned to a
28
+ * recovered function type) fails `noImplicitAny` until the original source
29
+ * replaces the recovered files; while the files are as pulled, `local check`
30
+ * counts those errors as inherited.
31
+ *
32
+ * A module is left out, with the reason, when the bundle cannot give it back
33
+ * whole: no module markers (built minified, or by another tool), a name an
34
+ * importer needs that the bundler inlined or dropped (an enum's members are
35
+ * inlined where they are used), code wrapped as CommonJS, or a name it uses
36
+ * from a package or a file the project does not hold.
37
+ */
38
+
39
+ /** One top-level statement of a bundle, with the names it declares at the top level. */
40
+ export interface BundleStatement {
41
+ text: string
42
+ declares: string[]
43
+ }
44
+
45
+ export interface BundleSection {
46
+ /** The path Bun marked it with, relative to where the bundle was built. */
47
+ marker: string
48
+ statements: BundleStatement[]
49
+ text: string
50
+ }
51
+
52
+ export interface ParsedBundle {
53
+ /** The bundler's own helpers (`__export`, `__toESM`, …) and hoisted imports, before the first module. */
54
+ prelude: BundleStatement[]
55
+ sections: BundleSection[]
56
+ }
57
+
58
+ /** A recovered module from one or more bundles, before it is written as files. */
59
+ export interface RecoveredModule {
60
+ /** Project path without extension: `functions/shared/staff-eligibility` (or `…/util/index`). */
61
+ path: string
62
+ /** `staff-availability@v3`: the bundles it came from. */
63
+ from: string[]
64
+ statements: BundleStatement[]
65
+ /** Export name → the bundle's local name. */
66
+ exports: Map<string, string>
67
+ /** Names an importer uses only as types: declared `any`. */
68
+ types: Set<string>
69
+ /** Local name → where it comes from (another recovered module, or a project file), by project path without extension. */
70
+ imports: Map<string, { from: string; name: string }>
71
+ }
72
+
73
+ export interface RecoveryResult {
74
+ modules: RecoveredModule[]
75
+ /** Project path without extension → why it could not be recovered. */
76
+ failed: Record<string, string>
77
+ /**
78
+ * What a test file imports that the bundle does not hold (it is built from
79
+ * the entry, not the tests), by recovered module: the Function still builds
80
+ * and runs, and that test needs the original source.
81
+ */
82
+ testOnly: Record<string, string[]>
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Tokens: enough of JavaScript to find top-level statements and what they declare.
87
+
88
+ interface Tok {
89
+ type: 'id' | 'num' | 'str' | 'punct' | 'regex' | 'tpl'
90
+ value: string
91
+ start: number
92
+ end: number
93
+ line: number
94
+ /** The first token on its line, at column 0. */
95
+ col0: boolean
96
+ }
97
+
98
+ const REGEX_AFTER = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void', 'throw', 'case', 'do', 'else', 'yield', 'await'])
99
+ const PUNCT = ['>>>=', '...', '===', '!==', '**=', '<<=', '>>=', '>>>', '&&=', '||=', '??=', '=>', '==', '!=', '<=', '>=', '&&', '||', '??', '?.', '++', '--', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '**', '<<', '>>']
100
+ const ID_START = /[A-Za-z_$\u0080-￿]/
101
+ const ID_PART = /[\w$\u0080-￿]/
102
+
103
+ export function tokenize(src: string): Tok[] {
104
+ const toks: Tok[] = []
105
+ // Brace depth at which each open `${` began, so its `}` resumes the template.
106
+ const tpl: number[] = []
107
+ let braces = 0
108
+ let line = 0
109
+ let lineStart = true
110
+ let col0 = true
111
+ let i = 0
112
+ const push = (type: Tok['type'], start: number, end: number) => {
113
+ toks.push({ type, value: src.slice(start, end), start, end, line, col0: lineStart && col0 })
114
+ lineStart = false
115
+ }
116
+ const template = (from: number): number => {
117
+ // From just after '`' or a resuming '}': to the closing '`' (returns after it) or a `${`.
118
+ let j = from
119
+ while (j < src.length) {
120
+ const c = src[j]!
121
+ if (c === '\\') { j += 2; continue }
122
+ if (c === '\n') line++
123
+ if (c === '`') return j + 1
124
+ if (c === '$' && src[j + 1] === '{') {
125
+ tpl.push(braces)
126
+ braces++
127
+ return j + 2
128
+ }
129
+ j++
130
+ }
131
+ return j
132
+ }
133
+ while (i < src.length) {
134
+ const c = src[i]!
135
+ if (c === '\n') { line++; lineStart = true; col0 = true; i++; continue }
136
+ if (c === ' ' || c === '\t' || c === '\r') { if (lineStart) col0 = false; i++; continue }
137
+ if (c === '/' && src[i + 1] === '/') { while (i < src.length && src[i] !== '\n') i++; continue }
138
+ if (c === '/' && src[i + 1] === '*') {
139
+ const end = src.indexOf('*/', i + 2)
140
+ const stop = end < 0 ? src.length : end + 2
141
+ for (let k = i; k < stop; k++) if (src[k] === '\n') line++
142
+ i = stop
143
+ continue
144
+ }
145
+ const start = i
146
+ if (c === '"' || c === "'") {
147
+ i++
148
+ while (i < src.length && src[i] !== c && src[i] !== '\n') i += src[i] === '\\' ? 2 : 1
149
+ i++
150
+ push('str', start, i)
151
+ continue
152
+ }
153
+ if (c === '`') {
154
+ i = template(i + 1)
155
+ push('tpl', start, i)
156
+ continue
157
+ }
158
+ if (c === '}' && tpl.length && braces - 1 === tpl[tpl.length - 1]) {
159
+ tpl.pop()
160
+ braces--
161
+ i = template(i + 1)
162
+ push('tpl', start, i)
163
+ continue
164
+ }
165
+ if (ID_START.test(c) || (c === '#' && ID_START.test(src[i + 1] ?? ''))) {
166
+ i++
167
+ while (i < src.length && ID_PART.test(src[i]!)) i++
168
+ push('id', start, i)
169
+ continue
170
+ }
171
+ if (/\d/.test(c) || (c === '.' && /\d/.test(src[i + 1] ?? ''))) {
172
+ i++
173
+ while (i < src.length && (/[\w.]/.test(src[i]!) || ((src[i] === '+' || src[i] === '-') && /[eE]/.test(src[i - 1]!) && !/^0[xX]/.test(src.slice(start, i))))) i++
174
+ push('num', start, i)
175
+ continue
176
+ }
177
+ if (c === '/') {
178
+ const prev = toks[toks.length - 1]
179
+ const regex = !prev || (prev.type === 'punct' && ![')', ']', '}'].includes(prev.value)) || (prev.type === 'id' && REGEX_AFTER.has(prev.value))
180
+ if (regex) {
181
+ i++
182
+ let cls = false
183
+ while (i < src.length && src[i] !== '\n') {
184
+ const r = src[i]!
185
+ if (r === '\\') { i += 2; continue }
186
+ if (r === '[') cls = true
187
+ else if (r === ']') cls = false
188
+ else if (r === '/' && !cls) break
189
+ i++
190
+ }
191
+ i++
192
+ while (i < src.length && ID_PART.test(src[i]!)) i++
193
+ push('regex', start, i)
194
+ continue
195
+ }
196
+ }
197
+ const multi = PUNCT.find((p) => src.startsWith(p, i) && !(p === '?.' && /\d/.test(src[i + 2] ?? '')))
198
+ i += multi ? multi.length : 1
199
+ if (c === '{') braces++
200
+ else if (c === '}') braces--
201
+ push('punct', start, i)
202
+ }
203
+ return toks
204
+ }
205
+
206
+ const OPEN = new Set(['(', '[', '{'])
207
+ const CLOSE = new Set([')', ']', '}'])
208
+ /** After a `}` at the top level, a statement goes on only with one of these at the start of the next line. */
209
+ const CONTINUES = new Set([';', ')', ']', '}', ',', '.', '?.', ':', '?', '=', '=>', '&&', '||', '??', '+', '-', '*', '/', '%'])
210
+
211
+ /** The top-level statements of a piece of bundled JavaScript. */
212
+ export function statementsOf(src: string): BundleStatement[] {
213
+ const toks = tokenize(src)
214
+ const out: BundleStatement[] = []
215
+ let depth = 0
216
+ let begin = 0
217
+ const close = (end: number) => {
218
+ const part = toks.slice(begin, end + 1)
219
+ if (part.length) out.push({ text: src.slice(part[0]!.start, part[part.length - 1]!.end), declares: declaredBy(part) })
220
+ begin = end + 1
221
+ }
222
+ for (let k = 0; k < toks.length; k++) {
223
+ const t = toks[k]!
224
+ if (t.type === 'punct' && OPEN.has(t.value)) depth++
225
+ else if (t.type === 'punct' && CLOSE.has(t.value)) depth--
226
+ else if (t.type === 'tpl' && t.value.endsWith('${')) depth++
227
+ if (t.type === 'tpl' && t.value.startsWith('}')) depth--
228
+ if (depth !== 0) continue
229
+ if (t.type === 'punct' && t.value === ';') close(k)
230
+ else if (t.type === 'punct' && t.value === '}') {
231
+ const next = toks[k + 1]
232
+ if (!next || (next.line > t.line && next.col0 && !(next.type === 'punct' && CONTINUES.has(next.value)))) close(k)
233
+ }
234
+ }
235
+ if (begin < toks.length) close(toks.length - 1)
236
+ return out
237
+ }
238
+
239
+ /** The names a top-level statement declares. */
240
+ function declaredBy(toks: Tok[]): string[] {
241
+ const first = toks[0]
242
+ if (!first || first.type !== 'id') return []
243
+ const at = (k: number) => toks[k]
244
+ if (first.value === 'import' && at(1)?.value !== '(' && at(1)?.value !== '.') {
245
+ const names: string[] = []
246
+ let entry: string[] = []
247
+ const flush = () => {
248
+ if (entry.length && entry[0] !== 'type') names.push(entry[entry.length - 1]!)
249
+ entry = []
250
+ }
251
+ for (let k = 1; k < toks.length; k++) {
252
+ const t = toks[k]!
253
+ if (t.type === 'str' || (t.type === 'id' && t.value === 'from')) break
254
+ if (t.type === 'id') entry.push(t.value)
255
+ else if (t.value === ',' || t.value === '{' || t.value === '}') flush()
256
+ }
257
+ flush()
258
+ return names.filter((n) => n !== 'as' && n !== '*')
259
+ }
260
+ let k = 0
261
+ if (first.value === 'async' && at(1)?.value === 'function') k = 1
262
+ if (at(k)?.value === 'function') {
263
+ const name = at(k + 1)?.value === '*' ? at(k + 2) : at(k + 1)
264
+ return name?.type === 'id' ? [name.value] : []
265
+ }
266
+ if (first.value === 'class') return at(1)?.type === 'id' ? [at(1)!.value] : []
267
+ if (first.value !== 'var' && first.value !== 'let' && first.value !== 'const') return []
268
+ // Declarators: a name or a pattern, each at the top of the statement, separated by commas.
269
+ const names: string[] = []
270
+ let depth = 0
271
+ let expectBinding = true
272
+ for (let k2 = 1; k2 < toks.length; k2++) {
273
+ const t = toks[k2]!
274
+ if (expectBinding && depth === 0) {
275
+ if (t.type === 'id') { names.push(t.value); expectBinding = false; continue }
276
+ if (t.value === '{' || t.value === '[') {
277
+ // A destructuring pattern: its bindings are the names not followed by `:` and not after `=`.
278
+ let d = 0
279
+ let j = k2
280
+ for (; j < toks.length; j++) {
281
+ const p = toks[j]!
282
+ if (OPEN.has(p.value)) d++
283
+ else if (CLOSE.has(p.value)) { d--; if (d === 0) break }
284
+ else if (p.type === 'id' && toks[j + 1]?.value !== ':' && toks[j - 1]?.value !== '=' && toks[j - 1]?.value !== '.' ) names.push(p.value)
285
+ }
286
+ k2 = j
287
+ expectBinding = false
288
+ continue
289
+ }
290
+ }
291
+ if (t.type === 'punct' && OPEN.has(t.value)) depth++
292
+ else if (t.type === 'punct' && CLOSE.has(t.value)) depth--
293
+ else if (t.type === 'tpl' && t.value.endsWith('${')) depth++
294
+ if (t.type === 'tpl' && t.value.startsWith('}')) depth--
295
+ if (depth === 0 && t.value === ',') expectBinding = true
296
+ }
297
+ return names
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // The bundle.
302
+
303
+ /** `// ../shared/eligibility.ts`: where Bun says a module begins. Its own `// @bun` header is not one. */
304
+ const MARKER = /^\/\/ ([^@\s][^\n]*\.(?:[cm]?[jt]sx?|json))$/
305
+
306
+ export function parseBundle(bundle: string): ParsedBundle {
307
+ const lines = bundle.split('\n')
308
+ const prelude: string[] = []
309
+ const sections: { marker: string; lines: string[] }[] = []
310
+ for (const line of lines) {
311
+ const m = MARKER.exec(line)
312
+ if (m) sections.push({ marker: m[1]!, lines: [] })
313
+ else if (sections.length) sections[sections.length - 1]!.lines.push(line)
314
+ else prelude.push(line)
315
+ }
316
+ return {
317
+ prelude: statementsOf(prelude.join('\n')),
318
+ sections: sections.map((s) => {
319
+ const text = s.lines.join('\n')
320
+ return { marker: s.marker, text, statements: statementsOf(text) }
321
+ }),
322
+ }
323
+ }
324
+
325
+ const PROBE = '__frontera_probe__'
326
+
327
+ /**
328
+ * Which of `names` a piece of JavaScript uses as free variables: Bun's own
329
+ * parser decides, by importing them all and keeping only those the code
330
+ * reads (`trimUnusedImports`). A parameter or local of the same name shadows
331
+ * a top-level one in another module, and only a real scope analysis can tell
332
+ * the two apart.
333
+ */
334
+ export function freeNames(code: string, names: Iterable<string>, loader: 'js' | 'ts' = 'js'): Set<string> {
335
+ const list = [...new Set(names)].filter((n) => /^[A-Za-z_$][\w$]*$/.test(n))
336
+ if (!list.length) return new Set()
337
+ const out = new Bun.Transpiler({ loader, trimUnusedImports: true }).transformSync(`import { ${list.join(', ')} } from "${PROBE}";\n${code}`)
338
+ const m = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*"${PROBE}"`).exec(out)
339
+ return new Set(m ? m[1]!.split(',').map((s) => s.trim().split(/\s+as\s+/)[0]!).filter(Boolean) : [])
340
+ }
341
+
342
+ /** An import or re-export in a source file: what it names, from where. */
343
+ interface ImportUse {
344
+ specifier: string
345
+ /** Imported names (`default` for a default import); `*` for a namespace, with its local name. */
346
+ names: { imported: string; local: string; type: boolean }[]
347
+ namespace?: string
348
+ /** `export * from`. */
349
+ all?: boolean
350
+ }
351
+
352
+ /** The import and re-export statements of TypeScript source, with each name and whether it is type-only. */
353
+ export function importsOf(source: string): ImportUse[] {
354
+ const out: ImportUse[] = []
355
+ const re = /\b(import|export)\s+(type\s+)?([\w$*{}\s,]*?)\s*from\s*['"]([^'"]+)['"]/g
356
+ for (const m of source.matchAll(re)) {
357
+ const typeAll = !!m[2]
358
+ const clause = m[3]!.trim()
359
+ const use: ImportUse = { specifier: m[4]!, names: [] }
360
+ if (m[1] === 'export' && clause === '*') use.all = true
361
+ const ns = /\*\s*as\s+([\w$]+)/.exec(clause)
362
+ if (ns) use.namespace = ns[1]!
363
+ const braces = /\{([^}]*)\}/.exec(clause)
364
+ const before = clause.replace(/\{[^}]*\}/, '').replace(/\*\s*as\s+[\w$]+/, '').replace(/,/g, ' ').trim()
365
+ if (m[1] === 'import' && before && /^[\w$]+$/.test(before)) use.names.push({ imported: 'default', local: before, type: typeAll })
366
+ for (const part of braces ? braces[1]!.split(',') : []) {
367
+ const p = part.trim()
368
+ if (!p) continue
369
+ const type = typeAll || /^type\s+/.test(p)
370
+ const [imported, local] = p.replace(/^type\s+/, '').split(/\s+as\s+/).map((s) => s.trim())
371
+ use.names.push({ imported: imported!, local: local ?? imported!, type })
372
+ }
373
+ out.push(use)
374
+ }
375
+ return out
376
+ }
377
+
378
+ /** The names an import of `specifier` in TypeScript `source` uses as values (the rest are types, or unused). */
379
+ function valueImports(source: string, specifier: string): Set<string> {
380
+ let js: string
381
+ try {
382
+ js = new Bun.Transpiler({ loader: 'ts', trimUnusedImports: true }).transformSync(source)
383
+ } catch {
384
+ return new Set()
385
+ }
386
+ const out = new Set<string>()
387
+ for (const use of importsOf(js)) {
388
+ if (use.specifier !== specifier) continue
389
+ for (const n of use.names) out.add(n.imported)
390
+ if (use.namespace) out.add('*')
391
+ }
392
+ // `export … from` is a value re-export the transpiler keeps as is.
393
+ return out
394
+ }
395
+
396
+ /** Bun's name for a module's anonymous default export: `<file name>_default`. */
397
+ function defaultName(path: string): string {
398
+ const base = posix.basename(path).replace(/\.[^.]+$/, '')
399
+ const name = (base === 'index' ? posix.basename(posix.dirname(path)) : base).replace(/[^\w$]/g, '_')
400
+ return `${/^\d/.test(name) ? `_${name}` : name}_default`
401
+ }
402
+
403
+ /** `level` in a module whose bundle renamed it `level2` to avoid another module's `level`. */
404
+ function bundleNameIn(declared: ReadonlySet<string>, name: string): string | null {
405
+ if (declared.has(name)) return name
406
+ const variants = [...declared].filter((d) => d.startsWith(name) && /^\d+$/.test(d.slice(name.length)))
407
+ return variants.length === 1 ? variants[0]! : null
408
+ }
409
+
410
+ const CODE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.js', '.mjs', '.cjs', '/index.ts', '/index.tsx', '/index.js']
411
+
412
+ export interface RecoverInput {
413
+ slug: string
414
+ version: number | string
415
+ bundle: string
416
+ /** The Function's folder in the project: `functions/staff-availability`. */
417
+ folder: string
418
+ /** Its source archive, by archive path (files from beside the folder under `frontera-imports/`). */
419
+ files: ReadonlyMap<string, string>
420
+ /** The archive path of its entry (`src/index.ts`). */
421
+ entry: string
422
+ /** What its code imports that the archive lacks, by path from the folder without extension (`../shared/eligibility`). */
423
+ missing: string[]
424
+ /** Whether a pull may write a recovered module at this project path (without extension). */
425
+ writable: (projectPath: string) => boolean
426
+ /** Source the pull holds from elsewhere (another Function's archive), by project path without extension: imported, never recovered. */
427
+ elsewhere?: ReadonlyMap<string, string>
428
+ }
429
+
430
+ /**
431
+ * Recover the missing modules (and any module they import that the archive
432
+ * also lacks) from one Function's bundle.
433
+ */
434
+ export function recoverFromBundle(input: RecoverInput): RecoveryResult {
435
+ const failed: Record<string, string> = {}
436
+ const project = (fromFolder: string) => posix.normalize(posix.join(input.folder, fromFolder))
437
+ const fail = (paths: string[], why: string): RecoveryResult => {
438
+ for (const p of paths) failed[project(p)] = why
439
+ return { modules: [], failed, testOnly: {} }
440
+ }
441
+ const parsed = parseBundle(input.bundle)
442
+ if (!parsed.sections.length) return fail(input.missing, 'its deployed bundle has no module markers to cut the file back out of (it was built minified, or by another tool)')
443
+
444
+ // Where the bundle was built from, found by the entry's marker: the last module, as Bun orders them.
445
+ const located = locateMarkers(parsed, input.entry)
446
+ if (!located) return fail(input.missing, `its deployed bundle has no module marked as its entry (${input.entry})`)
447
+ const markerOf: (fromFolder: string) => string = located
448
+
449
+ const bySection = new Map(parsed.sections.map((s, i) => [s.marker, i]))
450
+ // Every section's project path (without extension), when it is the Function's own or a file beside it.
451
+ const archiveOf = new Map<number, { project: string; content: string }>()
452
+ for (const [archivePath, content] of input.files) {
453
+ const fromFolder = fromOutsidePath(archivePath) ?? archivePath
454
+ const at = bySection.get(markerOf(fromFolder))
455
+ if (at !== undefined) archiveOf.set(at, { project: project(fromFolder).replace(/\.[^./]+$/, ''), content })
456
+ }
457
+ const sectionFor = (fromFolder: string): { index: number; ext: string } | null => {
458
+ for (const ext of CODE_EXTENSIONS) {
459
+ const at = bySection.get(markerOf(`${fromFolder}${ext}`))
460
+ if (at !== undefined) return { index: at, ext }
461
+ }
462
+ return null
463
+ }
464
+
465
+ const declaredIn = parsed.sections.map((s) => new Set(s.statements.flatMap((st) => st.declares)))
466
+ const preludeBy = new Map<string, number>()
467
+ parsed.prelude.forEach((st, i) => st.declares.forEach((n) => preludeBy.set(n, i)))
468
+ const sectionBy = new Map<string, number>()
469
+ declaredIn.forEach((names, i) => names.forEach((n) => sectionBy.set(n, i)))
470
+
471
+ // The modules to recover, by section; grows with what they import that the archive lacks.
472
+ const recovering = new Map<number, string>()
473
+ const queue: number[] = []
474
+ const want = (index: number, path: string) => {
475
+ if (recovering.has(index)) return
476
+ recovering.set(index, path)
477
+ queue.push(index)
478
+ }
479
+ for (const m of input.missing) {
480
+ const found = sectionFor(m)
481
+ if (!found) { failed[project(m)] = `its deployed bundle holds no module at ${m}${CODE_EXTENSIONS.slice(0, 3).join(', ')}, …`; continue }
482
+ const path = project(m) + (found.ext.startsWith('/index') ? '/index' : '')
483
+ if (!input.writable(path)) { failed[project(m)] = 'a pull doesn’t write into that folder'; continue }
484
+ want(found.index, path)
485
+ }
486
+
487
+ const modules = new Map<number, RecoveredModule>()
488
+ const reasons = new Map<number, string>()
489
+ const label = `${input.slug}@v${String(input.version).replace(/^v/, '')}`
490
+ while (queue.length) {
491
+ const index = queue.shift()!
492
+ try {
493
+ recoverSection(index)
494
+ } catch (err) {
495
+ reasons.set(index, `its code in the deployed bundle couldn’t be read back (${(err as Error).message.split('\n')[0]})`)
496
+ }
497
+ }
498
+ function recoverSection(index: number): void {
499
+ const section = parsed.sections[index]!
500
+ const path = recovering.get(index)!
501
+ const own = declaredIn[index]!
502
+ const wrapped = [...own].find((n) => /^(require|init)_/.test(n) && /__(commonJS|esm)\(/.test(section.text))
503
+ if (wrapped) { reasons.set(index, 'its deployed bundle holds it wrapped as a CommonJS module, which can’t be cut back out'); return }
504
+ const mod: RecoveredModule = { path, from: [label], statements: [], exports: new Map(), types: new Set(), imports: new Map() }
505
+ // The bundler's helpers it uses, and those they use, in the bundle's order.
506
+ const helpers = new Set<number>()
507
+ const pull = (code: string) => {
508
+ for (const n of freeNames(code, [...preludeBy.keys()].filter((k) => code.includes(k)))) {
509
+ const at = preludeBy.get(n)!
510
+ if (helpers.has(at)) continue
511
+ helpers.add(at)
512
+ pull(parsed.prelude[at]!.text)
513
+ }
514
+ }
515
+ pull(section.text)
516
+ mod.statements.push(...[...helpers].sort((a, b) => a - b).map((at) => parsed.prelude[at]!))
517
+ mod.statements.push(...section.statements)
518
+ const local = new Set([...own, ...[...helpers].flatMap((at) => parsed.prelude[at]!.declares)])
519
+ // What it uses from the other modules of the bundle.
520
+ const candidates = [...sectionBy.keys()].filter((n) => !local.has(n) && sectionBy.get(n) !== index && new RegExp(`(^|[^\\w$.])${n.replace(/\$/g, '\\$')}(?![\\w$])`).test(section.text))
521
+ for (const name of freeNames(section.text, candidates)) {
522
+ const at = sectionBy.get(name)!
523
+ const other = parsed.sections[at]!
524
+ const fromFolderOther = archiveOf.has(at) ? null : folderPathOf(markerOf, other.marker)
525
+ const heldPath = fromFolderOther ? project(fromFolderOther.replace(/\.[^./]+$/, '')) : null
526
+ const held = heldPath ? input.elsewhere?.get(heldPath) : undefined
527
+ const file = archiveOf.get(at) ?? (held !== undefined ? { project: heldPath!, content: held } : undefined)
528
+ if (file) {
529
+ const exported = new Set(new Bun.Transpiler({ loader: 'ts' }).scan(file.content).exports)
530
+ const original = exported.has(name) ? name : name === defaultName(other.marker) && exported.has('default') ? 'default' : [...exported].find((e) => name.startsWith(e) && /^\d+$/.test(name.slice(e.length)))
531
+ if (!original) { reasons.set(index, `it uses \`${name}\` from ${file.project}, which that file doesn’t export`); break }
532
+ mod.imports.set(name, { from: file.project, name: original })
533
+ continue
534
+ }
535
+ if (/(^|\/)node_modules\//.test(other.marker)) { reasons.set(index, `it uses \`${name}\` from the package ${packageOf(other.marker)}, which its deployed bundle holds inlined`); break }
536
+ // Another module from beside the folder that the archive lacks too.
537
+ const otherPath = heldPath
538
+ if (!otherPath || otherPath.startsWith('../') || !input.writable(otherPath)) { reasons.set(index, `it uses \`${name}\` from ${other.marker}, which the project doesn’t hold`); break }
539
+ want(at, otherPath)
540
+ mod.imports.set(name, { from: otherPath, name })
541
+ }
542
+ if (!reasons.has(index)) modules.set(index, mod)
543
+ }
544
+
545
+ // What importers in the archive need of each recovered module.
546
+ const testOnly: Record<string, string[]> = {}
547
+ const byPath = new Map([...modules.values()].map((m) => [m.path, m]))
548
+ const indexOf = new Map([...modules.entries()].map(([i, m]) => [m.path, i]))
549
+ for (const [archivePath, content] of input.files) {
550
+ if (!/\.(ts|tsx|mts|js|mjs)$/.test(archivePath) || archivePath.endsWith('.d.ts')) continue
551
+ const fromFolder = fromOutsidePath(archivePath) ?? archivePath
552
+ for (const use of importsOf(content)) {
553
+ if (!use.specifier.startsWith('.')) continue
554
+ const target = project(posix.join(posix.dirname(fromFolder), use.specifier)).replace(/\.(ts|tsx|js|mjs)$/, '')
555
+ const mod = byPath.get(target) ?? byPath.get(`${target}/index`)
556
+ if (!mod) continue
557
+ const index = indexOf.get(mod.path)!
558
+ if (reasons.has(index)) continue
559
+ const declared = declaredIn[index]!
560
+ const values = valueImports(content, use.specifier)
561
+ const need = (imported: string) => {
562
+ const name = imported === 'default'
563
+ ? bundleNameIn(declared, defaultName(parsed.sections[index]!.marker))
564
+ : bundleNameIn(declared, imported)
565
+ if (name) { mod.exports.set(imported, name); return true }
566
+ // A test imports from the module too, but the bundle was built from the entry: what only a
567
+ // test uses was never in it. That costs the test, not the Function.
568
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(archivePath)) {
569
+ testOnly[mod.path] = [...new Set([...(testOnly[mod.path] ?? []), imported])].sort()
570
+ return true
571
+ }
572
+ reasons.set(index, imported === 'default'
573
+ ? `its default export isn’t named in the deployed bundle (imported by ${posix.normalize(posix.join(input.folder, fromFolder))})`
574
+ : `\`${imported}\` isn’t in the deployed bundle: the bundler inlined or dropped it (an enum’s members and some constants are inlined where they are used), so ${posix.normalize(posix.join(input.folder, fromFolder))} can’t import it`)
575
+ return false
576
+ }
577
+ if (use.all) { for (const n of declared) if (!n.startsWith('__')) mod.exports.set(n, n); continue }
578
+ if (use.namespace && values.has('*')) {
579
+ const listed = namespaceExports(parsed.sections[index]!.text)
580
+ const members = listed ? [...listed.keys()] : [...content.matchAll(new RegExp(`\\b${use.namespace.replace(/\$/g, '\\$')}\\s*\\.\\s*([A-Za-z_$][\\w$]*)`, 'g'))].map((m) => m[1]!)
581
+ for (const n of new Set(members)) if (!need(n)) break
582
+ }
583
+ for (const n of use.names) {
584
+ if (n.type || !values.has(n.imported)) { mod.types.add(n.imported); continue }
585
+ if (!need(n.imported)) break
586
+ }
587
+ }
588
+ }
589
+
590
+ // A module that needs one that failed fails too.
591
+ let changed = true
592
+ while (changed) {
593
+ changed = false
594
+ for (const [index, mod] of modules) {
595
+ if (reasons.has(index)) continue
596
+ const broken = [...mod.imports.values()].find((i) => {
597
+ const at = indexOf.get(i.from)
598
+ return at !== undefined && reasons.has(at)
599
+ })
600
+ if (broken) { reasons.set(index, `it needs ${broken.from}, which couldn’t be recovered`); changed = true }
601
+ }
602
+ }
603
+
604
+ // Each module exports what its recovered siblings use of it.
605
+ for (const [index, mod] of modules) {
606
+ if (reasons.has(index)) continue
607
+ for (const [localName, imp] of mod.imports) {
608
+ const target = byPath.get(imp.from)
609
+ if (!target) continue
610
+ const exported = [...target.exports].find(([, l]) => l === imp.name)
611
+ if (exported) mod.imports.set(localName, { from: imp.from, name: exported[0] })
612
+ else target.exports.set(imp.name, imp.name)
613
+ }
614
+ }
615
+ for (const [index, why] of reasons) failed[recovering.get(index)!.replace(/\/index$/, '')] = why
616
+ const kept = [...modules.entries()].filter(([i]) => !reasons.has(i)).map(([, m]) => m)
617
+ return { modules: kept, failed, testOnly: Object.fromEntries(Object.entries(testOnly).filter(([path]) => kept.some((m) => m.path === path))) }
618
+ }
619
+
620
+ /** `__export(exports_x, { level: () => level2 })`: a module's namespace as the bundle lists it, export → local. */
621
+ function namespaceExports(text: string): Map<string, string> | null {
622
+ const m = /__export\(\s*exports_[\w$]+\s*,\s*\{([\s\S]*?)\}\s*\)/.exec(text)
623
+ if (!m) return null
624
+ const out = new Map<string, string>()
625
+ for (const e of m[1]!.matchAll(/([\w$]+)\s*:\s*\(\)\s*=>\s*([\w$]+)/g)) out.set(e[1]!, e[2]!)
626
+ return out
627
+ }
628
+
629
+ function packageOf(marker: string): string {
630
+ const rest = marker.slice(marker.lastIndexOf('node_modules/') + 'node_modules/'.length).split('/')
631
+ return rest[0]!.startsWith('@') ? `${rest[0]}/${rest[1]}` : rest[0]!
632
+ }
633
+
634
+ /**
635
+ * How the bundle's markers name a file, given its path from the Function's
636
+ * folder: found from the entry's marker. Built in the folder (`src/index.ts`),
637
+ * above it (`functions/x/src/index.ts`, `../../x/src/index.ts`) or inside it
638
+ * (`index.ts`, from `src/`).
639
+ */
640
+ function locateMarkers(parsed: ParsedBundle, entry: string): ((fromFolder: string) => string) | null {
641
+ const markers = parsed.sections.map((s) => s.marker)
642
+ const ordered = [markers[markers.length - 1]!, ...markers.slice(0, -1).reverse()].filter((m) => !/(^|\/)node_modules\//.test(m))
643
+ for (const marker of ordered) {
644
+ if (marker === entry || marker.endsWith(`/${entry}`)) {
645
+ const prefix = marker.slice(0, marker.length - entry.length)
646
+ return (fromFolder) => posix.normalize(`${prefix}${fromFolder}`)
647
+ }
648
+ }
649
+ // Built from inside the folder: the entry's marker is the tail of its path.
650
+ const parts = entry.split('/')
651
+ for (let k = 1; k < parts.length; k++) {
652
+ const tail = parts.slice(k).join('/')
653
+ if (!ordered.includes(tail)) continue
654
+ const inside = parts.slice(0, k).join('/')
655
+ const root = '/r/0/1/2/3/4/5/6/7/8/9'
656
+ return (fromFolder) => posix.relative(posix.join(root, inside), posix.join(root, fromFolder))
657
+ }
658
+ return null
659
+ }
660
+
661
+ /** A marker back to its path from the Function's folder, or null when it is not one `markerOf` can make. */
662
+ function folderPathOf(markerOf: (fromFolder: string) => string, marker: string): string | null {
663
+ // Try the marker's own tail at each depth above the folder: `../shared/x.ts`, `../../lib/x.ts`, …
664
+ const parts = marker.split('/')
665
+ for (let up = 1; up <= 6; up++) {
666
+ for (let k = 0; k < parts.length; k++) {
667
+ const candidate = `${'../'.repeat(up)}${parts.slice(k).join('/')}`
668
+ if (markerOf(candidate) === marker) return candidate
669
+ }
670
+ }
671
+ for (let k = 0; k < parts.length; k++) {
672
+ const candidate = parts.slice(k).join('/')
673
+ if (markerOf(candidate) === marker) return candidate
674
+ }
675
+ return null
676
+ }
677
+
678
+ // ---------------------------------------------------------------------------
679
+ // Merging what several Functions' bundles gave back for one file.
680
+
681
+ /**
682
+ * One module from two bundles: every statement of the first, and each of the
683
+ * second's that declares a name the first lacks, in the second's order. Two
684
+ * Functions that use different parts of a shared file each brought only
685
+ * their part (the bundler drops what a Function does not use), so the file
686
+ * the project keeps is their union. Null when the two cannot be one file: a
687
+ * name both declare and import, or import from different places.
688
+ */
689
+ export function mergeRecovered(a: RecoveredModule, b: RecoveredModule): RecoveredModule | null {
690
+ const statements = [...a.statements]
691
+ const declared = new Set(statements.flatMap((s) => s.declares))
692
+ const texts = new Set(statements.map((s) => s.text))
693
+ let pos = -1
694
+ for (const st of b.statements) {
695
+ const known = st.declares.length ? st.declares.every((n) => declared.has(n)) : texts.has(st.text)
696
+ if (known) {
697
+ const at = statements.findIndex((s) => s.text === st.text || (st.declares.length > 0 && s.declares.some((n) => st.declares.includes(n))))
698
+ if (at > pos) pos = at
699
+ continue
700
+ }
701
+ pos++
702
+ statements.splice(pos, 0, st)
703
+ st.declares.forEach((n) => declared.add(n))
704
+ texts.add(st.text)
705
+ }
706
+ const imports = new Map(a.imports)
707
+ for (const [local, imp] of b.imports) {
708
+ const has = imports.get(local)
709
+ if (has && (has.from !== imp.from || has.name !== imp.name)) return null
710
+ imports.set(local, imp)
711
+ }
712
+ if ([...imports.keys()].some((n) => declared.has(n))) return null
713
+ const exports = new Map(a.exports)
714
+ for (const [name, local] of b.exports) if (!exports.has(name)) exports.set(name, local)
715
+ const types = new Set([...a.types, ...b.types].filter((t) => !exports.has(t)))
716
+ return { path: a.path, from: [...new Set([...a.from, ...b.from])], statements, exports, types, imports }
717
+ }
718
+
719
+ // ---------------------------------------------------------------------------
720
+ // Writing it.
721
+
722
+ /** The first line of a file a pull recovered from a deployed bundle. */
723
+ export const RECOVERED_HEADER = '// Recovered by `frontera local sync pull` from the deployed bundle of'
724
+
725
+ function header(mod: RecoveredModule): string {
726
+ const froms = mod.from.length > 1 ? `${mod.from.slice(0, -1).join(', ')} and ${mod.from[mod.from.length - 1]}` : mod.from[0]
727
+ return [
728
+ `${RECOVERED_HEADER} ${froms}: its source in the workspace doesn’t hold this file.`,
729
+ `// This is bundled JavaScript, not the original: its types and any code ${mod.from.length > 1 ? 'those Functions' : 'the Function'} didn’t use are gone.`,
730
+ '// Replace it (and the .d.ts beside it) with the original source when you have it.',
731
+ ].join('\n')
732
+ }
733
+
734
+ const TYPE_PARAMS = '<_A = any, _B = any, _C = any, _D = any>'
735
+
736
+ /** A class (or a capitalised value, as a class expression is) is a type too: `let e: Escalator`. */
737
+ function usableAsType(mod: RecoveredModule, name: string, local: string): boolean {
738
+ return /^[A-Z]/.test(name) || mod.statements.some((s) => s.declares.includes(local) && /^class\s/.test(s.text))
739
+ }
740
+
741
+ /** The two files for a recovered module, by project path: `<path>.js` and `<path>.d.ts`. */
742
+ export function recoveredFiles(mod: RecoveredModule): Record<string, string> {
743
+ const dir = posix.dirname(mod.path)
744
+ const spec = (to: string) => {
745
+ const rel = posix.relative(dir, to)
746
+ return rel.startsWith('.') ? rel : `./${rel}`
747
+ }
748
+ const bySource = new Map<string, string[]>()
749
+ for (const [local, imp] of [...mod.imports].sort(([a], [b]) => a.localeCompare(b))) {
750
+ const list = bySource.get(imp.from) ?? []
751
+ list.push(imp.name === local ? local : `${imp.name} as ${local}`)
752
+ bySource.set(imp.from, list)
753
+ }
754
+ const imports = [...bySource].map(([from, names]) => `import { ${names.join(', ')} } from '${spec(from)}'`)
755
+ const exported = [...mod.exports].sort(([a], [b]) => a.localeCompare(b))
756
+ const js = [
757
+ header(mod),
758
+ ...imports,
759
+ '',
760
+ ...mod.statements.map((s) => s.text),
761
+ '',
762
+ exported.length ? `export { ${exported.map(([name, local]) => (name === local ? name : `${local} as ${name}`)).join(', ')} };` : 'export {};',
763
+ '',
764
+ ].join('\n')
765
+ const dts = [
766
+ header(mod),
767
+ ...exported.flatMap(([name, local]) => (name === 'default'
768
+ ? ['declare const _default: any', 'export default _default']
769
+ : [`export declare const ${name}: any`, ...(usableAsType(mod, name, local) ? [`export type ${name}${TYPE_PARAMS} = any`] : [])])),
770
+ ...[...mod.types].filter((t) => t !== 'default' && !mod.exports.has(t)).sort().map((t) => `export type ${t}${TYPE_PARAMS} = any`),
771
+ ...(exported.length || mod.types.size ? [] : ['export {}']),
772
+ '',
773
+ ].join('\n')
774
+ return { [`${mod.path}.js`]: js, [`${mod.path}.d.ts`]: dts }
775
+ }