@thebaycloud/cli 1.0.0
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/CHANGELOG.md +224 -0
- package/README.md +53 -0
- package/index.js +1269 -0
- package/lib/brand.js +124 -0
- package/lib/bundle.js +399 -0
- package/lib/check.js +281 -0
- package/lib/confirm.js +46 -0
- package/lib/draft.js +488 -0
- package/lib/envfile.js +158 -0
- package/lib/exec-args.js +40 -0
- package/lib/prebuilt.js +86 -0
- package/lib/resolver.js +63 -0
- package/lib/who.js +14 -0
- package/package.json +48 -0
- package/vendor/README.md +6 -0
- package/vendor/detector.js +423 -0
- package/vendor/inputs.json +21 -0
- package/vendor/resolve.js +2156 -0
|
@@ -0,0 +1,2156 @@
|
|
|
1
|
+
// supersonic-vendor-stamp d638cb7c23598653
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// ../../packages/cli/src/resolver.entry.ts
|
|
21
|
+
var resolver_entry_exports = {};
|
|
22
|
+
__export(resolver_entry_exports, {
|
|
23
|
+
CONFIG_FILENAME: () => CONFIG_FILENAME,
|
|
24
|
+
ConfigError: () => ConfigError,
|
|
25
|
+
DEFAULT_SCALE: () => DEFAULT_SCALE,
|
|
26
|
+
PRIMITIVE: () => PRIMITIVE,
|
|
27
|
+
ProcessError: () => ProcessError,
|
|
28
|
+
ProcfileError: () => ProcfileError,
|
|
29
|
+
RUNTIME_UNSUPPORTED: () => RUNTIME_UNSUPPORTED,
|
|
30
|
+
RUNTIME_VERSIONS: () => RUNTIME_VERSIONS,
|
|
31
|
+
ResolveError: () => ResolveError,
|
|
32
|
+
appResources: () => appResources,
|
|
33
|
+
assertConsumed: () => assertConsumed,
|
|
34
|
+
bindToPort: () => bindToPort,
|
|
35
|
+
declaredLanguages: () => declaredLanguages,
|
|
36
|
+
deployableParts: () => deployableParts,
|
|
37
|
+
deriveLane: () => deriveLane,
|
|
38
|
+
detect: () => detect,
|
|
39
|
+
inferAppConfig: () => inferAppConfig,
|
|
40
|
+
isDeployablePart: () => isDeployablePart,
|
|
41
|
+
isServiceless: () => isServiceless,
|
|
42
|
+
mergeProcfile: () => mergeProcfile,
|
|
43
|
+
missingSecrets: () => missingSecrets,
|
|
44
|
+
normalizeLanguage: () => normalizeLanguage,
|
|
45
|
+
parseAppConfig: () => parseAppConfig,
|
|
46
|
+
parseProcfile: () => parseProcfile,
|
|
47
|
+
platformOwned: () => platformOwned,
|
|
48
|
+
primaryService: () => primaryService,
|
|
49
|
+
pythonInstall: () => pythonInstall,
|
|
50
|
+
pythonModule: () => pythonModule,
|
|
51
|
+
readAppConfig: () => readAppConfig,
|
|
52
|
+
readProcfile: () => readProcfile,
|
|
53
|
+
readRepoFacts: () => readRepoFacts,
|
|
54
|
+
resolve: () => resolve2,
|
|
55
|
+
resolveProcesses: () => resolveProcesses,
|
|
56
|
+
runtimeMismatch: () => runtimeMismatch,
|
|
57
|
+
serviceFor: () => serviceFor,
|
|
58
|
+
serviceLanguage: () => serviceLanguage,
|
|
59
|
+
servicePath: () => servicePath,
|
|
60
|
+
unemittable: () => unemittable,
|
|
61
|
+
validate: () => validate
|
|
62
|
+
});
|
|
63
|
+
module.exports = __toCommonJS(resolver_entry_exports);
|
|
64
|
+
|
|
65
|
+
// lib/resolve.ts
|
|
66
|
+
var import_node_fs7 = require("node:fs");
|
|
67
|
+
var import_node_path7 = require("node:path");
|
|
68
|
+
|
|
69
|
+
// lib/app-config.ts
|
|
70
|
+
var import_node_fs = require("node:fs");
|
|
71
|
+
var import_node_path = require("node:path");
|
|
72
|
+
|
|
73
|
+
// lib/db-address.ts
|
|
74
|
+
var CLOUD_RUN_DB = { host: "127.0.0.1", port: "5432" };
|
|
75
|
+
|
|
76
|
+
// lib/lanes.ts
|
|
77
|
+
var SERVICE_LANES = ["container"];
|
|
78
|
+
var ALL_LANES = ["static", ...SERVICE_LANES];
|
|
79
|
+
var DB_HOST = CLOUD_RUN_DB.host;
|
|
80
|
+
var DB_PORT = CLOUD_RUN_DB.port;
|
|
81
|
+
var CLOUD_SQL_PROXY_IMAGE = process.env.CLOUD_SQL_PROXY_IMAGE ?? "gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.14.1";
|
|
82
|
+
function databaseEnv(db, at = CLOUD_RUN_DB) {
|
|
83
|
+
return [
|
|
84
|
+
`DATABASE_URL=${db.databaseUrl}`,
|
|
85
|
+
`POSTGRES_SERVER=${at.host}`,
|
|
86
|
+
`POSTGRES_HOST=${at.host}`,
|
|
87
|
+
`POSTGRES_PORT=${at.port}`,
|
|
88
|
+
`POSTGRES_USER=${db.user}`,
|
|
89
|
+
`POSTGRES_PASSWORD=${db.password}`,
|
|
90
|
+
`POSTGRES_DB=${db.dbName}`,
|
|
91
|
+
`PGHOST=${at.host}`,
|
|
92
|
+
`PGPORT=${at.port}`,
|
|
93
|
+
`PGUSER=${db.user}`,
|
|
94
|
+
`PGPASSWORD=${db.password}`,
|
|
95
|
+
`PGDATABASE=${db.dbName}`,
|
|
96
|
+
`DB_HOST=${at.host}`,
|
|
97
|
+
`DB_PORT=${at.port}`,
|
|
98
|
+
`DB_USER=${db.user}`,
|
|
99
|
+
`DB_PASSWORD=${db.password}`,
|
|
100
|
+
`DB_NAME=${db.dbName}`
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
function databaseEnvNames() {
|
|
104
|
+
return databaseEnv({ databaseUrl: "", user: "", password: "", dbName: "" }).map((pair2) => pair2.slice(0, pair2.indexOf("=")));
|
|
105
|
+
}
|
|
106
|
+
var DEFAULT_SCALE = {
|
|
107
|
+
memory: process.env.RUNNER_MEMORY || "2Gi",
|
|
108
|
+
cpu: 1,
|
|
109
|
+
maxInstances: 10,
|
|
110
|
+
timeout: 900,
|
|
111
|
+
concurrency: 80,
|
|
112
|
+
cpuBoost: true
|
|
113
|
+
};
|
|
114
|
+
function withScale(over) {
|
|
115
|
+
const declared = Object.fromEntries(
|
|
116
|
+
Object.entries(over ?? {}).filter(([, v]) => v !== void 0)
|
|
117
|
+
);
|
|
118
|
+
return { ...DEFAULT_SCALE, ...declared };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// lib/app-config.ts
|
|
122
|
+
var CONFIG_FILENAME = "supersonic.json";
|
|
123
|
+
var ConfigError = class extends Error {
|
|
124
|
+
};
|
|
125
|
+
var LANGUAGES = /* @__PURE__ */ new Set(["node", "python", "static", "other"]);
|
|
126
|
+
var ALWAYS_OWNED_PREFIXES = [/^SUPERSONIC_/];
|
|
127
|
+
var ALWAYS_OWNED_EXACT = /* @__PURE__ */ new Set(["PORT"]);
|
|
128
|
+
var DATABASE_OWNED_PREFIXES = [/^POSTGRES_/, /^PG/, /^DB_/];
|
|
129
|
+
var DATABASE_OWNED_EXACT = new Set(databaseEnvNames());
|
|
130
|
+
function platformOwned(name, database) {
|
|
131
|
+
if (ALWAYS_OWNED_EXACT.has(name) || ALWAYS_OWNED_PREFIXES.some((re) => re.test(name))) return true;
|
|
132
|
+
if (database?.provider !== "managed") return false;
|
|
133
|
+
return DATABASE_OWNED_EXACT.has(name) || DATABASE_OWNED_PREFIXES.some((re) => re.test(name));
|
|
134
|
+
}
|
|
135
|
+
function safeDir(dir, where) {
|
|
136
|
+
if (dir === void 0 || dir === null || dir === "") return ".";
|
|
137
|
+
if (typeof dir !== "string") throw new ConfigError(`${where}: "dir" must be a string`);
|
|
138
|
+
const d = dir.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
139
|
+
if (!d || d === ".") return ".";
|
|
140
|
+
if (d.startsWith("/") || d.split("/").includes("..")) {
|
|
141
|
+
throw new ConfigError(`${where}: "dir" must be inside the repository (got ${JSON.stringify(dir)})`);
|
|
142
|
+
}
|
|
143
|
+
return d;
|
|
144
|
+
}
|
|
145
|
+
function str(v, where) {
|
|
146
|
+
if (v === void 0 || v === null) return void 0;
|
|
147
|
+
if (typeof v !== "string") throw new ConfigError(`${where} must be a string`);
|
|
148
|
+
return v;
|
|
149
|
+
}
|
|
150
|
+
function num(v, where) {
|
|
151
|
+
if (v === void 0 || v === null) return void 0;
|
|
152
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new ConfigError(`${where} must be a number`);
|
|
153
|
+
return v;
|
|
154
|
+
}
|
|
155
|
+
function literals(v, where, database) {
|
|
156
|
+
if (v === void 0 || v === null) return void 0;
|
|
157
|
+
if (typeof v !== "object" || Array.isArray(v)) {
|
|
158
|
+
throw new ConfigError(`${where} must be an object of NAME: value, or an array of variable NAMES`);
|
|
159
|
+
}
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [k, raw] of Object.entries(v)) {
|
|
162
|
+
if (platformOwned(k, database)) {
|
|
163
|
+
throw new ConfigError(
|
|
164
|
+
`${where}: "${k}" is set by the platform and cannot be declared here. ` + ownedBecause(k, database)
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (typeof raw !== "string" && typeof raw !== "number" && typeof raw !== "boolean") {
|
|
168
|
+
throw new ConfigError(`${where}.${k} must be a string, number or boolean`);
|
|
169
|
+
}
|
|
170
|
+
out[k] = String(raw);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
function ownedBecause(name, database) {
|
|
175
|
+
if (ALWAYS_OWNED_EXACT.has(name) || ALWAYS_OWNED_PREFIXES.some((re) => re.test(name))) {
|
|
176
|
+
return name === "PORT" ? `Cloud Run assigns the port and injects it \u2014 read it from the environment instead.` : `SUPERSONIC_* is the platform's own namespace.`;
|
|
177
|
+
}
|
|
178
|
+
return `The platform provisions this app's database, so it writes this value itself. If the app already HAS a database, declare it instead: "resources": { "database": { "provider": "external", "urlFrom": "${name}" } }`;
|
|
179
|
+
}
|
|
180
|
+
function names(v, where, database) {
|
|
181
|
+
if (v === void 0 || v === null) return void 0;
|
|
182
|
+
if (!Array.isArray(v) || v.some((e) => typeof e !== "string")) {
|
|
183
|
+
throw new ConfigError(`${where} must be an array of variable NAMES`);
|
|
184
|
+
}
|
|
185
|
+
for (const n of v) {
|
|
186
|
+
if (platformOwned(n, database)) {
|
|
187
|
+
throw new ConfigError(`${where}: "${n}" is set by the platform and cannot be declared here. ` + ownedBecause(n, database));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return v;
|
|
191
|
+
}
|
|
192
|
+
function health(v, where) {
|
|
193
|
+
if (v === void 0 || v === null) return void 0;
|
|
194
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError(`${where} must be an object`);
|
|
195
|
+
const o = v;
|
|
196
|
+
const path = str(o.path, `${where}.path`) ?? "/";
|
|
197
|
+
if (!path.startsWith("/")) throw new ConfigError(`${where}.path must start with /`);
|
|
198
|
+
const expect = num(o.expect, `${where}.expect`) ?? 200;
|
|
199
|
+
if (expect < 100 || expect > 599) throw new ConfigError(`${where}.expect must be an HTTP status code`);
|
|
200
|
+
return { path, expect };
|
|
201
|
+
}
|
|
202
|
+
function scale(v, where) {
|
|
203
|
+
if (v === void 0 || v === null) return void 0;
|
|
204
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError(`${where} must be an object`);
|
|
205
|
+
const o = v;
|
|
206
|
+
const memory = str(o.memory, `${where}.memory`);
|
|
207
|
+
if (memory !== void 0 && !/^\d+(Mi|Gi)$/.test(memory)) {
|
|
208
|
+
throw new ConfigError(`${where}.memory must look like "512Mi" or "2Gi" (got ${JSON.stringify(memory)})`);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
memory,
|
|
212
|
+
cpu: num(o.cpu, `${where}.cpu`),
|
|
213
|
+
maxInstances: num(o.maxInstances, `${where}.maxInstances`),
|
|
214
|
+
timeout: num(o.timeout, `${where}.timeout`),
|
|
215
|
+
concurrency: num(o.concurrency, `${where}.concurrency`),
|
|
216
|
+
cpuBoost: o.cpuBoost === void 0 ? void 0 : Boolean(o.cpuBoost)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function processConfigs(v, where) {
|
|
220
|
+
if (v === void 0 || v === null) return void 0;
|
|
221
|
+
if (typeof v !== "object" || Array.isArray(v)) {
|
|
222
|
+
throw new ConfigError(`${where} must be an object of NAME: { command, \u2026 } \u2014 for example { "web": { "command": "\u2026" } }`);
|
|
223
|
+
}
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const [name, raw] of Object.entries(v)) {
|
|
226
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
227
|
+
throw new ConfigError(`${where}: "${name}" is not a valid process name \u2014 letters, digits, "-" and "_" only`);
|
|
228
|
+
}
|
|
229
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
230
|
+
throw new ConfigError(`${where}.${name} must be an object`);
|
|
231
|
+
}
|
|
232
|
+
const p = raw;
|
|
233
|
+
out[name] = {
|
|
234
|
+
command: str(p.command, `${where}.${name}.command`),
|
|
235
|
+
kind: str(p.kind, `${where}.${name}.kind`),
|
|
236
|
+
memory: str(p.memory, `${where}.${name}.memory`),
|
|
237
|
+
cpu: num(p.cpu, `${where}.${name}.cpu`),
|
|
238
|
+
visibility: str(p.visibility, `${where}.${name}.visibility`),
|
|
239
|
+
health: health(p.health, `${where}.${name}.health`),
|
|
240
|
+
maxInstances: num(p.maxInstances, `${where}.${name}.maxInstances`),
|
|
241
|
+
concurrency: num(p.concurrency, `${where}.${name}.concurrency`),
|
|
242
|
+
cpuBoost: p.cpuBoost === void 0 ? void 0 : Boolean(p.cpuBoost),
|
|
243
|
+
timeout: num(p.timeout, `${where}.${name}.timeout`),
|
|
244
|
+
instances: num(p.instances, `${where}.${name}.instances`),
|
|
245
|
+
schedule: str(p.schedule, `${where}.${name}.schedule`),
|
|
246
|
+
timezone: str(p.timezone, `${where}.${name}.timezone`),
|
|
247
|
+
taskTimeout: num(p.taskTimeout, `${where}.${name}.taskTimeout`),
|
|
248
|
+
retries: num(p.retries, `${where}.${name}.retries`),
|
|
249
|
+
shutdownGrace: num(p.shutdownGrace, `${where}.${name}.shutdownGrace`)
|
|
250
|
+
};
|
|
251
|
+
for (const k of Object.keys(out[name])) {
|
|
252
|
+
if (out[name][k] === void 0) delete out[name][k];
|
|
253
|
+
}
|
|
254
|
+
if (out[name].visibility !== void 0 && out[name].visibility !== "public" && out[name].visibility !== "internal") {
|
|
255
|
+
throw new ConfigError(`${where}.${name}.visibility must be "public" or "internal"`);
|
|
256
|
+
}
|
|
257
|
+
if (out[name].kind !== void 0 && !PROCESS_KINDS.has(out[name].kind)) {
|
|
258
|
+
throw new ConfigError(`${where}.${name}.kind must be one of ${[...PROCESS_KINDS].join(", ")}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
var PROCESS_KINDS = /* @__PURE__ */ new Set(["web", "worker", "cron", "release"]);
|
|
264
|
+
function resources(v) {
|
|
265
|
+
if (v === void 0 || v === null) return void 0;
|
|
266
|
+
const where = `${CONFIG_FILENAME} resources`;
|
|
267
|
+
if (typeof v !== "object" || Array.isArray(v)) throw new ConfigError(`${where} must be an object`);
|
|
268
|
+
const o = v;
|
|
269
|
+
let database;
|
|
270
|
+
if (o.database !== void 0 && o.database !== null && o.database !== false) {
|
|
271
|
+
if (typeof o.database !== "object" || Array.isArray(o.database)) {
|
|
272
|
+
throw new ConfigError(`${where}.database must be an object`);
|
|
273
|
+
}
|
|
274
|
+
const d = o.database;
|
|
275
|
+
const provider = str(d.provider, `${where}.database.provider`) ?? "managed";
|
|
276
|
+
if (provider !== "managed" && provider !== "external") {
|
|
277
|
+
throw new ConfigError(
|
|
278
|
+
`${where}.database.provider must be "managed" (the platform creates it) or "external" (it already exists and the platform only injects it) \u2014 got ${JSON.stringify(provider)}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (provider === "external") {
|
|
282
|
+
const urlFrom = str(d.urlFrom, `${where}.database.urlFrom`)?.trim();
|
|
283
|
+
if (!urlFrom) {
|
|
284
|
+
throw new ConfigError(
|
|
285
|
+
`${where}.database: an external database needs "urlFrom" \u2014 the NAME of the secret holding its connection URL, for example "DATABASE_URL". The value itself belongs in your .env or in \`supersonic env set\`, never in this file.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
if (platformOwned(urlFrom)) {
|
|
289
|
+
throw new ConfigError(`${where}.database.urlFrom: "${urlFrom}" is set by the platform and cannot hold your connection URL.`);
|
|
290
|
+
}
|
|
291
|
+
database = { provider: "external", engine: str(d.engine, `${where}.database.engine`), urlFrom };
|
|
292
|
+
} else {
|
|
293
|
+
const engine = str(d.engine, `${where}.database.engine`) ?? "postgres";
|
|
294
|
+
if (engine !== "postgres") {
|
|
295
|
+
throw new ConfigError(
|
|
296
|
+
`${where}.database.engine: only "postgres" is provisioned today (got ${JSON.stringify(engine)}). An ${engine} the app already has can be declared with "provider": "external".`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
database = { provider: "managed", engine: "postgres", version: str(d.version, `${where}.database.version`) };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { database, bucket: o.bucket === void 0 ? void 0 : Boolean(o.bucket) };
|
|
303
|
+
}
|
|
304
|
+
function parseAppConfig(text) {
|
|
305
|
+
let raw;
|
|
306
|
+
try {
|
|
307
|
+
raw = JSON.parse(text);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
throw new ConfigError(`${CONFIG_FILENAME} is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
310
|
+
}
|
|
311
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new ConfigError(`${CONFIG_FILENAME} must be a JSON object`);
|
|
312
|
+
const o = raw;
|
|
313
|
+
if (!Array.isArray(o.services) || o.services.length === 0) {
|
|
314
|
+
throw new ConfigError(`${CONFIG_FILENAME} needs a non-empty "services" array`);
|
|
315
|
+
}
|
|
316
|
+
const declaredResources = resources(o.resources);
|
|
317
|
+
const database = declaredDatabase(declaredResources, o.services);
|
|
318
|
+
const services = o.services.map((s, i) => {
|
|
319
|
+
const where = `${CONFIG_FILENAME} services[${i}]`;
|
|
320
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) throw new ConfigError(`${where} must be an object`);
|
|
321
|
+
const svc = s;
|
|
322
|
+
const language = str(svc.language, `${where}.language`);
|
|
323
|
+
if (language !== void 0 && !LANGUAGES.has(language)) {
|
|
324
|
+
throw new ConfigError(`${where}.language must be one of ${[...LANGUAGES].join(", ")}`);
|
|
325
|
+
}
|
|
326
|
+
const envIsNameList = Array.isArray(svc.env);
|
|
327
|
+
const uses = svc.uses === void 0 ? void 0 : names(svc.uses, `${where}.uses`, database);
|
|
328
|
+
const known = ["database", "bucket", "redis", "elasticsearch"];
|
|
329
|
+
for (const u of uses ?? []) {
|
|
330
|
+
if (!known.includes(u)) {
|
|
331
|
+
throw new ConfigError(`${where}.uses: "${u}" is not a resource \u2014 expected one of ${known.join(", ")}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
name: str(svc.name, `${where}.name`),
|
|
336
|
+
dir: safeDir(svc.dir, where),
|
|
337
|
+
language,
|
|
338
|
+
runtime: str(svc.runtime, `${where}.runtime`),
|
|
339
|
+
framework: str(svc.framework, `${where}.framework`),
|
|
340
|
+
install: str(svc.install, `${where}.install`),
|
|
341
|
+
build: str(svc.build, `${where}.build`),
|
|
342
|
+
preDeploy: str(svc.preDeploy, `${where}.preDeploy`),
|
|
343
|
+
release: str(svc.release, `${where}.release`),
|
|
344
|
+
start: str(svc.start, `${where}.start`),
|
|
345
|
+
processes: processConfigs(svc.processes, `${where}.processes`),
|
|
346
|
+
outputDir: str(svc.outputDir, `${where}.outputDir`),
|
|
347
|
+
spaFallback: svc.spaFallback === void 0 ? void 0 : Boolean(svc.spaFallback),
|
|
348
|
+
dockerfile: str(svc.dockerfile, `${where}.dockerfile`),
|
|
349
|
+
context: svc.context === void 0 ? void 0 : safeDir(svc.context, `${where}.context`),
|
|
350
|
+
needsDB: svc.needsDB === void 0 ? void 0 : Boolean(svc.needsDB),
|
|
351
|
+
uses,
|
|
352
|
+
env: envIsNameList ? void 0 : literals(svc.env, `${where}.env`, database),
|
|
353
|
+
envNeeded: envIsNameList ? names(svc.env, `${where}.env`, database) : void 0,
|
|
354
|
+
buildEnv: literals(svc.buildEnv, `${where}.buildEnv`, database),
|
|
355
|
+
secrets: names(svc.secrets, `${where}.secrets`, database),
|
|
356
|
+
health: health(svc.health, `${where}.health`),
|
|
357
|
+
scale: scale(svc.scale, `${where}.scale`),
|
|
358
|
+
path: str(svc.path, `${where}.path`)
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
for (const [i, s] of services.entries()) {
|
|
362
|
+
if (s.preDeploy !== void 0 && s.release !== void 0 && s.preDeploy !== s.release) {
|
|
363
|
+
throw new ConfigError(
|
|
364
|
+
`${CONFIG_FILENAME} services[${i}]: "preDeploy" and "release" are the same field \u2014 keep "release".`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const [i, s] of services.entries()) {
|
|
369
|
+
if (!s.processes || s === primaryService({ services })) continue;
|
|
370
|
+
throw new ConfigError(
|
|
371
|
+
`${CONFIG_FILENAME} services[${i}]: "processes" on a sibling service is not deployed yet.
|
|
372
|
+
Only the service on "/" runs workers and crons today. Move them there, or give this service its own app.`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const seen = /* @__PURE__ */ new Set();
|
|
376
|
+
for (const s of services) {
|
|
377
|
+
const p = servicePath(s);
|
|
378
|
+
if (seen.has(p)) throw new ConfigError(`${CONFIG_FILENAME}: two services both serve ${p}`);
|
|
379
|
+
seen.add(p);
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
version: typeof o.version === "number" ? o.version : 1,
|
|
383
|
+
resources: appResources(declaredResources, services),
|
|
384
|
+
services
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function declaredDatabase(declared, rawServices) {
|
|
388
|
+
if (declared?.database) return declared.database;
|
|
389
|
+
const wants = rawServices.some((s) => {
|
|
390
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) return false;
|
|
391
|
+
const svc = s;
|
|
392
|
+
return Boolean(svc.needsDB) || Array.isArray(svc.uses) && svc.uses.includes("database");
|
|
393
|
+
});
|
|
394
|
+
return wants ? { provider: "managed", engine: "postgres" } : void 0;
|
|
395
|
+
}
|
|
396
|
+
function appResources(declared, services) {
|
|
397
|
+
const wantsDb = services.some(usesDatabase);
|
|
398
|
+
const wantsBucket = services.some((s) => (s.uses ?? []).includes("bucket"));
|
|
399
|
+
const database = declared?.database ?? (wantsDb ? { provider: "managed", engine: "postgres" } : void 0);
|
|
400
|
+
const bucket = declared?.bucket ?? (wantsBucket ? true : void 0);
|
|
401
|
+
if (!database && bucket === void 0) return declared;
|
|
402
|
+
return { database, bucket };
|
|
403
|
+
}
|
|
404
|
+
function primaryService(config) {
|
|
405
|
+
return config.services.find((s) => (s.path ?? "/") === "/") ?? config.services[0];
|
|
406
|
+
}
|
|
407
|
+
function servicePath(s) {
|
|
408
|
+
const p = (s.path ?? "/").trim();
|
|
409
|
+
if (!p.startsWith("/")) throw new ConfigError(`${CONFIG_FILENAME}: "path" must start with / (got ${JSON.stringify(s.path)})`);
|
|
410
|
+
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
411
|
+
}
|
|
412
|
+
function inDir(cmd, dir) {
|
|
413
|
+
if (cmd === void 0) return void 0;
|
|
414
|
+
if (!cmd.trim()) return "";
|
|
415
|
+
return dir === "." ? cmd : `(cd ${dir} && ${cmd})`;
|
|
416
|
+
}
|
|
417
|
+
function usesDatabase(s) {
|
|
418
|
+
return Boolean(s.needsDB) || (s.uses ?? []).includes("database");
|
|
419
|
+
}
|
|
420
|
+
function releaseCommand(s) {
|
|
421
|
+
return s.release ?? s.preDeploy ?? s.processes?.release?.command;
|
|
422
|
+
}
|
|
423
|
+
function readAppConfig(dir) {
|
|
424
|
+
const path = (0, import_node_path.join)(dir, CONFIG_FILENAME);
|
|
425
|
+
if (!(0, import_node_fs.existsSync)(path)) return null;
|
|
426
|
+
return parseAppConfig((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// lib/infer-services.ts
|
|
430
|
+
var import_node_fs6 = require("node:fs");
|
|
431
|
+
var import_node_path6 = require("node:path");
|
|
432
|
+
|
|
433
|
+
// lib/repo-facts.ts
|
|
434
|
+
var import_node_fs2 = require("node:fs");
|
|
435
|
+
var import_node_path2 = require("node:path");
|
|
436
|
+
var MANIFESTS = {
|
|
437
|
+
"package.json": "node",
|
|
438
|
+
"pyproject.toml": "python",
|
|
439
|
+
"requirements.txt": "python",
|
|
440
|
+
"Pipfile": "python",
|
|
441
|
+
"setup.py": "python",
|
|
442
|
+
"uv.lock": "python",
|
|
443
|
+
"go.mod": "go",
|
|
444
|
+
"Cargo.toml": "rust",
|
|
445
|
+
"Gemfile": "ruby",
|
|
446
|
+
"composer.json": "php",
|
|
447
|
+
"pom.xml": "java",
|
|
448
|
+
"build.gradle": "java",
|
|
449
|
+
"build.gradle.kts": "java"
|
|
450
|
+
};
|
|
451
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
452
|
+
"node_modules",
|
|
453
|
+
".git",
|
|
454
|
+
".venv",
|
|
455
|
+
"venv",
|
|
456
|
+
"__pycache__",
|
|
457
|
+
"vendor",
|
|
458
|
+
"dist",
|
|
459
|
+
"build",
|
|
460
|
+
"target",
|
|
461
|
+
".next",
|
|
462
|
+
".nuxt",
|
|
463
|
+
".output",
|
|
464
|
+
"coverage",
|
|
465
|
+
".terraform",
|
|
466
|
+
"site-packages"
|
|
467
|
+
]);
|
|
468
|
+
var MAX_DEPTH = 3;
|
|
469
|
+
function readRepoFacts(dir) {
|
|
470
|
+
const declarations = [];
|
|
471
|
+
const dockerfiles = [];
|
|
472
|
+
let frontier = [{ abs: dir, rel: "", depth: 0 }];
|
|
473
|
+
while (frontier.length) {
|
|
474
|
+
const next = [];
|
|
475
|
+
for (const cur of frontier) {
|
|
476
|
+
let entries;
|
|
477
|
+
try {
|
|
478
|
+
entries = (0, import_node_fs2.readdirSync)(cur.abs, { withFileTypes: true });
|
|
479
|
+
} catch {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
for (const e of entries) {
|
|
483
|
+
const rel = cur.rel ? `${cur.rel}/${e.name}` : e.name;
|
|
484
|
+
if (e.isDirectory()) {
|
|
485
|
+
if (SKIP.has(e.name) || e.name.startsWith(".")) continue;
|
|
486
|
+
if (cur.depth + 1 <= MAX_DEPTH) next.push({ abs: (0, import_node_path2.join)(cur.abs, e.name), rel, depth: cur.depth + 1 });
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (!e.isFile()) continue;
|
|
490
|
+
if (e.name === "Dockerfile") dockerfiles.push(rel);
|
|
491
|
+
const language = MANIFESTS[e.name];
|
|
492
|
+
if (language) declarations.push({ language, from: rel, root: cur.depth === 0 });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
frontier = next;
|
|
496
|
+
}
|
|
497
|
+
return { declarations, dockerfiles };
|
|
498
|
+
}
|
|
499
|
+
function declaredLanguages(facts) {
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
for (const d of facts.declarations) seen.add(d.language);
|
|
502
|
+
return [...seen];
|
|
503
|
+
}
|
|
504
|
+
function normalizeLanguage(runtime) {
|
|
505
|
+
const r = String(runtime || "").toLowerCase();
|
|
506
|
+
if (!r) return null;
|
|
507
|
+
if (r.startsWith("node")) return "node";
|
|
508
|
+
if (r.startsWith("python")) return "python";
|
|
509
|
+
if (r.startsWith("go")) return "go";
|
|
510
|
+
if (r.startsWith("rust")) return "rust";
|
|
511
|
+
if (r.startsWith("ruby")) return "ruby";
|
|
512
|
+
if (r.startsWith("php")) return "php";
|
|
513
|
+
if (r.startsWith("java")) return "java";
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// lib/detect.ts
|
|
518
|
+
var import_node_fs5 = require("node:fs");
|
|
519
|
+
var import_node_path5 = require("node:path");
|
|
520
|
+
|
|
521
|
+
// lib/procfile.ts
|
|
522
|
+
var import_node_fs3 = require("node:fs");
|
|
523
|
+
var import_node_path3 = require("node:path");
|
|
524
|
+
var PROCFILE = "Procfile";
|
|
525
|
+
var ENTRY = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
|
|
526
|
+
var ProcfileError = class extends Error {
|
|
527
|
+
};
|
|
528
|
+
function parseProcfile(text) {
|
|
529
|
+
const out = [];
|
|
530
|
+
const seen = /* @__PURE__ */ new Map();
|
|
531
|
+
text.split(/\r?\n/).forEach((raw, i) => {
|
|
532
|
+
const line = i + 1;
|
|
533
|
+
const trimmed = raw.trim();
|
|
534
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
535
|
+
const m = ENTRY.exec(trimmed);
|
|
536
|
+
if (!m) {
|
|
537
|
+
throw new ProcfileError(
|
|
538
|
+
`${PROCFILE} line ${line}: expected "name: command", got ${JSON.stringify(trimmed)}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const name = m[1].toLowerCase();
|
|
542
|
+
const command = m[2].trim();
|
|
543
|
+
if (!command) throw new ProcfileError(`${PROCFILE} line ${line}: "${name}" has no command`);
|
|
544
|
+
const first = seen.get(name);
|
|
545
|
+
if (first !== void 0) {
|
|
546
|
+
throw new ProcfileError(`${PROCFILE}: "${name}" is declared twice, on lines ${first} and ${line}`);
|
|
547
|
+
}
|
|
548
|
+
seen.set(name, line);
|
|
549
|
+
out.push({ name, command, line });
|
|
550
|
+
});
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
function readProcfile(dir) {
|
|
554
|
+
const path = (0, import_node_path3.join)(dir, PROCFILE);
|
|
555
|
+
if (!(0, import_node_fs3.existsSync)(path)) return null;
|
|
556
|
+
return parseProcfile((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// lib/repo-runtime.ts
|
|
560
|
+
var import_node_fs4 = require("node:fs");
|
|
561
|
+
var import_node_path4 = require("node:path");
|
|
562
|
+
|
|
563
|
+
// lib/plan-deps.ts
|
|
564
|
+
var RUNTIME_VERSIONS = { python: "3.14", node: "24" };
|
|
565
|
+
var RUNTIME_UNSUPPORTED = "Runtime not available";
|
|
566
|
+
function lowestAccepted(spec) {
|
|
567
|
+
const m = spec.match(/>=\s*(\d+)(?:\.(\d+))?/);
|
|
568
|
+
return m ? [Number(m[1]), Number(m[2] ?? 0)] : null;
|
|
569
|
+
}
|
|
570
|
+
function below(have, need) {
|
|
571
|
+
const [hMaj, hMin] = have.split(".").map(Number);
|
|
572
|
+
return hMaj < need[0] || hMaj === need[0] && (hMin ?? 0) < need[1];
|
|
573
|
+
}
|
|
574
|
+
function runtimeMismatch(manifests) {
|
|
575
|
+
const py = manifests.pyproject?.match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
576
|
+
if (py) {
|
|
577
|
+
const need = lowestAccepted(py);
|
|
578
|
+
if (need && below(RUNTIME_VERSIONS.python, need)) {
|
|
579
|
+
return `this app needs Python ${py} and the runner has ${RUNTIME_VERSIONS.python} \u2014 widen requires-python in pyproject.toml to accept ${RUNTIME_VERSIONS.python}, or wait for the runner to move. Nothing in the code can fix this one.`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const engines = manifests.packageJson?.engines?.node;
|
|
583
|
+
if (engines) {
|
|
584
|
+
const need = lowestAccepted(engines);
|
|
585
|
+
if (need && below(RUNTIME_VERSIONS.node, need)) {
|
|
586
|
+
return `this app needs Node ${engines} and the runner has ${RUNTIME_VERSIONS.node} \u2014 widen engines.node in package.json to accept ${RUNTIME_VERSIONS.node}, or wait for the runner to move. Nothing in the code can fix this one.`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// lib/repo-runtime.ts
|
|
593
|
+
var RUNTIME_LANGUAGES = ["python", "node", "go", "rust", "ruby", "php", "java"];
|
|
594
|
+
var NO_PIN = /^(system|latest|current|lts|stable|nightly|beta|node|default|\*)$|^(ref|path|pypy|graalvm|truffleruby|jruby|conda|miniconda|anaconda|mamba)[:@-]?/i;
|
|
595
|
+
var PLATFORM_DEFAULT_VERSION = {
|
|
596
|
+
python: "3.14",
|
|
597
|
+
node: "24",
|
|
598
|
+
go: "1.24",
|
|
599
|
+
rust: "1.85",
|
|
600
|
+
ruby: "3.4",
|
|
601
|
+
php: "8.4",
|
|
602
|
+
java: "21"
|
|
603
|
+
};
|
|
604
|
+
var KNOWN_VERSIONS = {
|
|
605
|
+
python: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"],
|
|
606
|
+
// Even majors are the LTS lines, which is what `lts/*` and `lts/<codename>` mean.
|
|
607
|
+
node: ["18", "20", "22", "24"],
|
|
608
|
+
go: ["1.21", "1.22", "1.23", "1.24"],
|
|
609
|
+
rust: ["1.78", "1.79", "1.80", "1.81", "1.82", "1.83", "1.84", "1.85"],
|
|
610
|
+
ruby: ["3.0", "3.1", "3.2", "3.3", "3.4"],
|
|
611
|
+
php: ["8.0", "8.1", "8.2", "8.3", "8.4"],
|
|
612
|
+
// eclipse-temurin publishes majors and full builds; only the majors are stable
|
|
613
|
+
// names. See `javaTag` for why every Java answer is reduced to one.
|
|
614
|
+
java: ["8", "11", "17", "21", "24", "25"]
|
|
615
|
+
};
|
|
616
|
+
var NODE_LTS_CODENAMES = {
|
|
617
|
+
argon: "4",
|
|
618
|
+
boron: "6",
|
|
619
|
+
carbon: "8",
|
|
620
|
+
dubnium: "10",
|
|
621
|
+
erbium: "12",
|
|
622
|
+
fermium: "14",
|
|
623
|
+
gallium: "16",
|
|
624
|
+
hydrogen: "18",
|
|
625
|
+
iron: "20",
|
|
626
|
+
jod: "22",
|
|
627
|
+
krypton: "24"
|
|
628
|
+
};
|
|
629
|
+
var NEWEST_NODE_LTS = KNOWN_VERSIONS.node.filter((v) => Number(v) % 2 === 0).at(-1);
|
|
630
|
+
var TOOL_ALIASES = {
|
|
631
|
+
python: "python",
|
|
632
|
+
nodejs: "node",
|
|
633
|
+
node: "node",
|
|
634
|
+
golang: "go",
|
|
635
|
+
go: "go",
|
|
636
|
+
rust: "rust",
|
|
637
|
+
ruby: "ruby",
|
|
638
|
+
php: "php",
|
|
639
|
+
java: "java"
|
|
640
|
+
};
|
|
641
|
+
var RuntimeVersionError = class extends Error {
|
|
642
|
+
};
|
|
643
|
+
var ZERO = [0, 0, 0];
|
|
644
|
+
function parts(v) {
|
|
645
|
+
const m = v.trim().match(/^v?(\d+(?:\.\d+)*)$/);
|
|
646
|
+
return m ? m[1].split(".").map(Number) : null;
|
|
647
|
+
}
|
|
648
|
+
function ver(p) {
|
|
649
|
+
return [p[0] ?? 0, p[1] ?? 0, p[2] ?? 0];
|
|
650
|
+
}
|
|
651
|
+
function cmp(a, b) {
|
|
652
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
653
|
+
return 0;
|
|
654
|
+
}
|
|
655
|
+
function bump(p, at) {
|
|
656
|
+
const next = p.slice(0, at + 1);
|
|
657
|
+
next[at] = (next[at] ?? 0) + 1;
|
|
658
|
+
return ver(next);
|
|
659
|
+
}
|
|
660
|
+
function familySpan(v) {
|
|
661
|
+
const p = parts(v);
|
|
662
|
+
if (!p) return null;
|
|
663
|
+
return { lo: ver(p), loInc: true, hi: bump(p, p.length - 1), hiInc: false };
|
|
664
|
+
}
|
|
665
|
+
function overlaps(a, b) {
|
|
666
|
+
if (b.hi) {
|
|
667
|
+
const c = cmp(a.lo, b.hi);
|
|
668
|
+
if (c > 0 || c === 0 && !(a.loInc && b.hiInc)) return false;
|
|
669
|
+
}
|
|
670
|
+
if (a.hi) {
|
|
671
|
+
const c = cmp(b.lo, a.hi);
|
|
672
|
+
if (c > 0 || c === 0 && !(b.loInc && a.hiInc)) return false;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
function clauseSpan(clause) {
|
|
677
|
+
const c = clause.trim();
|
|
678
|
+
if (!c) return null;
|
|
679
|
+
const wild = c.match(/^(>=|<=|==|!=|~=|~>|\^|~|=|>|<)?\s*v?(\d+(?:\.\d+)*)\.(?:\*|x)$/i);
|
|
680
|
+
if (wild) {
|
|
681
|
+
const span = familySpan(wild[2]);
|
|
682
|
+
if (!span) return null;
|
|
683
|
+
return wild[1] === "!=" ? { exclude: span } : span;
|
|
684
|
+
}
|
|
685
|
+
const m = c.match(/^(>=|<=|==|!=|~=|~>|\^|~|=|>|<)?\s*v?(\d+(?:\.\d+)*)$/);
|
|
686
|
+
if (!m) return null;
|
|
687
|
+
const op = m[1] ?? "=";
|
|
688
|
+
const p = parts(m[2]);
|
|
689
|
+
const lo = ver(p);
|
|
690
|
+
switch (op) {
|
|
691
|
+
case ">=":
|
|
692
|
+
return { lo, loInc: true, hi: null, hiInc: false };
|
|
693
|
+
case ">":
|
|
694
|
+
return { lo, loInc: false, hi: null, hiInc: false };
|
|
695
|
+
case "<":
|
|
696
|
+
return { lo: ZERO, loInc: true, hi: lo, hiInc: false };
|
|
697
|
+
case "<=":
|
|
698
|
+
return { lo: ZERO, loInc: true, hi: lo, hiInc: true };
|
|
699
|
+
case "=":
|
|
700
|
+
case "==":
|
|
701
|
+
return familySpan(m[2]);
|
|
702
|
+
case "!=": {
|
|
703
|
+
const span = familySpan(m[2]);
|
|
704
|
+
return span ? { exclude: span } : null;
|
|
705
|
+
}
|
|
706
|
+
// npm: `^1.2.3` → <2.0.0, but `^0.2.3` → <0.3.0. Nobody pins a runtime below
|
|
707
|
+
// 1.0, and the rule is cheap enough to get right rather than to assume.
|
|
708
|
+
case "^": {
|
|
709
|
+
const at = p[0] === 0 ? p[1] === 0 ? 2 : 1 : 0;
|
|
710
|
+
return { lo, loInc: true, hi: bump(p, Math.min(at, p.length - 1)), hiInc: false };
|
|
711
|
+
}
|
|
712
|
+
// npm tilde: `~1.2.3` and `~1.2` both stop at 1.3.0; `~1` stops at 2.0.0.
|
|
713
|
+
case "~":
|
|
714
|
+
return { lo, loInc: true, hi: bump(p, p.length >= 2 ? 1 : 0), hiInc: false };
|
|
715
|
+
// PEP 440 `~=` and Ruby's pessimistic `~>` are the same rule: drop the last
|
|
716
|
+
// component that was written, and bump the one before it.
|
|
717
|
+
case "~=":
|
|
718
|
+
case "~>":
|
|
719
|
+
return { lo, loInc: true, hi: bump(p, Math.max(0, p.length - 2)), hiInc: false };
|
|
720
|
+
default:
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function hyphenSpan(alt) {
|
|
725
|
+
const m = alt.match(/^v?(\d+(?:\.\d+)*)(?:\.(?:\*|x))*\s+-\s+v?(\d+(?:\.\d+)*)(?:\.(?:\*|x))*$/i);
|
|
726
|
+
if (!m) return null;
|
|
727
|
+
const lo = parts(m[1]);
|
|
728
|
+
const hi = familySpan(m[2]);
|
|
729
|
+
if (!lo || !hi) return null;
|
|
730
|
+
return { lo: ver(lo), loInc: true, hi: hi.hi, hiInc: false };
|
|
731
|
+
}
|
|
732
|
+
function satisfying(language, spec) {
|
|
733
|
+
const alternatives = spec.split("||").map((s) => s.trim()).filter(Boolean);
|
|
734
|
+
if (!alternatives.length) return null;
|
|
735
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
736
|
+
for (const alt of alternatives) {
|
|
737
|
+
const hyphen = hyphenSpan(alt);
|
|
738
|
+
if (hyphen) {
|
|
739
|
+
for (const known of KNOWN_VERSIONS[language]) {
|
|
740
|
+
if (overlaps(familySpan(known), hyphen)) accepted.add(known);
|
|
741
|
+
}
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const clauses = alt.split(/\s*,\s*|\s+/).filter(Boolean);
|
|
745
|
+
const spans = [];
|
|
746
|
+
const excluded = [];
|
|
747
|
+
for (const clause of clauses) {
|
|
748
|
+
const parsed = clauseSpan(clause);
|
|
749
|
+
if (!parsed) return null;
|
|
750
|
+
if ("exclude" in parsed) excluded.push(parsed.exclude);
|
|
751
|
+
else spans.push(parsed);
|
|
752
|
+
}
|
|
753
|
+
if (!spans.length && !excluded.length) return null;
|
|
754
|
+
for (const known of KNOWN_VERSIONS[language]) {
|
|
755
|
+
const fam = familySpan(known);
|
|
756
|
+
if (!spans.every((s) => overlaps(fam, s))) continue;
|
|
757
|
+
if (excluded.some((e) => cmp(e.lo, fam.lo) <= 0 && e.hi && fam.hi && cmp(fam.hi, e.hi) <= 0)) continue;
|
|
758
|
+
accepted.add(known);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return KNOWN_VERSIONS[language].filter((v) => accepted.has(v));
|
|
762
|
+
}
|
|
763
|
+
function isExact(spec) {
|
|
764
|
+
return /^v?\d+(\.\d+)*$/.test(spec.trim());
|
|
765
|
+
}
|
|
766
|
+
var FILE_READS = [
|
|
767
|
+
["toolVersions", [".tool-versions"]],
|
|
768
|
+
["miseToml", ["mise.toml", ".mise.toml"]],
|
|
769
|
+
["pythonVersion", [".python-version"]],
|
|
770
|
+
["runtimeTxt", ["runtime.txt"]],
|
|
771
|
+
["pyproject", ["pyproject.toml"]],
|
|
772
|
+
["nvmrc", [".nvmrc"]],
|
|
773
|
+
["nodeVersion", [".node-version"]],
|
|
774
|
+
["goMod", ["go.mod"]],
|
|
775
|
+
["rustToolchainToml", ["rust-toolchain.toml"]],
|
|
776
|
+
["rustToolchain", ["rust-toolchain"]],
|
|
777
|
+
["rubyVersion", [".ruby-version"]],
|
|
778
|
+
["gemfile", ["Gemfile"]],
|
|
779
|
+
["sdkmanrc", [".sdkmanrc"]],
|
|
780
|
+
["pomXml", ["pom.xml"]],
|
|
781
|
+
["buildGradle", ["build.gradle", "build.gradle.kts"]]
|
|
782
|
+
];
|
|
783
|
+
function readRuntimeFiles(dir) {
|
|
784
|
+
const text = (names2) => {
|
|
785
|
+
for (const n of names2) {
|
|
786
|
+
const p = (0, import_node_path4.join)(dir, n);
|
|
787
|
+
try {
|
|
788
|
+
if ((0, import_node_fs4.existsSync)(p)) return (0, import_node_fs4.readFileSync)(p, "utf8");
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
};
|
|
794
|
+
const json = (name) => {
|
|
795
|
+
try {
|
|
796
|
+
return JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path4.join)(dir, name), "utf8"));
|
|
797
|
+
} catch {
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
const files = {};
|
|
802
|
+
for (const [key, names2] of FILE_READS) files[key] = text(names2);
|
|
803
|
+
files.packageJson = json("package.json");
|
|
804
|
+
files.composerJson = json("composer.json");
|
|
805
|
+
return files;
|
|
806
|
+
}
|
|
807
|
+
var firstLine = (v) => (v ?? "").trim().split("\n")[0].trim();
|
|
808
|
+
function parseToolVersions(text) {
|
|
809
|
+
const out = /* @__PURE__ */ new Map();
|
|
810
|
+
for (const raw of (text ?? "").split("\n")) {
|
|
811
|
+
const line = raw.split("#")[0].trim();
|
|
812
|
+
if (!line) continue;
|
|
813
|
+
const [tool, ...rest] = line.split(/\s+/);
|
|
814
|
+
const language = TOOL_ALIASES[tool?.toLowerCase()];
|
|
815
|
+
const value = rest[0];
|
|
816
|
+
if (language && value && !out.has(language)) out.set(language, value);
|
|
817
|
+
}
|
|
818
|
+
return out;
|
|
819
|
+
}
|
|
820
|
+
function parseMiseTools(text) {
|
|
821
|
+
const out = /* @__PURE__ */ new Map();
|
|
822
|
+
const body = text ?? "";
|
|
823
|
+
const start = body.search(/^\s*\[tools\]\s*$/m);
|
|
824
|
+
if (start === -1) return out;
|
|
825
|
+
const rest = body.slice(start).split("\n").slice(1);
|
|
826
|
+
for (const raw of rest) {
|
|
827
|
+
const line = raw.split("#")[0].trim();
|
|
828
|
+
if (/^\[/.test(line)) break;
|
|
829
|
+
if (!line) continue;
|
|
830
|
+
const m = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
831
|
+
if (!m) continue;
|
|
832
|
+
const language = TOOL_ALIASES[m[1].toLowerCase()];
|
|
833
|
+
if (!language || out.has(language)) continue;
|
|
834
|
+
const value = m[2].match(/["']([^"']+)["']/)?.[1];
|
|
835
|
+
if (value) out.set(language, value);
|
|
836
|
+
}
|
|
837
|
+
return out;
|
|
838
|
+
}
|
|
839
|
+
function javaMajor(raw) {
|
|
840
|
+
let v = raw.trim();
|
|
841
|
+
v = v.replace(/^(?:temurin|openjdk|adoptopenjdk|graalvm|corretto|zulu|liberica|oracle|sapmachine|semeru)[-@]/i, "");
|
|
842
|
+
v = v.replace(/-(?:tem|open|amzn|zulu|librca|ms|sem|graal|oracle|sapmchn)$/i, "");
|
|
843
|
+
v = v.replace(/^JavaVersion\.VERSION_/i, "").replace(/^VERSION_/i, "");
|
|
844
|
+
v = v.replace(/_/g, ".");
|
|
845
|
+
const p = parts(v);
|
|
846
|
+
if (!p) return null;
|
|
847
|
+
const major = p[0] === 1 && p.length > 1 ? p[1] : p[0];
|
|
848
|
+
return String(major);
|
|
849
|
+
}
|
|
850
|
+
var RUST_CHANNEL = /^(stable|beta|nightly)(-\d{4}-\d{2}-\d{2})?$/i;
|
|
851
|
+
function runtimePins(f) {
|
|
852
|
+
const pins = [];
|
|
853
|
+
const add = (language, raw, spec, from, kind = "exact") => {
|
|
854
|
+
const trimmed = spec.trim();
|
|
855
|
+
if (!trimmed || NO_PIN.test(trimmed)) return;
|
|
856
|
+
pins.push({ language, raw, spec: trimmed, from, kind });
|
|
857
|
+
};
|
|
858
|
+
const addFromVersionManager = (tools, from) => {
|
|
859
|
+
for (const [language, value] of tools) {
|
|
860
|
+
if (language === "rust" && RUST_CHANNEL.test(value)) continue;
|
|
861
|
+
const spec = language === "java" ? javaMajor(value) : value.replace(/^v/, "");
|
|
862
|
+
if (spec) add(language, value, spec, from);
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
addFromVersionManager(parseToolVersions(f.toolVersions), ".tool-versions");
|
|
866
|
+
addFromVersionManager(parseMiseTools(f.miseToml), "mise.toml");
|
|
867
|
+
const pv = firstLine(f.pythonVersion);
|
|
868
|
+
if (pv) add("python", pv, pv, ".python-version");
|
|
869
|
+
const rt = firstLine(f.runtimeTxt);
|
|
870
|
+
if (/^python-/i.test(rt)) add("python", rt, rt.replace(/^python-/i, ""), "runtime.txt");
|
|
871
|
+
const requiresPython = f.pyproject?.match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
872
|
+
if (requiresPython) add("python", requiresPython, requiresPython, "pyproject.toml requires-python", "range");
|
|
873
|
+
const poetryTable = f.pyproject?.match(/^[ \t]*\[tool\.poetry\.dependencies\][ \t]*\r?\n([\s\S]*?)(?=^[ \t]*\[|$(?![\s\S]))/m)?.[1];
|
|
874
|
+
const poetryPython = poetryTable?.match(/^\s*python\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
875
|
+
if (poetryPython) {
|
|
876
|
+
add("python", poetryPython, poetryPython, "pyproject.toml [tool.poetry.dependencies] python", "range");
|
|
877
|
+
}
|
|
878
|
+
const nvm = firstLine(f.nvmrc);
|
|
879
|
+
if (nvm) {
|
|
880
|
+
const codename = nvm.toLowerCase().match(/^lts\/(.+)$/)?.[1];
|
|
881
|
+
const lts = codename === "*" ? NEWEST_NODE_LTS : codename ? NODE_LTS_CODENAMES[codename] : null;
|
|
882
|
+
if (codename) {
|
|
883
|
+
if (lts) add("node", nvm, lts, ".nvmrc");
|
|
884
|
+
} else add("node", nvm, nvm.replace(/^v/, ""), ".nvmrc");
|
|
885
|
+
}
|
|
886
|
+
const nodeVersion = firstLine(f.nodeVersion);
|
|
887
|
+
if (nodeVersion) add("node", nodeVersion, nodeVersion.replace(/^v/, ""), ".node-version");
|
|
888
|
+
const pkg = f.packageJson ?? null;
|
|
889
|
+
if (pkg?.volta?.node) add("node", pkg.volta.node, pkg.volta.node.replace(/^v/, ""), "package.json volta.node");
|
|
890
|
+
if (pkg?.engines?.node) add("node", pkg.engines.node, pkg.engines.node, "package.json engines.node", "range");
|
|
891
|
+
const toolchain = f.goMod?.match(/^\s*toolchain\s+go?([0-9][^\s]*)/m)?.[1];
|
|
892
|
+
if (toolchain) add("go", `go${toolchain}`, toolchain, "go.mod toolchain");
|
|
893
|
+
const goLine = f.goMod?.match(/^\s*go\s+([0-9][^\s]*)/m)?.[1];
|
|
894
|
+
if (goLine) add("go", goLine, goLine, "go.mod");
|
|
895
|
+
const channel = f.rustToolchainToml?.match(/^\s*channel\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
896
|
+
if (channel && !RUST_CHANNEL.test(channel.trim())) add("rust", channel, channel, "rust-toolchain.toml");
|
|
897
|
+
const bare = firstLine(f.rustToolchain);
|
|
898
|
+
if (bare && !RUST_CHANNEL.test(bare)) add("rust", bare, bare, "rust-toolchain");
|
|
899
|
+
const rubyVersion = firstLine(f.rubyVersion);
|
|
900
|
+
if (rubyVersion) add("ruby", rubyVersion, rubyVersion.replace(/^ruby-/i, ""), ".ruby-version");
|
|
901
|
+
const gemfileRuby = f.gemfile?.match(/^\s*ruby\s+["']([^"']+)["']/m)?.[1];
|
|
902
|
+
if (gemfileRuby) add("ruby", gemfileRuby, gemfileRuby, "Gemfile", "range");
|
|
903
|
+
const composer = f.composerJson ?? null;
|
|
904
|
+
const platformPhp = composer?.config?.platform?.php;
|
|
905
|
+
if (platformPhp) add("php", platformPhp, platformPhp, "composer.json config.platform.php");
|
|
906
|
+
const requirePhp = composer?.require?.php;
|
|
907
|
+
if (requirePhp) add("php", requirePhp, requirePhp, "composer.json require.php", "range");
|
|
908
|
+
const sdkman = f.sdkmanrc?.match(/^\s*java\s*=\s*(.+)$/m)?.[1];
|
|
909
|
+
if (sdkman) {
|
|
910
|
+
const major = javaMajor(sdkman);
|
|
911
|
+
if (major) add("java", sdkman.trim(), major, ".sdkmanrc");
|
|
912
|
+
}
|
|
913
|
+
for (const field of ["maven.compiler.release", "java.version", "maven.compiler.source", "maven.compiler.target"]) {
|
|
914
|
+
const found = f.pomXml?.match(new RegExp(`<${field.replace(/\./g, "\\.")}>([^<]+)<`))?.[1];
|
|
915
|
+
if (!found) continue;
|
|
916
|
+
const major = javaMajor(found);
|
|
917
|
+
if (major) {
|
|
918
|
+
add("java", found.trim(), major, `pom.xml ${field}`);
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const gradle = f.buildGradle?.match(/JavaLanguageVersion\.of\((\d+)\)/)?.[1] ?? f.buildGradle?.match(/jvmToolchain\((\d+)\)/)?.[1] ?? f.buildGradle?.match(/(?:source|target)Compatibility\s*=?\s*["']?([A-Za-z0-9_.]+)["']?/)?.[1];
|
|
923
|
+
if (gradle) {
|
|
924
|
+
const major = javaMajor(gradle);
|
|
925
|
+
if (major) add("java", gradle.trim(), major, "build.gradle");
|
|
926
|
+
}
|
|
927
|
+
return pins;
|
|
928
|
+
}
|
|
929
|
+
function pinFor(pins, language) {
|
|
930
|
+
return pins.find((p) => p.language === language) ?? null;
|
|
931
|
+
}
|
|
932
|
+
function assertValidTag(tag, where) {
|
|
933
|
+
if (!/^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/.test(tag)) {
|
|
934
|
+
throw new RuntimeVersionError(
|
|
935
|
+
`${where} asks for "${tag}", which is not a version an image can be pulled by.
|
|
936
|
+
Write a concrete version there (like "3.12"), or set "build": { "image": "\u2026" } in supersonic.json to choose the base image yourself.`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function resolveRuntime(language, pin) {
|
|
941
|
+
const fallback = (why) => ({
|
|
942
|
+
language,
|
|
943
|
+
version: PLATFORM_DEFAULT_VERSION[language],
|
|
944
|
+
versionFrom: why
|
|
945
|
+
});
|
|
946
|
+
if (!pin) return fallback("platform default");
|
|
947
|
+
const spec = pin.spec.trim();
|
|
948
|
+
if (pin.kind === "exact" && !isExact(spec)) {
|
|
949
|
+
throw new RuntimeVersionError(
|
|
950
|
+
`${pin.from} says "${pin.raw.trim()}", which is not a version.
|
|
951
|
+
Write a plain version there (like "${PLATFORM_DEFAULT_VERSION[language]}"), or set "build": { "image": "\u2026" } in supersonic.json to choose the base image yourself.`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
if (isExact(spec)) {
|
|
955
|
+
const version = spec.replace(/^v/, "");
|
|
956
|
+
assertValidTag(version, pin.from);
|
|
957
|
+
return {
|
|
958
|
+
language,
|
|
959
|
+
version,
|
|
960
|
+
versionFrom: version === pin.raw.trim() ? pin.from : `${pin.from} ${pin.raw.trim()} \u2192 ${version}`
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const options = satisfying(language, spec);
|
|
964
|
+
if (!options) {
|
|
965
|
+
return fallback(`platform default \u2014 ${pin.from} says "${pin.raw.trim()}", which is not a version range we read`);
|
|
966
|
+
}
|
|
967
|
+
const chosen = options.at(-1);
|
|
968
|
+
if (!chosen) {
|
|
969
|
+
return fallback(
|
|
970
|
+
`platform default \u2014 ${pin.from} asks for "${pin.raw.trim()}" and no ${language} we know of satisfies it`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
assertValidTag(chosen, pin.from);
|
|
974
|
+
return { language, version: chosen, versionFrom: `${pin.from} ${pin.raw.trim()} \u2192 ${chosen}` };
|
|
975
|
+
}
|
|
976
|
+
var trim = (v) => (v ?? "").trim().split("\n")[0].trim();
|
|
977
|
+
function repoRuntime(f) {
|
|
978
|
+
const pv = trim(f.pythonVersion);
|
|
979
|
+
if (pv) return { language: "python", spec: pv, from: ".python-version" };
|
|
980
|
+
const rt = trim(f.runtimeTxt);
|
|
981
|
+
if (/^python-/i.test(rt)) return { language: "python", spec: rt.replace(/^python-/i, ""), from: "runtime.txt" };
|
|
982
|
+
const nvm = trim(f.nvmrc);
|
|
983
|
+
if (nvm) return { language: "node", spec: nvm.replace(/^v/, ""), from: ".nvmrc" };
|
|
984
|
+
const requires = f.pyproject?.match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
985
|
+
if (requires) return { language: "python", spec: requires.trim(), from: "pyproject.toml" };
|
|
986
|
+
const engines = f.packageJson?.engines?.node;
|
|
987
|
+
if (engines) return { language: "node", spec: engines.trim(), from: "package.json" };
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
function pair(v) {
|
|
991
|
+
const m = v.match(/^(\d+)(?:\.(\d+))?/);
|
|
992
|
+
return m ? [Number(m[1]), Number(m[2] ?? 0)] : null;
|
|
993
|
+
}
|
|
994
|
+
function ge(have, need) {
|
|
995
|
+
return have[0] > need[0] || have[0] === need[0] && have[1] >= need[1];
|
|
996
|
+
}
|
|
997
|
+
function runnerServes(r) {
|
|
998
|
+
const have = pair(RUNTIME_VERSIONS[r.language]);
|
|
999
|
+
if (!have) return false;
|
|
1000
|
+
const spec = r.spec.trim();
|
|
1001
|
+
if (/^\d+(\.\d+)*$/.test(spec)) {
|
|
1002
|
+
const want = pair(spec);
|
|
1003
|
+
return spec.includes(".") ? have[0] === want[0] && have[1] === want[1] : have[0] === want[0];
|
|
1004
|
+
}
|
|
1005
|
+
const clauses = spec.split(/\s*,\s*|\s+/).filter(Boolean);
|
|
1006
|
+
if (!clauses.length) return false;
|
|
1007
|
+
for (const c of clauses) {
|
|
1008
|
+
const m = c.match(/^(>=|<=|==|!=|~=|\^|>|<)?\s*v?(\d+(?:\.\d+)*)$/);
|
|
1009
|
+
if (!m) return false;
|
|
1010
|
+
const [, op = ">=", v] = m;
|
|
1011
|
+
const want = pair(v);
|
|
1012
|
+
const exact = have[0] === want[0] && have[1] === want[1];
|
|
1013
|
+
switch (op) {
|
|
1014
|
+
case ">=":
|
|
1015
|
+
if (!ge(have, want)) return false;
|
|
1016
|
+
break;
|
|
1017
|
+
case ">":
|
|
1018
|
+
if (ge(want, have)) return false;
|
|
1019
|
+
break;
|
|
1020
|
+
case "<":
|
|
1021
|
+
if (ge(have, want)) return false;
|
|
1022
|
+
break;
|
|
1023
|
+
case "<=":
|
|
1024
|
+
if (!ge(want, have) && !exact) return false;
|
|
1025
|
+
break;
|
|
1026
|
+
case "==":
|
|
1027
|
+
if (!exact) return false;
|
|
1028
|
+
break;
|
|
1029
|
+
case "!=":
|
|
1030
|
+
if (exact) return false;
|
|
1031
|
+
break;
|
|
1032
|
+
// `~=3.11` and `^3.11` both mean "this minor, or compatible with it", and
|
|
1033
|
+
// the honest answer for a runner pinned to one minor is only yes when it IS
|
|
1034
|
+
// that minor.
|
|
1035
|
+
case "~=":
|
|
1036
|
+
case "^":
|
|
1037
|
+
if (!exact) return false;
|
|
1038
|
+
break;
|
|
1039
|
+
default:
|
|
1040
|
+
return false;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// lib/detect.ts
|
|
1047
|
+
function yarnInstall(dir) {
|
|
1048
|
+
const lock = readText(dir, "yarn.lock") ?? "";
|
|
1049
|
+
const berry = /^__metadata:/m.test(lock) || /^\s{2}version:\s*\d/m.test(lock);
|
|
1050
|
+
return berry ? "corepack enable && yarn install --immutable" : "corepack enable && yarn install --frozen-lockfile";
|
|
1051
|
+
}
|
|
1052
|
+
var PACKAGE_RULES = {
|
|
1053
|
+
python: [
|
|
1054
|
+
// `uv sync` builds the local project, and the cached layer runs before the
|
|
1055
|
+
// source is copied. `--no-install-project` is what lets it be cached at all.
|
|
1056
|
+
{
|
|
1057
|
+
file: "uv.lock",
|
|
1058
|
+
manager: "uv",
|
|
1059
|
+
install: "pip install --no-cache-dir uv && uv sync --frozen --no-dev --no-install-project",
|
|
1060
|
+
installProject: "uv sync --frozen --no-dev"
|
|
1061
|
+
},
|
|
1062
|
+
// `POETRY_VIRTUALENVS_CREATE=false` is not a preference, it is what makes the
|
|
1063
|
+
// installed packages reachable. Poetry's default is a venv under
|
|
1064
|
+
// `~/.cache/pypoetry/virtualenvs/<hash>` — a path nothing in the image knows,
|
|
1065
|
+
// and one this Dockerfile cannot put on `PATH` because the hash is computed at
|
|
1066
|
+
// install time. So the build was green, every dependency was installed, and
|
|
1067
|
+
// the container exited 127 on its own entry point. Installing into the image's
|
|
1068
|
+
// system python is what a container wants anyway: the isolation a venv buys is
|
|
1069
|
+
// already the container's job.
|
|
1070
|
+
{
|
|
1071
|
+
file: "poetry.lock",
|
|
1072
|
+
manager: "poetry",
|
|
1073
|
+
install: "pip install --no-cache-dir poetry && POETRY_VIRTUALENVS_CREATE=false poetry install --no-root --only main"
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
file: "Pipfile.lock",
|
|
1077
|
+
manager: "pipenv",
|
|
1078
|
+
install: "pip install --no-cache-dir pipenv && pipenv install --deploy --system"
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
file: "requirements.txt",
|
|
1082
|
+
manager: "pip",
|
|
1083
|
+
install: "pip install --no-cache-dir -r requirements.txt"
|
|
1084
|
+
},
|
|
1085
|
+
// No `--no-install-project` equivalent exists for `pip install .`, so this one
|
|
1086
|
+
// installs after `COPY . .` and forgoes the cached layer. Stated rather than
|
|
1087
|
+
// emitted as a Dockerfile that cannot build.
|
|
1088
|
+
{ file: "pyproject.toml", manager: "pip", installProject: "pip install --no-cache-dir ." }
|
|
1089
|
+
],
|
|
1090
|
+
node: [
|
|
1091
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm", install: "corepack enable && pnpm install --frozen-lockfile" },
|
|
1092
|
+
{ file: "yarn.lock", manager: "yarn", installFor: yarnInstall },
|
|
1093
|
+
// bun >= 1.2 writes a TEXT lockfile. Listing only `bun.lockb` drops every
|
|
1094
|
+
// modern bun repo to `npm install`, which cannot install a bun workspace.
|
|
1095
|
+
{ file: "bun.lock", manager: "bun", install: "bun install --frozen-lockfile" },
|
|
1096
|
+
{ file: "bun.lockb", manager: "bun", install: "bun install --frozen-lockfile" },
|
|
1097
|
+
{ file: "package-lock.json", manager: "npm", install: "npm ci" },
|
|
1098
|
+
// `npm ci` refuses to run without a lockfile, and a lockfile-less project is
|
|
1099
|
+
// the common case for the people this platform is for.
|
|
1100
|
+
{ file: "package.json", manager: "npm", install: "npm install" }
|
|
1101
|
+
],
|
|
1102
|
+
go: [
|
|
1103
|
+
{ file: "go.sum", manager: "go", install: "go mod download" },
|
|
1104
|
+
{ file: "go.mod", manager: "go", install: "go mod download" }
|
|
1105
|
+
],
|
|
1106
|
+
rust: [
|
|
1107
|
+
// cargo resolves and compiles in one step; there is no install to cache apart
|
|
1108
|
+
// from the build itself.
|
|
1109
|
+
{ file: "Cargo.lock", manager: "cargo" },
|
|
1110
|
+
{ file: "Cargo.toml", manager: "cargo" }
|
|
1111
|
+
],
|
|
1112
|
+
ruby: [
|
|
1113
|
+
{ file: "Gemfile.lock", manager: "bundler", install: "bundle install --without development test" },
|
|
1114
|
+
{ file: "Gemfile", manager: "bundler", install: "bundle install --without development test" }
|
|
1115
|
+
],
|
|
1116
|
+
php: [
|
|
1117
|
+
{ file: "composer.lock", manager: "composer", install: "composer install --no-dev --optimize-autoloader" },
|
|
1118
|
+
{ file: "composer.json", manager: "composer", install: "composer install --no-dev --optimize-autoloader" }
|
|
1119
|
+
],
|
|
1120
|
+
// Java is the row docs/MAKE-DEPLOYS-WORK.md Part 8 names as missing: the
|
|
1121
|
+
// manifest COPY has no `pom.xml` or `build.gradle*` either, so "now covers Go,
|
|
1122
|
+
// Ruby, Java and PHP" was false for Java out of the box. Both halves are here.
|
|
1123
|
+
java: [
|
|
1124
|
+
{ file: "pom.xml", manager: "maven", install: "mvn -B -q -DskipTests dependency:go-offline" },
|
|
1125
|
+
{ file: "build.gradle.kts", manager: "gradle" },
|
|
1126
|
+
{ file: "build.gradle", manager: "gradle" }
|
|
1127
|
+
]
|
|
1128
|
+
};
|
|
1129
|
+
var PACKAGE_MANIFESTS = [
|
|
1130
|
+
...new Set(RUNTIME_LANGUAGES.flatMap((l) => PACKAGE_RULES[l].map((r) => r.file)))
|
|
1131
|
+
];
|
|
1132
|
+
var readText = (dir, file) => {
|
|
1133
|
+
try {
|
|
1134
|
+
const p = (0, import_node_path5.join)(dir, file);
|
|
1135
|
+
return (0, import_node_fs5.existsSync)(p) ? (0, import_node_fs5.readFileSync)(p, "utf8") : null;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
var readJson = (dir, file) => {
|
|
1141
|
+
const raw = readText(dir, file);
|
|
1142
|
+
if (!raw) return null;
|
|
1143
|
+
try {
|
|
1144
|
+
const v = JSON.parse(raw);
|
|
1145
|
+
return v && typeof v === "object" ? v : null;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
var hasFile = (dir, file) => {
|
|
1151
|
+
try {
|
|
1152
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(dir, file));
|
|
1153
|
+
} catch {
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
function nodeDeps(pkg) {
|
|
1158
|
+
const p = pkg ?? {};
|
|
1159
|
+
return /* @__PURE__ */ new Set([...Object.keys(p.dependencies ?? {}), ...Object.keys(p.devDependencies ?? {})]);
|
|
1160
|
+
}
|
|
1161
|
+
function pythonDepsText(dir) {
|
|
1162
|
+
return [
|
|
1163
|
+
readText(dir, "requirements.txt") ?? "",
|
|
1164
|
+
readText(dir, "pyproject.toml") ?? "",
|
|
1165
|
+
readText(dir, "Pipfile") ?? ""
|
|
1166
|
+
].join("\n").toLowerCase();
|
|
1167
|
+
}
|
|
1168
|
+
var CONFIGS = {
|
|
1169
|
+
next: ["next.config.js", "next.config.mjs", "next.config.cjs", "next.config.ts"],
|
|
1170
|
+
astro: ["astro.config.mjs", "astro.config.js", "astro.config.cjs", "astro.config.ts"],
|
|
1171
|
+
svelte: ["svelte.config.js", "svelte.config.mjs", "svelte.config.ts"]
|
|
1172
|
+
};
|
|
1173
|
+
function dirFacts(dir, rel) {
|
|
1174
|
+
const pkg = readJson(dir, "package.json");
|
|
1175
|
+
const readFirst = (names2) => {
|
|
1176
|
+
for (const n of names2) {
|
|
1177
|
+
const s = readText(dir, n);
|
|
1178
|
+
if (s) return s;
|
|
1179
|
+
}
|
|
1180
|
+
return null;
|
|
1181
|
+
};
|
|
1182
|
+
return {
|
|
1183
|
+
dir,
|
|
1184
|
+
rel,
|
|
1185
|
+
pkg,
|
|
1186
|
+
deps: nodeDeps(pkg),
|
|
1187
|
+
pythonText: pythonDepsText(dir),
|
|
1188
|
+
nextConfig: readFirst(CONFIGS.next),
|
|
1189
|
+
astroConfig: readFirst(CONFIGS.astro),
|
|
1190
|
+
svelteConfig: readFirst(CONFIGS.svelte),
|
|
1191
|
+
gemfile: readText(dir, "Gemfile"),
|
|
1192
|
+
composer: readJson(dir, "composer.json"),
|
|
1193
|
+
cargo: readText(dir, "Cargo.toml")
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function astroHasAdapter(src) {
|
|
1197
|
+
const s = src ?? "";
|
|
1198
|
+
return /adapter\s*:/.test(s) || /@astrojs\/(node|vercel|netlify|cloudflare|deno)/.test(s);
|
|
1199
|
+
}
|
|
1200
|
+
function nextIsExport(src) {
|
|
1201
|
+
return /output\s*:\s*["'`]export["'`]/.test(src ?? "");
|
|
1202
|
+
}
|
|
1203
|
+
function svelteHasNodeAdapter(f) {
|
|
1204
|
+
return f.deps.has("@sveltejs/adapter-node") || /@sveltejs\/adapter-node/.test(f.svelteConfig ?? "");
|
|
1205
|
+
}
|
|
1206
|
+
function djangoPackage(dir) {
|
|
1207
|
+
const manage = readText(dir, "manage.py") ?? "";
|
|
1208
|
+
const declared = manage.match(/DJANGO_SETTINGS_MODULE["']\s*,\s*["']([\w.]+)["']/)?.[1];
|
|
1209
|
+
if (declared) return declared.split(".")[0];
|
|
1210
|
+
try {
|
|
1211
|
+
for (const e of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1212
|
+
if (e.isDirectory() && (0, import_node_fs5.existsSync)((0, import_node_path5.join)(dir, e.name, "wsgi.py"))) return e.name;
|
|
1213
|
+
}
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
function cargoBinary(src) {
|
|
1219
|
+
const pkgSection = (src ?? "").split(/^\s*\[/m).find((s) => s.startsWith("package]"));
|
|
1220
|
+
return pkgSection?.match(/^\s*name\s*=\s*["']([^"']+)["']/m)?.[1] ?? null;
|
|
1221
|
+
}
|
|
1222
|
+
var FRAMEWORK_START = [
|
|
1223
|
+
{
|
|
1224
|
+
when: (f) => f.deps.has("next") || Boolean(f.nextConfig),
|
|
1225
|
+
start: (f) => nextIsExport(f.nextConfig) ? null : "next start -p $PORT",
|
|
1226
|
+
token: "next"
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
when: (f) => f.deps.has("nuxt") || hasFile(f.dir, "nuxt.config.ts") || hasFile(f.dir, "nuxt.config.js"),
|
|
1230
|
+
start: () => "node .output/server/index.mjs",
|
|
1231
|
+
token: "nuxt"
|
|
1232
|
+
},
|
|
1233
|
+
// React Router 7 is Remix; the framework kept the deploy shape and changed the
|
|
1234
|
+
// name, and both spellings are in the wild.
|
|
1235
|
+
{
|
|
1236
|
+
when: (f) => f.deps.has("@react-router/serve") || f.deps.has("@react-router/node") || f.deps.has("@remix-run/serve") || f.deps.has("@remix-run/node"),
|
|
1237
|
+
start: () => "react-router-serve ./build/server/index.js",
|
|
1238
|
+
token: "remix"
|
|
1239
|
+
},
|
|
1240
|
+
{
|
|
1241
|
+
when: (f) => f.deps.has("astro") || Boolean(f.astroConfig),
|
|
1242
|
+
start: (f) => astroHasAdapter(f.astroConfig) ? "node ./dist/server/entry.mjs" : null,
|
|
1243
|
+
token: "astro"
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
when: (f) => f.deps.has("@sveltejs/kit") || Boolean(f.svelteConfig),
|
|
1247
|
+
start: (f) => svelteHasNodeAdapter(f) ? "node build" : null,
|
|
1248
|
+
token: "svelte"
|
|
1249
|
+
},
|
|
1250
|
+
{ when: (f) => f.deps.has("@nestjs/core"), start: () => "node dist/main.js", token: "nest" },
|
|
1251
|
+
{
|
|
1252
|
+
when: (f) => hasFile(f.dir, "manage.py"),
|
|
1253
|
+
start: (f) => `gunicorn ${djangoPackage(f.dir) ?? "config"}.wsgi:application -b :$PORT`,
|
|
1254
|
+
extra: "gunicorn",
|
|
1255
|
+
token: "django"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
when: (f) => hasFile(f.dir, "main.py") && /fastapi/.test(f.pythonText),
|
|
1259
|
+
start: () => "uvicorn main:app --host 0.0.0.0 --port $PORT",
|
|
1260
|
+
extra: "uvicorn",
|
|
1261
|
+
token: "fastapi"
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
when: (f) => hasFile(f.dir, "app/main.py") && /fastapi/.test(f.pythonText),
|
|
1265
|
+
start: () => "uvicorn app.main:app --host 0.0.0.0 --port $PORT",
|
|
1266
|
+
extra: "uvicorn",
|
|
1267
|
+
token: "fastapi"
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
when: (f) => hasFile(f.dir, "app.py") && /flask/.test(f.pythonText),
|
|
1271
|
+
start: () => "gunicorn app:app -b :$PORT",
|
|
1272
|
+
extra: "gunicorn",
|
|
1273
|
+
token: "flask"
|
|
1274
|
+
},
|
|
1275
|
+
{ when: (f) => hasFile(f.dir, "wsgi.py"), start: () => "gunicorn wsgi:app -b :$PORT", extra: "gunicorn" },
|
|
1276
|
+
{
|
|
1277
|
+
when: (f) => Boolean(f.gemfile) && /rails/i.test(f.gemfile ?? ""),
|
|
1278
|
+
start: () => "bundle exec rails s -b 0.0.0.0 -p $PORT",
|
|
1279
|
+
token: "rails"
|
|
1280
|
+
},
|
|
1281
|
+
{ when: (f) => hasFile(f.dir, "config.ru"), start: () => "bundle exec rackup -p $PORT -o 0.0.0.0" },
|
|
1282
|
+
// Only when there is a binary to run. With several main packages and no
|
|
1283
|
+
// convention to choose between them, nothing builds `/app/server` — so naming
|
|
1284
|
+
// it as the start command would be a container that exits 127 on a file the
|
|
1285
|
+
// build never produced.
|
|
1286
|
+
{ when: (f) => hasFile(f.dir, "go.mod"), start: (f) => goMainPackage(f.dir).pattern ? "/app/server" : null },
|
|
1287
|
+
{
|
|
1288
|
+
when: (f) => Boolean(f.cargo),
|
|
1289
|
+
start: (f) => `/app/target/release/${cargoBinary(f.cargo) ?? "app"}`
|
|
1290
|
+
},
|
|
1291
|
+
// The two PHP rows are DEVELOPMENT servers. `php -S` and `php artisan serve` are
|
|
1292
|
+
// single-threaded and serialise requests, and Cloud Run's default concurrency is
|
|
1293
|
+
// 80 — so a burst of 80 requests queues behind one worker and the app looks
|
|
1294
|
+
// hung. They are here as a first-deploy default on the explicit condition that
|
|
1295
|
+
// `phpConcurrency` below pins concurrency to 1 for them; replacing them with
|
|
1296
|
+
// frankenphp or php-fpm+nginx is the real fix and is not this step's work.
|
|
1297
|
+
{
|
|
1298
|
+
when: (f) => hasFile(f.dir, "artisan"),
|
|
1299
|
+
start: () => "php artisan serve --host 0.0.0.0 --port $PORT",
|
|
1300
|
+
token: "laravel"
|
|
1301
|
+
},
|
|
1302
|
+
{ when: (f) => hasFile(f.dir, "index.php"), start: () => "php -S 0.0.0.0:$PORT" }
|
|
1303
|
+
];
|
|
1304
|
+
var PYTHON_ENTRIES = ["main.py", "app/main.py", "src/main.py", "app.py", "api/main.py", "src/app.py"];
|
|
1305
|
+
var PYTHON_RUNNABLE = [...PYTHON_ENTRIES, "manage.py", "wsgi.py", "asgi.py"];
|
|
1306
|
+
function bindToPort(cmd) {
|
|
1307
|
+
return cmd.replace(/(--port[= ])\d+/g, "$1$PORT").replace(/(--bind[= ]\S*?:)\d+/g, "$1$PORT").replace(/(-b\s+\S*?:)\d+/g, "$1$PORT").replace(/(-p\s+)\d+/g, "$1$PORT");
|
|
1308
|
+
}
|
|
1309
|
+
function pythonModule(serviceDir) {
|
|
1310
|
+
for (const entry of PYTHON_ENTRIES) {
|
|
1311
|
+
if ((0, import_node_fs5.existsSync)((0, import_node_path5.join)(serviceDir, entry))) return entry.replace(/\.py$/, "").split("/").join(".");
|
|
1312
|
+
}
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
function pythonInstall(serviceDir, detected) {
|
|
1316
|
+
const rule = PACKAGE_RULES.python.find((r) => (0, import_node_fs5.existsSync)((0, import_node_path5.join)(serviceDir, r.file)));
|
|
1317
|
+
if (!rule) return void 0;
|
|
1318
|
+
if (rule.file === "requirements.txt" && detected) return detected;
|
|
1319
|
+
const full = [rule.install, rule.installProject].filter(Boolean).join(" && ");
|
|
1320
|
+
return full || void 0;
|
|
1321
|
+
}
|
|
1322
|
+
function serviceLanguage(language, isStatic = false) {
|
|
1323
|
+
if (isStatic) return "static";
|
|
1324
|
+
const l = (language ?? "").toLowerCase();
|
|
1325
|
+
if (l.startsWith("python")) return "python";
|
|
1326
|
+
if (l === "node" || /^(java)?script|^typescript/.test(l)) return "node";
|
|
1327
|
+
if (l === "static") return "static";
|
|
1328
|
+
return "other";
|
|
1329
|
+
}
|
|
1330
|
+
var NEEDS = [
|
|
1331
|
+
{ when: (f) => f.deps.has("canvas"), packages: ["libcairo2-dev", "libpango1.0-dev", "libjpeg-dev"] },
|
|
1332
|
+
{ when: (f) => /(^|\n|\s)mysqlclient/.test(f.pythonText), packages: ["default-libmysqlclient-dev", "pkg-config"] },
|
|
1333
|
+
{ when: (f) => /(^|\n|\s)weasyprint/.test(f.pythonText), packages: ["libpango-1.0-0", "libpangoft2-1.0-0"] }
|
|
1334
|
+
];
|
|
1335
|
+
function detectDatabase(f) {
|
|
1336
|
+
const dep = (n) => f.deps.has(n);
|
|
1337
|
+
const py = (n) => new RegExp(`(^|[^\\w-])${n}`, "i").test(f.pythonText);
|
|
1338
|
+
if (dep("@prisma/client") || dep("prisma") || hasFile(f.dir, "prisma/schema.prisma")) {
|
|
1339
|
+
const provider = readText(f.dir, "prisma/schema.prisma")?.match(/provider\s*=\s*"(\w+)"/)?.[1];
|
|
1340
|
+
const engine = provider === "mysql" ? "mysql" : provider === "sqlite" ? "sqlite" : provider === "mongodb" ? "mongodb" : "postgres";
|
|
1341
|
+
return { engine, via: "Prisma" };
|
|
1342
|
+
}
|
|
1343
|
+
if (dep("drizzle-orm")) {
|
|
1344
|
+
return { engine: dep("mysql2") ? "mysql" : dep("better-sqlite3") ? "sqlite" : "postgres", via: "Drizzle" };
|
|
1345
|
+
}
|
|
1346
|
+
if (dep("mongoose")) return { engine: "mongodb", via: "Mongoose" };
|
|
1347
|
+
if (dep("typeorm")) return { engine: dep("mysql2") || dep("mysql") ? "mysql" : "postgres", via: "TypeORM" };
|
|
1348
|
+
if (dep("sequelize")) return { engine: dep("mysql2") || dep("mysql") ? "mysql" : "postgres", via: "Sequelize" };
|
|
1349
|
+
if (dep("pg") || dep("postgres")) return { engine: "postgres", via: "pg" };
|
|
1350
|
+
if (dep("mysql2") || dep("mysql")) return { engine: "mysql", via: "mysql" };
|
|
1351
|
+
if (py("psycopg2")) return { engine: "postgres", via: "psycopg2" };
|
|
1352
|
+
if (py("psycopg")) return { engine: "postgres", via: "psycopg" };
|
|
1353
|
+
if (py("asyncpg")) return { engine: "postgres", via: "asyncpg" };
|
|
1354
|
+
if (py("django")) return { engine: "postgres", via: "Django ORM" };
|
|
1355
|
+
if (py("sqlalchemy")) return { engine: "postgres", via: "SQLAlchemy" };
|
|
1356
|
+
if (py("pymysql") || py("mysqlclient")) return { engine: "mysql", via: "mysql" };
|
|
1357
|
+
if (py("pymongo")) return { engine: "mongodb", via: "pymongo" };
|
|
1358
|
+
const gem = f.gemfile ?? "";
|
|
1359
|
+
if (/^\s*gem\s+["']pg["']/m.test(gem)) return { engine: "postgres", via: "pg" };
|
|
1360
|
+
if (/^\s*gem\s+["']mysql2["']/m.test(gem)) return { engine: "mysql", via: "mysql2" };
|
|
1361
|
+
const goMod = readText(f.dir, "go.mod") ?? "";
|
|
1362
|
+
if (/github\.com\/lib\/pq|github\.com\/jackc\/pgx/.test(goMod)) return { engine: "postgres", via: "pgx" };
|
|
1363
|
+
if (/github\.com\/go-sql-driver\/mysql/.test(goMod)) return { engine: "mysql", via: "go-sql-driver" };
|
|
1364
|
+
const require2 = f.composer?.require ?? {};
|
|
1365
|
+
if ("laravel/framework" in require2) return { engine: "mysql", via: "Eloquent" };
|
|
1366
|
+
return void 0;
|
|
1367
|
+
}
|
|
1368
|
+
function detectRelease(f, procfile, config) {
|
|
1369
|
+
const declared = config?.release ?? config?.preDeploy ?? config?.processes?.release?.command;
|
|
1370
|
+
if (declared?.trim()) return declared.trim();
|
|
1371
|
+
const fromProcfile = procfile?.find((e) => e.name === "release")?.command;
|
|
1372
|
+
if (fromProcfile?.trim()) return fromProcfile.trim();
|
|
1373
|
+
if (hasFile(f.dir, "manage.py")) return "python manage.py migrate --noinput";
|
|
1374
|
+
if (hasFile(f.dir, "alembic.ini")) return "alembic upgrade head";
|
|
1375
|
+
if (hasFile(f.dir, "prisma/schema.prisma")) return "npx --no-install prisma migrate deploy";
|
|
1376
|
+
if (f.gemfile && /rails/i.test(f.gemfile)) return "bundle exec rails db:migrate";
|
|
1377
|
+
return void 0;
|
|
1378
|
+
}
|
|
1379
|
+
var GO_SERVER_DIRS = ["server", "api", "web", "app", "service", "daemon", "http"];
|
|
1380
|
+
function goMainPackage(dir) {
|
|
1381
|
+
const mains = [];
|
|
1382
|
+
const walk = (abs, rel, depth) => {
|
|
1383
|
+
if (depth > 3 || mains.length > 8) return;
|
|
1384
|
+
let entries;
|
|
1385
|
+
try {
|
|
1386
|
+
entries = (0, import_node_fs5.readdirSync)(abs, { withFileTypes: true });
|
|
1387
|
+
} catch {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
let isMain = false;
|
|
1391
|
+
for (const e of entries) {
|
|
1392
|
+
if (e.isFile() && e.name.endsWith(".go") && !e.name.endsWith("_test.go")) {
|
|
1393
|
+
const src = readText(abs, e.name) ?? "";
|
|
1394
|
+
if (/^\s*package\s+main\s*$/m.test(src)) isMain = true;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (isMain) mains.push(rel);
|
|
1398
|
+
for (const e of entries) {
|
|
1399
|
+
if (!e.isDirectory()) continue;
|
|
1400
|
+
if (e.name.startsWith(".") || e.name === "vendor" || e.name === "node_modules") continue;
|
|
1401
|
+
walk((0, import_node_path5.join)(abs, e.name), rel === "." ? `./${e.name}` : `${rel}/${e.name}`, depth + 1);
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
walk(dir, ".", 0);
|
|
1405
|
+
if (mains.length === 1) return { pattern: mains[0], sure: true };
|
|
1406
|
+
if (mains.includes(".")) return { pattern: ".", sure: true };
|
|
1407
|
+
const underCmd = mains.filter((m) => m.startsWith("./cmd/"));
|
|
1408
|
+
if (underCmd.length === 1) return { pattern: underCmd[0], sure: true };
|
|
1409
|
+
const named = GO_SERVER_DIRS.map((n) => underCmd.find((m) => m === `./cmd/${n}`)).filter(Boolean);
|
|
1410
|
+
if (named.length === 1) return { pattern: named[0], sure: true };
|
|
1411
|
+
return { pattern: null, sure: false };
|
|
1412
|
+
}
|
|
1413
|
+
function buildFor(language, manager, f) {
|
|
1414
|
+
if (language === "node") {
|
|
1415
|
+
const scripts = f.pkg?.scripts ?? {};
|
|
1416
|
+
return { build: scripts.build ? `${manager === "bun" ? "bun" : manager} run build` : void 0, sure: true };
|
|
1417
|
+
}
|
|
1418
|
+
if (language === "go") {
|
|
1419
|
+
const main = goMainPackage(f.dir);
|
|
1420
|
+
return main.pattern ? { build: `go build -o /app/server ${main.pattern}`, sure: main.sure } : { sure: false };
|
|
1421
|
+
}
|
|
1422
|
+
if (language === "rust") return { build: "cargo build --release", sure: true };
|
|
1423
|
+
if (language === "java") {
|
|
1424
|
+
return manager === "maven" ? { build: "mvn -B -DskipTests package", sure: true } : { build: `${hasFile(f.dir, "gradlew") ? "./gradlew" : "gradle"} --no-daemon build -x test`, sure: true };
|
|
1425
|
+
}
|
|
1426
|
+
if (language === "ruby" && f.gemfile && /rails/i.test(f.gemfile)) {
|
|
1427
|
+
return { build: "bundle exec rails assets:precompile", sure: true };
|
|
1428
|
+
}
|
|
1429
|
+
return { sure: true };
|
|
1430
|
+
}
|
|
1431
|
+
function staticOutputDir(f) {
|
|
1432
|
+
if (f.deps.has("next")) return nextIsExport(f.nextConfig) ? "out" : void 0;
|
|
1433
|
+
if (f.deps.has("astro") || f.astroConfig) return astroHasAdapter(f.astroConfig) ? void 0 : "dist";
|
|
1434
|
+
if (f.deps.has("@sveltejs/kit") || f.svelteConfig) return svelteHasNodeAdapter(f) ? void 0 : "build";
|
|
1435
|
+
if (f.deps.has("react-scripts")) return "build";
|
|
1436
|
+
if (f.deps.has("vite")) return "dist";
|
|
1437
|
+
if (hasFile(f.dir, "index.html")) return ".";
|
|
1438
|
+
return void 0;
|
|
1439
|
+
}
|
|
1440
|
+
function languagesIn(dir) {
|
|
1441
|
+
return RUNTIME_LANGUAGES.filter((l) => PACKAGE_RULES[l].some((r) => hasFile(dir, r.file)));
|
|
1442
|
+
}
|
|
1443
|
+
function toolchainFor(language, f, rel, repoRoot) {
|
|
1444
|
+
let rule = PACKAGE_RULES[language].find((r) => hasFile(f.dir, r.file));
|
|
1445
|
+
const rootInstall = language === "node" && rel !== "." && workspaceRootOf(repoRoot) ? PACKAGE_RULES.node.find((r) => r.install || r.installFor ? hasFile(repoRoot, r.file) : false) : void 0;
|
|
1446
|
+
const hoisted = rootInstall && rootInstall.file !== "package.json" ? rootInstall : void 0;
|
|
1447
|
+
if (hoisted) rule = hoisted;
|
|
1448
|
+
if (!rule) return null;
|
|
1449
|
+
const installFrom = hoisted ? repoRoot : f.dir;
|
|
1450
|
+
const { build, sure } = buildFor(language, rule.manager, f);
|
|
1451
|
+
const own = pinFor(runtimePins(readRuntimeFiles(f.dir)), language);
|
|
1452
|
+
const inherited = !own && repoRoot !== f.dir ? pinFor(runtimePins(readRuntimeFiles(repoRoot)), language) : null;
|
|
1453
|
+
const runtime = resolveRuntime(
|
|
1454
|
+
language,
|
|
1455
|
+
own ?? (inherited ? { ...inherited, from: `${inherited.from} (repo root)` } : null)
|
|
1456
|
+
);
|
|
1457
|
+
return {
|
|
1458
|
+
tc: {
|
|
1459
|
+
language,
|
|
1460
|
+
version: runtime.version,
|
|
1461
|
+
versionFrom: runtime.versionFrom,
|
|
1462
|
+
packageManager: rule.manager,
|
|
1463
|
+
install: rule.installFor ? rule.installFor(installFrom) : rule.install,
|
|
1464
|
+
installProject: rule.installProject,
|
|
1465
|
+
build,
|
|
1466
|
+
dir: rel,
|
|
1467
|
+
// The root, when the root is where the dependencies live.
|
|
1468
|
+
installDir: hoisted ? "." : void 0
|
|
1469
|
+
},
|
|
1470
|
+
sure
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function workspaceRootOf(dir) {
|
|
1474
|
+
const pkg = readJson(dir, "package.json");
|
|
1475
|
+
const w = (pkg ?? {}).workspaces;
|
|
1476
|
+
return Array.isArray(w) ? w.length > 0 : Boolean(w && typeof w === "object");
|
|
1477
|
+
}
|
|
1478
|
+
function detect(dir, options = {}, rel = ".") {
|
|
1479
|
+
const { run, config } = options;
|
|
1480
|
+
const f = dirFacts(dir, rel);
|
|
1481
|
+
const repoRoot = options.repoRoot ?? (rel === "." ? dir : (0, import_node_path5.resolve)(dir, ...rel.split("/").filter((s) => s && s !== ".").map(() => "..")));
|
|
1482
|
+
const present = languagesIn(dir);
|
|
1483
|
+
const built = present.map((l) => toolchainFor(l, f, rel, repoRoot)).filter((t) => t !== null);
|
|
1484
|
+
let command;
|
|
1485
|
+
let framework;
|
|
1486
|
+
let extra;
|
|
1487
|
+
let confidence = "certain";
|
|
1488
|
+
let procfile = null;
|
|
1489
|
+
try {
|
|
1490
|
+
procfile = readProcfile(dir);
|
|
1491
|
+
} catch {
|
|
1492
|
+
procfile = null;
|
|
1493
|
+
}
|
|
1494
|
+
const configWeb = config?.processes?.web?.command ?? config?.start;
|
|
1495
|
+
const procfileWeb = procfile?.find((e) => e.name === "web")?.command;
|
|
1496
|
+
const pkgScripts = f.pkg?.scripts ?? {};
|
|
1497
|
+
const nodeManager = built.find((b) => b.tc.language === "node")?.tc.packageManager ?? "npm";
|
|
1498
|
+
const staticDir = staticOutputDir(f);
|
|
1499
|
+
const scriptStart = pkgScripts.start && !staticDir ? `${nodeManager} start` : void 0;
|
|
1500
|
+
const declared = run?.trim() || configWeb?.trim() || procfileWeb?.trim() || scriptStart;
|
|
1501
|
+
if (declared) {
|
|
1502
|
+
command = declared;
|
|
1503
|
+
framework = FRAMEWORK_START.find((r) => r.when(f))?.token;
|
|
1504
|
+
} else {
|
|
1505
|
+
const row = FRAMEWORK_START.find((r) => r.when(f));
|
|
1506
|
+
const started = row?.start(f) ?? null;
|
|
1507
|
+
if (row && started) {
|
|
1508
|
+
command = started;
|
|
1509
|
+
framework = row.token;
|
|
1510
|
+
extra = row.extra;
|
|
1511
|
+
confidence = "inferred";
|
|
1512
|
+
} else {
|
|
1513
|
+
framework = row?.token;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (command) command = bindToPort(command);
|
|
1517
|
+
const outputDir = command ? void 0 : staticDir;
|
|
1518
|
+
if (!command && !outputDir) confidence = "guessed";
|
|
1519
|
+
if (extra) {
|
|
1520
|
+
const python = built.find((b) => b.tc.language === "python");
|
|
1521
|
+
if (python && !new RegExp(`(^|[^\\w-])${extra}`, "i").test(f.pythonText)) {
|
|
1522
|
+
const into = python.tc.packageManager === "uv" ? `uv pip install ${extra}` : `pip install --no-cache-dir ${extra}`;
|
|
1523
|
+
const target = python.tc.installProject ? "installProject" : "install";
|
|
1524
|
+
python.tc[target] = python.tc[target] ? `${python.tc[target]} && ${into}` : into;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
const needs = [...new Set(NEEDS.filter((n) => n.when(f)).flatMap((n) => n.packages))];
|
|
1528
|
+
if (built.some((b) => !b.sure) && confidence === "certain") confidence = "inferred";
|
|
1529
|
+
const toolchains = orderToolchains(built.map((b) => b.tc), framework, command);
|
|
1530
|
+
return {
|
|
1531
|
+
toolchains,
|
|
1532
|
+
language: toolchains[0]?.language ?? "static",
|
|
1533
|
+
framework,
|
|
1534
|
+
command,
|
|
1535
|
+
release: detectRelease(f, procfile, config),
|
|
1536
|
+
outputDir,
|
|
1537
|
+
database: detectDatabase(f),
|
|
1538
|
+
needs,
|
|
1539
|
+
confidence
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function orderToolchains(toolchains, framework, command) {
|
|
1543
|
+
if (toolchains.length < 2) return toolchains;
|
|
1544
|
+
const cmd = command ?? "";
|
|
1545
|
+
const serves = (t) => {
|
|
1546
|
+
if (t.language === "python") return /\b(gunicorn|uvicorn|hypercorn|daphne|granian|waitress|python3?)\b/.test(cmd);
|
|
1547
|
+
if (t.language === "node") return /\b(node|npm|pnpm|yarn|bun|next|nuxt|react-router-serve)\b/.test(cmd);
|
|
1548
|
+
if (t.language === "ruby") return /\b(bundle|ruby|rackup|rails)\b/.test(cmd);
|
|
1549
|
+
if (t.language === "php") return /\bphp\b/.test(cmd);
|
|
1550
|
+
if (t.language === "go") return /\/app\/server\b/.test(cmd);
|
|
1551
|
+
if (t.language === "rust") return /\/app\/target\/release\//.test(cmd);
|
|
1552
|
+
if (t.language === "java") return /\bjava\b/.test(cmd);
|
|
1553
|
+
return false;
|
|
1554
|
+
};
|
|
1555
|
+
const first = toolchains.findIndex(serves);
|
|
1556
|
+
if (first > 0) return [toolchains[first], ...toolchains.filter((_, i) => i !== first)];
|
|
1557
|
+
if (first === -1 && framework) {
|
|
1558
|
+
const byFramework = toolchains.findIndex((t) => t.language === (["django", "fastapi", "flask"].includes(framework) ? "python" : ["rails"].includes(framework) ? "ruby" : ["laravel"].includes(framework) ? "php" : "node"));
|
|
1559
|
+
if (byFramework > 0) return [toolchains[byFramework], ...toolchains.filter((_, i) => i !== byFramework)];
|
|
1560
|
+
}
|
|
1561
|
+
return toolchains;
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// lib/dockerfile-context.ts
|
|
1565
|
+
function copySources(dockerfile) {
|
|
1566
|
+
const out = [];
|
|
1567
|
+
const lines = dockerfile.split(/\r?\n/);
|
|
1568
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1569
|
+
let raw = lines[i];
|
|
1570
|
+
let last = i;
|
|
1571
|
+
while (/\\\s*$/.test(raw) && last + 1 < lines.length) {
|
|
1572
|
+
last++;
|
|
1573
|
+
raw = raw.replace(/\\\s*$/, " ") + lines[last];
|
|
1574
|
+
}
|
|
1575
|
+
const started = i;
|
|
1576
|
+
i = last;
|
|
1577
|
+
const m = /^\s*(COPY|ADD)\s+(.*)$/i.exec(raw);
|
|
1578
|
+
if (!m) continue;
|
|
1579
|
+
let rest = m[2].trim();
|
|
1580
|
+
if (rest.startsWith("#")) continue;
|
|
1581
|
+
let fromStage = false;
|
|
1582
|
+
while (rest.startsWith("--")) {
|
|
1583
|
+
const flag = rest.slice(0, rest.search(/\s/) < 0 ? rest.length : rest.search(/\s/));
|
|
1584
|
+
if (/^--from=/i.test(flag)) fromStage = true;
|
|
1585
|
+
rest = rest.slice(flag.length).trim();
|
|
1586
|
+
}
|
|
1587
|
+
if (fromStage || !rest) continue;
|
|
1588
|
+
let args;
|
|
1589
|
+
if (rest.startsWith("[")) {
|
|
1590
|
+
try {
|
|
1591
|
+
const parsed = JSON.parse(rest);
|
|
1592
|
+
args = Array.isArray(parsed) ? parsed.map(String) : [];
|
|
1593
|
+
} catch {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
args = rest.split(/\s+/).filter(Boolean).map((a) => a.replace(/^["']|["']$/g, ""));
|
|
1598
|
+
}
|
|
1599
|
+
if (args.length < 2) continue;
|
|
1600
|
+
for (const src of args.slice(0, -1)) {
|
|
1601
|
+
if (src.includes("$")) continue;
|
|
1602
|
+
out.push({ path: src, line: started + 1 });
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
return out;
|
|
1606
|
+
}
|
|
1607
|
+
function buildOwner(candidates) {
|
|
1608
|
+
const dirs = candidates.map((c) => c.dir.replace(/^\.\//, "").replace(/\/+$/, "")).filter((d) => d && d !== ".");
|
|
1609
|
+
for (const c of candidates) {
|
|
1610
|
+
if (!c.dockerfile) continue;
|
|
1611
|
+
const mine = c.dir.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1612
|
+
for (const { path } of copySources(c.dockerfile)) {
|
|
1613
|
+
const head = path.replace(/^\.\//, "").split("/")[0];
|
|
1614
|
+
if (head && head !== mine && dirs.includes(head)) return c.dir;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return null;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// lib/infer-services.ts
|
|
1621
|
+
var BROWSER_FACING = /next|nuxt|remix|svelte|astro|vite|create react app|static/i;
|
|
1622
|
+
var NOT_AN_APP = /* @__PURE__ */ new Set([
|
|
1623
|
+
"e2e",
|
|
1624
|
+
"test",
|
|
1625
|
+
"tests",
|
|
1626
|
+
"spec",
|
|
1627
|
+
"specs",
|
|
1628
|
+
"docs",
|
|
1629
|
+
"doc",
|
|
1630
|
+
"examples",
|
|
1631
|
+
"example",
|
|
1632
|
+
"fixtures",
|
|
1633
|
+
"scripts",
|
|
1634
|
+
"tools",
|
|
1635
|
+
"infra",
|
|
1636
|
+
"terraform",
|
|
1637
|
+
"deploy",
|
|
1638
|
+
"deployment",
|
|
1639
|
+
"ci",
|
|
1640
|
+
"benchmark",
|
|
1641
|
+
"benchmarks",
|
|
1642
|
+
"bench",
|
|
1643
|
+
"migrations",
|
|
1644
|
+
"seeds"
|
|
1645
|
+
]);
|
|
1646
|
+
var NODE_FRAMEWORK = /^(next|nuxt|astro|vite|@remix-run\/|@sveltejs\/kit|@nestjs\/core|express|fastify|koa|hono)/;
|
|
1647
|
+
function dirOf(manifestPath) {
|
|
1648
|
+
const i = manifestPath.lastIndexOf("/");
|
|
1649
|
+
return i === -1 ? "." : manifestPath.slice(0, i);
|
|
1650
|
+
}
|
|
1651
|
+
function isWorkspaceRoot(repoDir) {
|
|
1652
|
+
const p = (0, import_node_path6.join)(repoDir, "package.json");
|
|
1653
|
+
if (!(0, import_node_fs6.existsSync)(p)) return false;
|
|
1654
|
+
try {
|
|
1655
|
+
return Boolean(JSON.parse((0, import_node_fs6.readFileSync)(p, "utf8")).workspaces);
|
|
1656
|
+
} catch {
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function isDeployablePart(absoluteDir, relDir) {
|
|
1661
|
+
if (relDir.split("/").some((seg) => NOT_AN_APP.has(seg.toLowerCase()))) return false;
|
|
1662
|
+
const pkgPath = (0, import_node_path6.join)(absoluteDir, "package.json");
|
|
1663
|
+
if ((0, import_node_fs6.existsSync)(pkgPath)) {
|
|
1664
|
+
try {
|
|
1665
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(pkgPath, "utf8"));
|
|
1666
|
+
const scripts = pkg.scripts ?? {};
|
|
1667
|
+
if (scripts.build || scripts.start) return true;
|
|
1668
|
+
const deps = Object.keys({ ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} });
|
|
1669
|
+
return deps.some((d) => NODE_FRAMEWORK.test(d));
|
|
1670
|
+
} catch {
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return PYTHON_RUNNABLE.some((entry) => (0, import_node_fs6.existsSync)((0, import_node_path6.join)(absoluteDir, entry)));
|
|
1675
|
+
}
|
|
1676
|
+
function startFor(stack, absoluteDir) {
|
|
1677
|
+
const bound = bindToPort(stack.startCommand);
|
|
1678
|
+
if (!/^python/i.test(stack.language)) return bound;
|
|
1679
|
+
const mod = pythonModule(absoluteDir);
|
|
1680
|
+
if (!mod) return bound;
|
|
1681
|
+
return bound.replace(/[\w.]+(?=:app\b)/, mod);
|
|
1682
|
+
}
|
|
1683
|
+
function languageOf(stack) {
|
|
1684
|
+
return serviceLanguage(stack.language, stack.serve.mode === "static");
|
|
1685
|
+
}
|
|
1686
|
+
function serviceFor(relDir, stack, absoluteDir) {
|
|
1687
|
+
const language = languageOf(stack);
|
|
1688
|
+
const name = relDir === "." ? "app" : import_node_path6.posix.basename(relDir);
|
|
1689
|
+
const base = {
|
|
1690
|
+
name,
|
|
1691
|
+
dir: relDir,
|
|
1692
|
+
language,
|
|
1693
|
+
install: language === "python" ? pythonInstall(absoluteDir, stack.installCommand) : stack.installCommand ?? void 0,
|
|
1694
|
+
build: stack.buildCommand ?? void 0,
|
|
1695
|
+
needsDB: stack.database?.engine ? true : void 0
|
|
1696
|
+
};
|
|
1697
|
+
if (language === "static") {
|
|
1698
|
+
return { ...base, outputDir: stack.serve.mode === "static" ? stack.serve.outputDir : "dist" };
|
|
1699
|
+
}
|
|
1700
|
+
return { ...base, start: startFor(stack, absoluteDir), release: stack.release };
|
|
1701
|
+
}
|
|
1702
|
+
function deployableParts(repoDir, facts) {
|
|
1703
|
+
const dirs = [];
|
|
1704
|
+
for (const d of facts.declarations) {
|
|
1705
|
+
const dir = dirOf(d.from);
|
|
1706
|
+
if (dirs.includes(dir)) continue;
|
|
1707
|
+
if (!isDeployablePart(dir === "." ? repoDir : (0, import_node_path6.join)(repoDir, dir), dir)) continue;
|
|
1708
|
+
dirs.push(dir);
|
|
1709
|
+
}
|
|
1710
|
+
const nested = dirs.filter((d) => d !== ".");
|
|
1711
|
+
return nested.length >= 2 || dirs.includes(".") && isWorkspaceRoot(repoDir) ? nested : dirs;
|
|
1712
|
+
}
|
|
1713
|
+
async function inferAppConfig(repoDir, detect2) {
|
|
1714
|
+
const facts = readRepoFacts(repoDir);
|
|
1715
|
+
if (!facts.declarations.length) return null;
|
|
1716
|
+
if (facts.dockerfiles.includes("Dockerfile")) return null;
|
|
1717
|
+
const parts2 = deployableParts(repoDir, facts);
|
|
1718
|
+
if (parts2.length === 1 && parts2[0] !== "." && isWorkspaceRoot(repoDir)) {
|
|
1719
|
+
try {
|
|
1720
|
+
const abs = (0, import_node_path6.join)(repoDir, parts2[0]);
|
|
1721
|
+
return { version: 1, services: [{ ...serviceFor(parts2[0], await detect2(abs), abs), path: "/" }] };
|
|
1722
|
+
} catch {
|
|
1723
|
+
return null;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (parts2.length < 2) return null;
|
|
1727
|
+
const owner = buildOwner(parts2.map((rel) => {
|
|
1728
|
+
const at = rel === "." ? repoDir : (0, import_node_path6.join)(repoDir, rel);
|
|
1729
|
+
const file = (0, import_node_path6.join)(at, "Dockerfile");
|
|
1730
|
+
return { dir: rel, dockerfile: (0, import_node_fs6.existsSync)(file) ? (0, import_node_fs6.readFileSync)(file, "utf8") : void 0 };
|
|
1731
|
+
}));
|
|
1732
|
+
if (owner) {
|
|
1733
|
+
try {
|
|
1734
|
+
const abs = owner === "." ? repoDir : (0, import_node_path6.join)(repoDir, owner);
|
|
1735
|
+
return { version: 1, services: [{ ...serviceFor(owner, await detect2(abs), abs), path: "/" }] };
|
|
1736
|
+
} catch {
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
let detected;
|
|
1741
|
+
try {
|
|
1742
|
+
detected = await Promise.all(parts2.map(async (rel) => {
|
|
1743
|
+
const abs = rel === "." ? repoDir : (0, import_node_path6.join)(repoDir, rel);
|
|
1744
|
+
return { rel, abs, stack: await detect2(abs) };
|
|
1745
|
+
}));
|
|
1746
|
+
} catch {
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
const primaryIdx = Math.max(0, detected.findIndex((p) => BROWSER_FACING.test(p.stack.framework)));
|
|
1750
|
+
const ordered = [detected[primaryIdx], ...detected.filter((_, i) => i !== primaryIdx)];
|
|
1751
|
+
const services = ordered.map((p, i) => {
|
|
1752
|
+
const svc = serviceFor(p.rel, p.stack, p.abs);
|
|
1753
|
+
if (i === 0) return { ...svc, path: "/" };
|
|
1754
|
+
return { ...svc, path: ordered.length === 2 ? "/api" : `/${svc.name}` };
|
|
1755
|
+
});
|
|
1756
|
+
return { version: 1, services };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// lib/resolve.ts
|
|
1760
|
+
var ResolveError = class extends Error {
|
|
1761
|
+
};
|
|
1762
|
+
function laneFor(i) {
|
|
1763
|
+
if (i.language === "static") return "static";
|
|
1764
|
+
return "container";
|
|
1765
|
+
}
|
|
1766
|
+
function deriveLane(s) {
|
|
1767
|
+
return laneFor({ language: s.language, runtime: s.runtime, dockerfile: s.dockerfile });
|
|
1768
|
+
}
|
|
1769
|
+
var LANE_CONSUMES = {
|
|
1770
|
+
static: ["install", "build", "outputDir", "spaFallback", "buildEnv", "env"],
|
|
1771
|
+
// No `start`: when the author committed the Dockerfile, its own CMD is the
|
|
1772
|
+
// start command and a second one in the config would be read by nobody. That
|
|
1773
|
+
// is still true, and it is the reason this list is not simply the union of the
|
|
1774
|
+
// two lanes it replaces.
|
|
1775
|
+
container: ["dockerfile", "context", "release", "processes", "env", "buildEnv", "secrets", "uses", "health", "scale", "framework"]
|
|
1776
|
+
};
|
|
1777
|
+
var GENERATED_DOCKERFILE_CONSUMES = [
|
|
1778
|
+
"install",
|
|
1779
|
+
"build",
|
|
1780
|
+
"start",
|
|
1781
|
+
"runtime"
|
|
1782
|
+
];
|
|
1783
|
+
var UNIVERSAL = ["name", "dir", "path", "lane", "envNeeded", "declared"];
|
|
1784
|
+
function assertConsumed(s) {
|
|
1785
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
1786
|
+
...LANE_CONSUMES[s.lane],
|
|
1787
|
+
...UNIVERSAL,
|
|
1788
|
+
// An app whose Dockerfile the platform writes reads four more fields, because
|
|
1789
|
+
// they are what it is written FROM. See GENERATED_DOCKERFILE_CONSUMES.
|
|
1790
|
+
...s.lane === "container" && !s.dockerfile ? GENERATED_DOCKERFILE_CONSUMES : []
|
|
1791
|
+
]);
|
|
1792
|
+
const ignored = s.declared.filter((f) => !allowed.has(f));
|
|
1793
|
+
if (!ignored.length) return;
|
|
1794
|
+
throw new ResolveError(
|
|
1795
|
+
`the ${s.lane} lane does not implement: ${ignored.join(", ")}
|
|
1796
|
+
Service "${s.name}" declares ${ignored.length === 1 ? "it" : "them"} and the deploy would ignore ${ignored.length === 1 ? "it" : "them"} silently.
|
|
1797
|
+
` + (s.lane === "static" ? ` Move ${ignored.length === 1 ? "it" : "them"} to a service with a \`start\` command, or remove ${ignored.length === 1 ? "it" : "them"}.` : ` Remove ${ignored.length === 1 ? "it" : "them"}, or change the service so this lane applies.`)
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
function healthOf(s) {
|
|
1801
|
+
return s.health ?? { path: "/", expect: 200 };
|
|
1802
|
+
}
|
|
1803
|
+
function declaredFields(s) {
|
|
1804
|
+
const out = [];
|
|
1805
|
+
const set = (v) => v !== void 0 && v !== null && !(typeof v === "string" && !v.trim());
|
|
1806
|
+
if (set(s.install)) out.push("install");
|
|
1807
|
+
if (set(s.build)) out.push("build");
|
|
1808
|
+
if (set(s.release) || set(s.preDeploy)) out.push("release");
|
|
1809
|
+
if (set(s.start)) out.push("start");
|
|
1810
|
+
if (set(s.outputDir)) out.push("outputDir");
|
|
1811
|
+
if (s.spaFallback) out.push("spaFallback");
|
|
1812
|
+
if (set(s.dockerfile)) out.push("dockerfile");
|
|
1813
|
+
if (set(s.context)) out.push("context");
|
|
1814
|
+
if (s.needsDB || (s.uses ?? []).length) out.push("uses");
|
|
1815
|
+
if (s.env && Object.keys(s.env).length) out.push("env");
|
|
1816
|
+
if (s.buildEnv && Object.keys(s.buildEnv).length) out.push("buildEnv");
|
|
1817
|
+
if ((s.secrets ?? []).length) out.push("secrets");
|
|
1818
|
+
if (Object.keys(s.processes ?? {}).length) out.push("processes");
|
|
1819
|
+
if (set(s.health)) out.push("health");
|
|
1820
|
+
if (set(s.scale)) out.push("scale");
|
|
1821
|
+
if (set(s.runtime)) out.push("runtime");
|
|
1822
|
+
if (set(s.framework)) out.push("framework");
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1825
|
+
function resolveService(s, index, runtimePinned = false) {
|
|
1826
|
+
const dir = s.dir ?? ".";
|
|
1827
|
+
const lane = laneFor({
|
|
1828
|
+
language: s.language,
|
|
1829
|
+
runtime: s.runtime,
|
|
1830
|
+
dockerfile: s.dockerfile,
|
|
1831
|
+
runtimePinned
|
|
1832
|
+
});
|
|
1833
|
+
const name = s.name ?? (index === 0 ? "app" : `svc${index}`);
|
|
1834
|
+
return {
|
|
1835
|
+
name,
|
|
1836
|
+
dir,
|
|
1837
|
+
path: servicePath(s),
|
|
1838
|
+
lane,
|
|
1839
|
+
runtime: s.runtime,
|
|
1840
|
+
framework: s.framework,
|
|
1841
|
+
// Wrapped in a subshell here, once, rather than by each lane — see inDir for
|
|
1842
|
+
// why the parentheses are load-bearing.
|
|
1843
|
+
install: inDir(s.install, dir),
|
|
1844
|
+
build: inDir(s.build, dir),
|
|
1845
|
+
release: inDir(releaseCommand(s), dir),
|
|
1846
|
+
start: inDir(s.start, dir),
|
|
1847
|
+
// Relative to the repo root, because that is what the uploader and the
|
|
1848
|
+
// static lane both address.
|
|
1849
|
+
outputDir: s.language === "static" ? dir === "." ? s.outputDir ?? "." : `${dir}/${s.outputDir ?? "."}`.replace(/\/\.$/, "") : void 0,
|
|
1850
|
+
spaFallback: Boolean(s.spaFallback),
|
|
1851
|
+
dockerfile: s.dockerfile,
|
|
1852
|
+
context: s.dockerfile ? s.context ?? dir : void 0,
|
|
1853
|
+
uses: s.uses ?? (s.needsDB ? ["database"] : []),
|
|
1854
|
+
processes: Object.keys(s.processes ?? {}),
|
|
1855
|
+
env: s.env ?? {},
|
|
1856
|
+
buildEnv: s.buildEnv ?? {},
|
|
1857
|
+
secrets: s.secrets ?? [],
|
|
1858
|
+
envNeeded: s.envNeeded ?? [],
|
|
1859
|
+
health: healthOf(s),
|
|
1860
|
+
scale: withScale(s.scale),
|
|
1861
|
+
declared: declaredFields(s)
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
async function resolve2(dir, detect2) {
|
|
1865
|
+
let config = null;
|
|
1866
|
+
let source = "config";
|
|
1867
|
+
config = readAppConfig(dir);
|
|
1868
|
+
if (!config) {
|
|
1869
|
+
if (!detect2) throw new ResolveError(`${dir} has no ${CONFIG_FILENAME} and no detector was supplied \u2014 run \`supersonic init\``);
|
|
1870
|
+
config = await inferAppConfig(dir, detect2);
|
|
1871
|
+
source = "inferred";
|
|
1872
|
+
}
|
|
1873
|
+
if (!config) {
|
|
1874
|
+
throw new ResolveError(
|
|
1875
|
+
`could not work out how to deploy ${dir}.
|
|
1876
|
+
Run \`supersonic init\` to write a ${CONFIG_FILENAME} draft, then check it.`
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
return resolveFrom(config, source, repoPinsRuntime(dir));
|
|
1880
|
+
}
|
|
1881
|
+
function repoPinsRuntime(dir) {
|
|
1882
|
+
const read = (f) => {
|
|
1883
|
+
try {
|
|
1884
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(dir, f)) ? (0, import_node_fs7.readFileSync)((0, import_node_path7.join)(dir, f), "utf8") : null;
|
|
1885
|
+
} catch {
|
|
1886
|
+
return null;
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const pin = repoRuntime({
|
|
1890
|
+
pythonVersion: read(".python-version"),
|
|
1891
|
+
runtimeTxt: read("runtime.txt"),
|
|
1892
|
+
pyproject: read("pyproject.toml"),
|
|
1893
|
+
nvmrc: read(".nvmrc"),
|
|
1894
|
+
packageJson: (() => {
|
|
1895
|
+
try {
|
|
1896
|
+
return JSON.parse(read("package.json") ?? "null");
|
|
1897
|
+
} catch {
|
|
1898
|
+
return null;
|
|
1899
|
+
}
|
|
1900
|
+
})()
|
|
1901
|
+
});
|
|
1902
|
+
return Boolean(pin && !runnerServes(pin));
|
|
1903
|
+
}
|
|
1904
|
+
function resolveFrom(config, source, runtimePinned = false) {
|
|
1905
|
+
const primary = primaryService(config);
|
|
1906
|
+
const ordered = [primary, ...config.services.filter((s) => s !== primary)];
|
|
1907
|
+
return {
|
|
1908
|
+
source,
|
|
1909
|
+
// Normalised here rather than only in the parser, so `uses` and `needsDB`
|
|
1910
|
+
// mean the same thing for an INFERRED app as for a written one. Inference
|
|
1911
|
+
// builds an AppConfig directly and never passes through parseAppConfig, and
|
|
1912
|
+
// a rule that holds on one of two paths is the shape of bug this whole
|
|
1913
|
+
// module exists to end.
|
|
1914
|
+
resources: appResources(config.resources, config.services) ?? {},
|
|
1915
|
+
services: ordered.map((svc, i) => resolveService(svc, i, runtimePinned))
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
function validate(app, dir) {
|
|
1919
|
+
const problems = [];
|
|
1920
|
+
if (!app.services.length) problems.push(`${CONFIG_FILENAME}: no services to deploy`);
|
|
1921
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1922
|
+
for (const s of app.services) {
|
|
1923
|
+
const where = `service "${s.name}"`;
|
|
1924
|
+
if (paths.has(s.path)) problems.push(`${where}: two services both serve ${s.path}`);
|
|
1925
|
+
paths.add(s.path);
|
|
1926
|
+
const abs = (0, import_node_path7.join)(dir, s.dir);
|
|
1927
|
+
if (!(0, import_node_fs7.existsSync)(abs)) {
|
|
1928
|
+
problems.push(`${where}: directory "${s.dir}" does not exist`);
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
if (s.lane === "static") {
|
|
1932
|
+
if (s.build && !s.declared.includes("outputDir")) {
|
|
1933
|
+
problems.push(`${where}: has a build command but no outputDir \u2014 what should be published?`);
|
|
1934
|
+
}
|
|
1935
|
+
if (!s.build && s.outputDir && !(0, import_node_fs7.existsSync)((0, import_node_path7.join)(dir, s.outputDir))) {
|
|
1936
|
+
problems.push(`${where}: outputDir "${s.outputDir}" does not exist and no build command would create it`);
|
|
1937
|
+
}
|
|
1938
|
+
} else if (!s.dockerfile && !s.start && !s.processes.length) {
|
|
1939
|
+
problems.push(
|
|
1940
|
+
`${where}: this service has no \`start\` command and no Dockerfile of its own
|
|
1941
|
+
If this app is a worker, a bot or a scheduled job, declare "processes" instead.`
|
|
1942
|
+
);
|
|
1943
|
+
}
|
|
1944
|
+
if (s.dockerfile) {
|
|
1945
|
+
if (!(0, import_node_fs7.existsSync)((0, import_node_path7.join)(dir, s.dockerfile))) {
|
|
1946
|
+
problems.push(`${where}: dockerfile "${s.dockerfile}" does not exist`);
|
|
1947
|
+
}
|
|
1948
|
+
if (s.context && !(0, import_node_fs7.existsSync)((0, import_node_path7.join)(dir, s.context))) {
|
|
1949
|
+
problems.push(`${where}: build context "${s.context}" does not exist`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
assertConsumed(s);
|
|
1954
|
+
} catch (e) {
|
|
1955
|
+
problems.push(e instanceof Error ? e.message : String(e));
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
if (problems.length) {
|
|
1959
|
+
throw new ResolveError(problems.map((p) => `\u2715 ${p}`).join("\n"));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function missingSecrets(app, available) {
|
|
1963
|
+
const have = new Set(available);
|
|
1964
|
+
const want = new Set(app.services.flatMap((s) => s.secrets));
|
|
1965
|
+
const db = app.resources.database;
|
|
1966
|
+
if (db?.provider === "external") want.add(db.urlFrom);
|
|
1967
|
+
return [...want].filter((n) => !have.has(n)).sort();
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// lib/processes.ts
|
|
1971
|
+
var PRIMITIVE = {
|
|
1972
|
+
web: "service",
|
|
1973
|
+
worker: "worker-pool",
|
|
1974
|
+
cron: "job",
|
|
1975
|
+
release: "job"
|
|
1976
|
+
};
|
|
1977
|
+
var WEB = "web";
|
|
1978
|
+
var RELEASE = "release";
|
|
1979
|
+
var ProcessError = class extends Error {
|
|
1980
|
+
};
|
|
1981
|
+
var ALLOWED = {
|
|
1982
|
+
web: ["command", "kind", "memory", "cpu", "visibility", "health", "maxInstances", "concurrency", "cpuBoost", "timeout", "shutdownGrace"],
|
|
1983
|
+
worker: ["command", "kind", "memory", "cpu", "instances", "shutdownGrace"],
|
|
1984
|
+
cron: ["command", "kind", "memory", "cpu", "schedule", "timezone", "taskTimeout", "retries"],
|
|
1985
|
+
release: ["command", "kind", "memory", "cpu", "taskTimeout", "retries"]
|
|
1986
|
+
};
|
|
1987
|
+
function unemittable(p) {
|
|
1988
|
+
const out = [];
|
|
1989
|
+
if ((p.kind === "web" || p.kind === "worker") && p.shutdownGrace !== void 0) {
|
|
1990
|
+
out.push(
|
|
1991
|
+
`${p.name}.shutdownGrace is not emitted yet \u2014 terminationGracePeriodSeconds has no gcloud flag and needs the spec-replace path (DEPLOY-PLAN-V2 step 4)`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
return out;
|
|
1995
|
+
}
|
|
1996
|
+
var DEFAULT_TASK_TIMEOUT = 1800;
|
|
1997
|
+
var DEFAULT_RETRIES = 0;
|
|
1998
|
+
var DEFAULT_INSTANCES = 1;
|
|
1999
|
+
function deriveKind(name, c) {
|
|
2000
|
+
if (c.kind) return c.kind;
|
|
2001
|
+
if (c.schedule) return "cron";
|
|
2002
|
+
if (name === WEB) return "web";
|
|
2003
|
+
if (name === RELEASE) return "release";
|
|
2004
|
+
return "worker";
|
|
2005
|
+
}
|
|
2006
|
+
function declaredFields2(c) {
|
|
2007
|
+
const set = (v) => v !== void 0 && v !== null && !(typeof v === "string" && !v.trim());
|
|
2008
|
+
return Object.keys(c).filter((k) => set(c[k]));
|
|
2009
|
+
}
|
|
2010
|
+
function assertEmittable(name, kind, c) {
|
|
2011
|
+
const allowed = new Set(ALLOWED[kind]);
|
|
2012
|
+
const stray = declaredFields2(c).filter((f) => !allowed.has(f));
|
|
2013
|
+
if (!stray.length) return;
|
|
2014
|
+
throw new ProcessError(
|
|
2015
|
+
`process "${name}" is a ${kind} and cannot declare: ${stray.join(", ")}
|
|
2016
|
+
A ${kind} runs as a Cloud Run ${PRIMITIVE[kind]}, which has no such setting \u2014 the deploy would accept ${stray.length === 1 ? "it" : "them"} and ignore ${stray.length === 1 ? "it" : "them"}.`
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
function positive(v, field, name) {
|
|
2020
|
+
if (v === void 0) return void 0;
|
|
2021
|
+
if (!Number.isInteger(v) || v < 1) {
|
|
2022
|
+
throw new ProcessError(`process "${name}": ${field} must be a whole number of 1 or more (got ${JSON.stringify(v)})`);
|
|
2023
|
+
}
|
|
2024
|
+
return v;
|
|
2025
|
+
}
|
|
2026
|
+
function resolveProcess(name, c) {
|
|
2027
|
+
const command = (c.command ?? "").trim();
|
|
2028
|
+
if (!command) throw new ProcessError(`process "${name}" has no command`);
|
|
2029
|
+
const kind = deriveKind(name, c);
|
|
2030
|
+
assertEmittable(name, kind, c);
|
|
2031
|
+
const declared = declaredFields2(c);
|
|
2032
|
+
const base = { name, command, declared };
|
|
2033
|
+
const memory = c.memory ?? DEFAULT_SCALE.memory;
|
|
2034
|
+
const cpu = c.cpu ?? DEFAULT_SCALE.cpu;
|
|
2035
|
+
if (kind === "web") {
|
|
2036
|
+
return {
|
|
2037
|
+
...base,
|
|
2038
|
+
kind,
|
|
2039
|
+
visibility: c.visibility ?? "public",
|
|
2040
|
+
health: c.health ?? { path: "/", expect: 200 },
|
|
2041
|
+
// Through withScale so the undefined-dropping rule holds here too: spreading
|
|
2042
|
+
// a partial over DEFAULT_SCALE overwrites defaults with undefined, which is
|
|
2043
|
+
// how `--concurrency undefined` once reached gcloud.
|
|
2044
|
+
scale: withScale({
|
|
2045
|
+
memory: c.memory,
|
|
2046
|
+
cpu: c.cpu,
|
|
2047
|
+
maxInstances: c.maxInstances,
|
|
2048
|
+
timeout: c.timeout,
|
|
2049
|
+
concurrency: c.concurrency,
|
|
2050
|
+
cpuBoost: c.cpuBoost
|
|
2051
|
+
}),
|
|
2052
|
+
shutdownGrace: positive(c.shutdownGrace, "shutdownGrace", name)
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
if (kind === "worker") {
|
|
2056
|
+
return {
|
|
2057
|
+
...base,
|
|
2058
|
+
kind,
|
|
2059
|
+
instances: positive(c.instances, "instances", name) ?? DEFAULT_INSTANCES,
|
|
2060
|
+
memory,
|
|
2061
|
+
cpu,
|
|
2062
|
+
shutdownGrace: positive(c.shutdownGrace, "shutdownGrace", name)
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
if (kind === "cron" && !c.schedule?.trim()) {
|
|
2066
|
+
throw new ProcessError(`process "${name}" is a cron and needs a "schedule" \u2014 a cron expression like "0 3 * * *"`);
|
|
2067
|
+
}
|
|
2068
|
+
return {
|
|
2069
|
+
...base,
|
|
2070
|
+
kind,
|
|
2071
|
+
schedule: c.schedule?.trim(),
|
|
2072
|
+
timezone: c.timezone?.trim(),
|
|
2073
|
+
memory,
|
|
2074
|
+
cpu,
|
|
2075
|
+
taskTimeout: positive(c.taskTimeout, "taskTimeout", name) ?? DEFAULT_TASK_TIMEOUT,
|
|
2076
|
+
retries: c.retries ?? DEFAULT_RETRIES
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function mergeProcfile(declared, procfile) {
|
|
2080
|
+
const out = {};
|
|
2081
|
+
for (const entry of procfile ?? []) out[entry.name] = { command: entry.command };
|
|
2082
|
+
for (const [name, cfg] of Object.entries(declared ?? {})) {
|
|
2083
|
+
const fromFile = out[name];
|
|
2084
|
+
if (fromFile && cfg.command && cfg.command.trim() !== fromFile.command) {
|
|
2085
|
+
throw new ProcessError(
|
|
2086
|
+
`process "${name}" has one command in Procfile and a different one in supersonic.json:
|
|
2087
|
+
Procfile: ${fromFile.command}
|
|
2088
|
+
supersonic.json: ${cfg.command}
|
|
2089
|
+
Keep one. Leave "command" out of supersonic.json to use the Procfile's.`
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
out[name] = { ...fromFile, ...cfg };
|
|
2093
|
+
}
|
|
2094
|
+
return out;
|
|
2095
|
+
}
|
|
2096
|
+
function resolveProcesses(configs) {
|
|
2097
|
+
const resolved = Object.entries(configs).map(([name, c]) => resolveProcess(name, c));
|
|
2098
|
+
const webs = resolved.filter((p) => p.kind === "web");
|
|
2099
|
+
if (webs.length > 1) {
|
|
2100
|
+
throw new ProcessError(
|
|
2101
|
+
`a service has ${webs.length} web processes (${webs.map((p) => p.name).join(", ")}) \u2014 only one can answer on the service's address. Split them into two services, each with its own path.`
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
const releases = resolved.filter((p) => p.kind === "release");
|
|
2105
|
+
if (releases.length > 1) {
|
|
2106
|
+
throw new ProcessError(
|
|
2107
|
+
`a service has ${releases.length} release processes (${releases.map((p) => p.name).join(", ")}) \u2014 the release phase runs exactly once, before traffic.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
return [...webs, ...resolved.filter((p) => p.kind !== "web")];
|
|
2111
|
+
}
|
|
2112
|
+
function isServiceless(declared) {
|
|
2113
|
+
return declared.length > 0 && !declared.some((p) => p.kind === "web");
|
|
2114
|
+
}
|
|
2115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2116
|
+
0 && (module.exports = {
|
|
2117
|
+
CONFIG_FILENAME,
|
|
2118
|
+
ConfigError,
|
|
2119
|
+
DEFAULT_SCALE,
|
|
2120
|
+
PRIMITIVE,
|
|
2121
|
+
ProcessError,
|
|
2122
|
+
ProcfileError,
|
|
2123
|
+
RUNTIME_UNSUPPORTED,
|
|
2124
|
+
RUNTIME_VERSIONS,
|
|
2125
|
+
ResolveError,
|
|
2126
|
+
appResources,
|
|
2127
|
+
assertConsumed,
|
|
2128
|
+
bindToPort,
|
|
2129
|
+
declaredLanguages,
|
|
2130
|
+
deployableParts,
|
|
2131
|
+
deriveLane,
|
|
2132
|
+
detect,
|
|
2133
|
+
inferAppConfig,
|
|
2134
|
+
isDeployablePart,
|
|
2135
|
+
isServiceless,
|
|
2136
|
+
mergeProcfile,
|
|
2137
|
+
missingSecrets,
|
|
2138
|
+
normalizeLanguage,
|
|
2139
|
+
parseAppConfig,
|
|
2140
|
+
parseProcfile,
|
|
2141
|
+
platformOwned,
|
|
2142
|
+
primaryService,
|
|
2143
|
+
pythonInstall,
|
|
2144
|
+
pythonModule,
|
|
2145
|
+
readAppConfig,
|
|
2146
|
+
readProcfile,
|
|
2147
|
+
readRepoFacts,
|
|
2148
|
+
resolve,
|
|
2149
|
+
resolveProcesses,
|
|
2150
|
+
runtimeMismatch,
|
|
2151
|
+
serviceFor,
|
|
2152
|
+
serviceLanguage,
|
|
2153
|
+
servicePath,
|
|
2154
|
+
unemittable,
|
|
2155
|
+
validate
|
|
2156
|
+
});
|