@supatype/cli 0.1.9 → 0.1.10
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 +131 -128
- 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/internal.d.ts.map +1 -1
- package/dist/commands/internal.js +37 -0
- package/dist/commands/internal.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/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/internal.ts +37 -0
- package/src/config.ts +36 -15
- package/src/resolve-api-url.ts +3 -2
- package/src/tsx-runner.ts +40 -1
- package/tests/config-load-failure.test.ts +44 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/internal.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Command } from "commander"
|
|
6
|
+
import { resolve } from "node:path"
|
|
7
|
+
import { pathToFileURL } from "node:url"
|
|
6
8
|
import { downloadAll } from "../binary-cache.js"
|
|
7
9
|
import { error, info } from "../ui/messages.js"
|
|
8
10
|
|
|
@@ -20,4 +22,39 @@ export function registerInternalCommands(program: Command): void {
|
|
|
20
22
|
process.exit(1)
|
|
21
23
|
}
|
|
22
24
|
})
|
|
25
|
+
|
|
26
|
+
// Interpreter for the standalone binary. It carries the Bun runtime, so it can import
|
|
27
|
+
// TypeScript directly; what it cannot do is spawn `node` with `tsx`, because a machine that
|
|
28
|
+
// installed with `curl | sh` has neither, and neither is embedded. So the binary re-executes
|
|
29
|
+
// itself for these two jobs rather than reaching for a toolchain that is not there.
|
|
30
|
+
//
|
|
31
|
+
// Hidden and prefixed, like _postinstall: they are an implementation detail of config loading,
|
|
32
|
+
// not commands anyone should run.
|
|
33
|
+
program
|
|
34
|
+
.command("_print-module <target>", { hidden: true })
|
|
35
|
+
.description("Import a module and print its default export as JSON")
|
|
36
|
+
.action(async (target: string) => {
|
|
37
|
+
try {
|
|
38
|
+
const href = target.startsWith("file:") ? target : pathToFileURL(resolve(target)).href
|
|
39
|
+
const mod = (await import(href)) as { default?: unknown }
|
|
40
|
+
const value = mod.default ?? mod
|
|
41
|
+
process.stdout.write(JSON.stringify(value))
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// stderr, because the caller parses stdout as JSON and a message there would corrupt it.
|
|
44
|
+
process.stderr.write(err instanceof Error ? (err.stack ?? err.message) : String(err))
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command("_run-ts <file>", { hidden: true })
|
|
51
|
+
.description("Execute a TypeScript file for its side effects")
|
|
52
|
+
.action(async (file: string) => {
|
|
53
|
+
try {
|
|
54
|
+
await import(pathToFileURL(resolve(file)).href)
|
|
55
|
+
} catch (err) {
|
|
56
|
+
process.stderr.write(err instanceof Error ? (err.stack ?? err.message) : String(err))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
23
60
|
}
|
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,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
|
+
})
|