@supatype/cli 0.1.9 → 0.1.11
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +139 -136
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/cli-install-method.d.ts +8 -0
- package/dist/cli-install-method.d.ts.map +1 -1
- package/dist/cli-install-method.js +10 -0
- package/dist/cli-install-method.js.map +1 -1
- package/dist/cli-version-embedded.d.ts.map +1 -1
- package/dist/cli-version-embedded.js +1 -1
- package/dist/cli-version-embedded.js.map +1 -1
- package/dist/commands/add.js +3 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +3 -2
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +3 -2
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +15 -18
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +16 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/internal.d.ts.map +1 -1
- package/dist/commands/internal.js +37 -0
- package/dist/commands/internal.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +15 -10
- package/dist/commands/push.js.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -15
- package/dist/config.js.map +1 -1
- package/dist/resolve-api-url.d.ts.map +1 -1
- package/dist/resolve-api-url.js +3 -2
- package/dist/resolve-api-url.js.map +1 -1
- package/dist/tsx-runner.d.ts +12 -0
- package/dist/tsx-runner.d.ts.map +1 -1
- package/dist/tsx-runner.js +35 -1
- package/dist/tsx-runner.js.map +1 -1
- package/dist/type-generation.d.ts +24 -0
- package/dist/type-generation.d.ts.map +1 -0
- package/dist/type-generation.js +44 -0
- package/dist/type-generation.js.map +1 -0
- package/package.json +1 -1
- package/src/cli-install-method.ts +11 -0
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/add.ts +3 -2
- package/src/commands/db.ts +3 -2
- package/src/commands/functions.ts +3 -2
- package/src/commands/generate.ts +13 -21
- package/src/commands/init.ts +16 -5
- package/src/commands/internal.ts +37 -0
- package/src/commands/push.ts +14 -8
- package/src/config.ts +36 -15
- package/src/resolve-api-url.ts +3 -2
- package/src/tsx-runner.ts +40 -1
- package/src/type-generation.ts +60 -0
- package/tests/app-command.test.ts +12 -2
- package/tests/config-load-failure.test.ts +44 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs"
|
|
|
2
2
|
import { resolve } from "node:path"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
-
import {
|
|
5
|
+
import { importModuleAsJson } from "./tsx-runner.js"
|
|
6
6
|
import { readEnvFile } from "./env-file.js"
|
|
7
7
|
import {
|
|
8
8
|
mergeProjectConfig,
|
|
@@ -157,6 +157,37 @@ function configLoadEnv(cwd: string): NodeJS.ProcessEnv {
|
|
|
157
157
|
return { ...readEnvFile(cwd), ...process.env }
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* A config file exists but could not be evaluated.
|
|
162
|
+
*
|
|
163
|
+
* Distinct from absence on purpose. Six call sites catch config loading so the CLI still works
|
|
164
|
+
* outside a project, and while they treated both cases the same, a config that failed to load
|
|
165
|
+
* looked exactly like no config at all. That is how a standalone binary that could not read any
|
|
166
|
+
* config shipped in v0.1.9 and still appeared to function: `status` printed a stack of stopped
|
|
167
|
+
* services, and `db check` fell back to DATABASE_URL from .env and reported a connection error.
|
|
168
|
+
* Those call sites should swallow absence and re-throw this.
|
|
169
|
+
*/
|
|
170
|
+
export class ConfigLoadError extends Error {
|
|
171
|
+
readonly configPath: string
|
|
172
|
+
|
|
173
|
+
constructor(configPath: string, detail: string) {
|
|
174
|
+
super(`Failed to load ${configPath}:\n${detail}`)
|
|
175
|
+
this.name = "ConfigLoadError"
|
|
176
|
+
this.configPath = configPath
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Re-throw a config that exists but could not be read; swallow anything else.
|
|
182
|
+
*
|
|
183
|
+
* For the call sites that catch config loading so the CLI still works outside a project. They
|
|
184
|
+
* want to ignore absence, not breakage, and treating the two alike is what let a standalone
|
|
185
|
+
* binary that could read no config at all still look like it was working.
|
|
186
|
+
*/
|
|
187
|
+
export function rethrowIfConfigBroken(err: unknown): void {
|
|
188
|
+
if (err instanceof ConfigLoadError) throw err
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
function loadFirstTsConfig(
|
|
161
192
|
cwd: string,
|
|
162
193
|
candidates: string[],
|
|
@@ -166,24 +197,19 @@ function loadFirstTsConfig(
|
|
|
166
197
|
if (!existsSync(configPath)) continue
|
|
167
198
|
|
|
168
199
|
const urlPath = "file:///" + configPath.replace(/\\/g, "/")
|
|
169
|
-
const
|
|
170
|
-
const mod = await import(${JSON.stringify(urlPath)})
|
|
171
|
-
const config = mod.default ?? mod
|
|
172
|
-
process.stdout.write(JSON.stringify(config))
|
|
173
|
-
`
|
|
174
|
-
const result = evalTsSnippet(snippet, { cwd, env: configLoadEnv(cwd) })
|
|
200
|
+
const result = importModuleAsJson(urlPath, { cwd, env: configLoadEnv(cwd) })
|
|
175
201
|
if (result.exitCode === 0) {
|
|
176
202
|
return JSON.parse(result.stdout) as Record<string, unknown>
|
|
177
203
|
}
|
|
178
204
|
|
|
179
205
|
const failure = result.stderr || result.stdout
|
|
180
206
|
if (!shouldStripCliImportOnLoadFailure(failure)) {
|
|
181
|
-
throw new
|
|
207
|
+
throw new ConfigLoadError(candidate, failure)
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
const fallback = loadTsConfigWithoutCliImport(configPath, cwd)
|
|
185
211
|
if (fallback !== null) return fallback
|
|
186
|
-
throw new
|
|
212
|
+
throw new ConfigLoadError(candidate, failure)
|
|
187
213
|
}
|
|
188
214
|
return null
|
|
189
215
|
}
|
|
@@ -217,12 +243,7 @@ function loadTsConfigWithoutCliImport(
|
|
|
217
243
|
writeFileSync(tmpPath, wrapper, "utf8")
|
|
218
244
|
try {
|
|
219
245
|
const urlPath = "file:///" + tmpPath.replace(/\\/g, "/")
|
|
220
|
-
const
|
|
221
|
-
const mod = await import(${JSON.stringify(urlPath)})
|
|
222
|
-
const config = mod.default ?? mod
|
|
223
|
-
process.stdout.write(JSON.stringify(config))
|
|
224
|
-
`
|
|
225
|
-
const result = evalTsSnippet(snippet, { cwd, env: configLoadEnv(cwd) })
|
|
246
|
+
const result = importModuleAsJson(urlPath, { cwd, env: configLoadEnv(cwd) })
|
|
226
247
|
if (result.exitCode !== 0) return null
|
|
227
248
|
return JSON.parse(result.stdout) as Record<string, unknown>
|
|
228
249
|
} finally {
|
package/src/resolve-api-url.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Resolve the project API base URL (Kong gateway or direct server) for CLI HTTP calls.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { loadConfig } from "./config.js"
|
|
5
|
+
import { loadConfig, rethrowIfConfigBroken } from "./config.js"
|
|
6
6
|
import { readEnvValue } from "./env-file.js"
|
|
7
7
|
import { serverBaseUrl } from "./project-config.js"
|
|
8
8
|
|
|
@@ -34,7 +34,8 @@ export function resolveProjectApiUrl(cwd: string): string {
|
|
|
34
34
|
if (fromConfig) {
|
|
35
35
|
return fromConfig.replace(/\/+$/, "")
|
|
36
36
|
}
|
|
37
|
-
} catch {
|
|
37
|
+
} catch (err) {
|
|
38
|
+
rethrowIfConfigBroken(err)
|
|
38
39
|
// No supatype.config: fall through to PORT default.
|
|
39
40
|
}
|
|
40
41
|
|
package/src/tsx-runner.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { createRequire } from "node:module"
|
|
|
10
10
|
import { fileURLToPath } from "node:url"
|
|
11
11
|
import { writeFileSync, unlinkSync } from "node:fs"
|
|
12
12
|
import { tmpdir } from "node:os"
|
|
13
|
+
import { isCompiledBinary } from "./cli-install-method.js"
|
|
13
14
|
|
|
14
15
|
const _require = createRequire(import.meta.url)
|
|
15
16
|
|
|
@@ -51,7 +52,9 @@ export function runTsFile(
|
|
|
51
52
|
filePath: string,
|
|
52
53
|
opts: SpawnSyncOptions = {},
|
|
53
54
|
): RunResult {
|
|
54
|
-
|
|
55
|
+
// Same split as importModuleAsJson: the binary interprets TypeScript itself, Node uses tsx.
|
|
56
|
+
const argv = isCompiledBinary() ? ["_run-ts", filePath] : [TSX_BIN, filePath]
|
|
57
|
+
const result = spawnSync(process.execPath, argv, {
|
|
55
58
|
encoding: "utf8",
|
|
56
59
|
maxBuffer: 50 * 1024 * 1024,
|
|
57
60
|
...opts,
|
|
@@ -87,3 +90,39 @@ export function evalTsSnippet(
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Import a module and return its default export as JSON text.
|
|
96
|
+
*
|
|
97
|
+
* Two interpreters, one contract. Under Node this writes a snippet and runs it through tsx, as
|
|
98
|
+
* it always has. As a compiled binary it re-executes itself, because there is no node_modules to
|
|
99
|
+
* resolve tsx from, no writable directory beside the executable, and on a `curl | sh` machine no
|
|
100
|
+
* Node either: the previous behaviour failed with
|
|
101
|
+
* `ENOENT: open '/$bunfs/root/supatype-eval-....mts'` and broke init, update, dev and push.
|
|
102
|
+
*
|
|
103
|
+
* Callers get the same stdout, stderr and exit code either way.
|
|
104
|
+
*/
|
|
105
|
+
export function importModuleAsJson(
|
|
106
|
+
target: string,
|
|
107
|
+
opts: SpawnSyncOptions = {},
|
|
108
|
+
): RunResult {
|
|
109
|
+
if (isCompiledBinary()) {
|
|
110
|
+
const result = spawnSync(process.execPath, ["_print-module", target], {
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
113
|
+
...opts,
|
|
114
|
+
})
|
|
115
|
+
return {
|
|
116
|
+
stdout: String(result.stdout ?? ""),
|
|
117
|
+
stderr: String(result.stderr ?? ""),
|
|
118
|
+
exitCode: result.status ?? 1,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const snippet = `
|
|
123
|
+
const mod = await import(${JSON.stringify(target)})
|
|
124
|
+
const config = mod.default ?? mod
|
|
125
|
+
process.stdout.write(JSON.stringify(config))
|
|
126
|
+
`
|
|
127
|
+
return evalTsSnippet(snippet, opts)
|
|
128
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write a project's generated types to disk.
|
|
3
|
+
*
|
|
4
|
+
* `push` used to send `types_path` and `client_path` to the engine and read only `message`,
|
|
5
|
+
* expecting the engine to write the files. It does not write them, so with `output.types`
|
|
6
|
+
* configured the generated TypeScript was printed to the terminal and nothing reached disk:
|
|
7
|
+
* `supatype push` claims to generate types and produced none. `supatype generate` had it right
|
|
8
|
+
* all along, and this is that logic, shared, so the two cannot disagree again.
|
|
9
|
+
*
|
|
10
|
+
* Each path is optional and an absent one is skipped, because the two callers differ on purpose:
|
|
11
|
+
* `generate` falls back to defaults and always writes, while `push` writes only what the project
|
|
12
|
+
* asked for and must not start creating files in projects that never configured any.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { dirname, resolve } from "node:path"
|
|
17
|
+
import { generateClientAugmentation } from "./augmentation-generator.js"
|
|
18
|
+
import { ensureEngine, engineRequest } from "./engine-client.js"
|
|
19
|
+
|
|
20
|
+
export interface GenerateTypesRequest {
|
|
21
|
+
cwd: string
|
|
22
|
+
ast: unknown
|
|
23
|
+
/** Relative path for the database types, from `output.types`. */
|
|
24
|
+
typesPath?: string | undefined
|
|
25
|
+
/** Relative path for the client augmentation, from `output.client`. */
|
|
26
|
+
clientPath?: string | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Writes what was asked for and returns one message per file, for the caller to report. */
|
|
30
|
+
export async function writeGeneratedTypes(req: GenerateTypesRequest): Promise<string[]> {
|
|
31
|
+
const written: string[] = []
|
|
32
|
+
|
|
33
|
+
if (req.typesPath !== undefined && req.typesPath !== "") {
|
|
34
|
+
await ensureEngine()
|
|
35
|
+
const result = await engineRequest<{ code?: string; message?: string }>(
|
|
36
|
+
"/generate",
|
|
37
|
+
{ ast: req.ast, lang: "typescript" },
|
|
38
|
+
)
|
|
39
|
+
// `code` is the field the engine fills; `message` is the older shape. Reading only `message`
|
|
40
|
+
// and printing it is how the generated file ended up in the terminal.
|
|
41
|
+
const code = result.code ?? result.message
|
|
42
|
+
if (code === undefined) {
|
|
43
|
+
throw new Error("Engine returned no output for type generation.")
|
|
44
|
+
}
|
|
45
|
+
const outPath = resolve(req.cwd, req.typesPath)
|
|
46
|
+
mkdirSync(dirname(outPath), { recursive: true })
|
|
47
|
+
writeFileSync(outPath, code, "utf8")
|
|
48
|
+
written.push(`Types written to ${req.typesPath}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Generated locally from the AST, so it needs no engine round trip.
|
|
52
|
+
if (req.clientPath !== undefined && req.clientPath !== "") {
|
|
53
|
+
const outPath = resolve(req.cwd, req.clientPath)
|
|
54
|
+
mkdirSync(dirname(outPath), { recursive: true })
|
|
55
|
+
writeFileSync(outPath, generateClientAugmentation(req.ast), "utf8")
|
|
56
|
+
written.push(`Client augmentation written to ${req.clientPath}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return written
|
|
60
|
+
}
|
|
@@ -12,10 +12,20 @@ const CLI_BIN = resolve(__dirname, "../bin/supatype.js")
|
|
|
12
12
|
|
|
13
13
|
function runCli(cwd: string, args: string[]): { stdout: string; stderr: string; exitCode: number } {
|
|
14
14
|
const result = spawnSync(process.execPath, [CLI_BIN, ...args], {
|
|
15
|
-
encoding: "utf8",
|
|
16
15
|
cwd,
|
|
17
|
-
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
// 60s, not 10s: this spawns the built CLI, which imports Ink, React and commander before it
|
|
18
|
+
// does anything. That is about 1.2s idle, and CI runs `turbo run test` across every package at
|
|
19
|
+
// once, where it exceeded 10s and the subprocess was killed. The assertion then compared
|
|
20
|
+
// against empty output and pointed at the CLI rather than at contention.
|
|
21
|
+
timeout: 60_000,
|
|
18
22
|
})
|
|
23
|
+
if (result.signal) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`CLI subprocess killed by ${result.signal} after the spawn timeout. `
|
|
26
|
+
+ `Args: ${args.join(" ")}. This is usually machine contention, not a CLI fault.`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
19
29
|
return {
|
|
20
30
|
stdout: String(result.stdout ?? ""),
|
|
21
31
|
stderr: String(result.stderr ?? ""),
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { spawnSync } from "node:child_process"
|
|
5
|
+
import { describe, expect, it } from "vitest"
|
|
6
|
+
|
|
7
|
+
const CLI = join(import.meta.dirname, "../bin/supatype.js")
|
|
8
|
+
|
|
9
|
+
function runIn(dir: string, args: string[]): { out: string; code: number } {
|
|
10
|
+
const r = spawnSync(process.execPath, [CLI, ...args], {
|
|
11
|
+
cwd: dir,
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
timeout: 60_000,
|
|
14
|
+
})
|
|
15
|
+
if (r.signal) throw new Error(`CLI killed by ${r.signal}, usually machine contention`)
|
|
16
|
+
return { out: `${r.stdout ?? ""}${r.stderr ?? ""}`, code: r.status ?? 1 }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("a config that cannot be read", () => {
|
|
20
|
+
// A broken config used to be indistinguishable from no config: the call sites that catch
|
|
21
|
+
// loadConfig so the CLI works outside a project swallowed both. That is why a standalone
|
|
22
|
+
// binary which could read no config at all still looked like it was working, for a whole
|
|
23
|
+
// release.
|
|
24
|
+
it("is reported, rather than treated as absent", () => {
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-broken-config-"))
|
|
26
|
+
writeFileSync(
|
|
27
|
+
join(dir, "supatype.config.ts"),
|
|
28
|
+
'throw new Error("deliberately broken")\n',
|
|
29
|
+
"utf8",
|
|
30
|
+
)
|
|
31
|
+
const { out, code } = runIn(dir, ["db", "check"])
|
|
32
|
+
expect(out).toContain("Failed to load supatype.config.ts")
|
|
33
|
+
expect(out).not.toContain("No connection. Pass --connection")
|
|
34
|
+
expect(code).not.toBe(0)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("does not stop the CLI working with no config at all", () => {
|
|
38
|
+
const dir = mkdtempSync(join(tmpdir(), "supatype-no-config-"))
|
|
39
|
+
mkdirSync(join(dir, "empty"), { recursive: true })
|
|
40
|
+
const { out } = runIn(join(dir, "empty"), ["db", "check"])
|
|
41
|
+
expect(out).toContain("No connection. Pass --connection")
|
|
42
|
+
expect(out).not.toContain("Failed to load")
|
|
43
|
+
})
|
|
44
|
+
})
|