@faable/faable 1.17.0 → 1.19.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/dist/api/FaableApi.js +15 -0
- package/dist/commands/deploy/index.js +93 -54
- package/dist/commands/deploy/remote/follow.js +61 -0
- package/dist/commands/deploy/remote/index.js +48 -0
- package/dist/commands/deploy/remote/manifest.js +77 -0
- package/dist/commands/deploy/remote/upload.js +61 -0
- package/package.json +2 -1
- package/dist/commands/deploy/buildpacks/Buildpack.js +0 -11
- package/dist/commands/deploy/buildpacks/DetectError.js +0 -78
- package/dist/commands/deploy/buildpacks/docker/index.js +0 -42
- package/dist/commands/deploy/buildpacks/foreign_platforms.js +0 -21
- package/dist/commands/deploy/buildpacks/node/analyze_package.js +0 -35
- package/dist/commands/deploy/buildpacks/node/build_project.js +0 -21
- package/dist/commands/deploy/buildpacks/node/ensure_dependencies.js +0 -78
- package/dist/commands/deploy/buildpacks/node/frameworks.js +0 -108
- package/dist/commands/deploy/buildpacks/node/index.js +0 -83
- package/dist/commands/deploy/buildpacks/node/inject_serve.js +0 -20
- package/dist/commands/deploy/buildpacks/node/node_version.js +0 -41
- package/dist/commands/deploy/buildpacks/node/wrap_next_config.js +0 -138
- package/dist/commands/deploy/buildpacks/python/index.js +0 -127
- package/dist/commands/deploy/buildpacks/python/parse_procfile.js +0 -23
- package/dist/commands/deploy/buildpacks/python/providers/cerebrium.js +0 -43
- package/dist/commands/deploy/buildpacks/python/providers/parse_cerebrium_toml.js +0 -56
- package/dist/commands/deploy/buildpacks/python/providers/pipfile.js +0 -13
- package/dist/commands/deploy/buildpacks/python/providers/pyproject.js +0 -13
- package/dist/commands/deploy/buildpacks/python/providers/requirements.js +0 -14
- package/dist/commands/deploy/buildpacks/python/python_version.js +0 -41
- package/dist/commands/deploy/buildpacks/python/resolve_start.js +0 -149
- package/dist/commands/deploy/buildpacks/registry.js +0 -61
- package/dist/commands/deploy/buildpacks/shared/docker_image.js +0 -49
- package/dist/commands/deploy/buildpacks/shared/has_any_of_files.js +0 -14
- package/dist/commands/deploy/buildpacks/shared/read_text_file.js +0 -31
- package/dist/commands/deploy/buildpacks/shared/templates/Dockerfile +0 -38
- package/dist/commands/deploy/buildpacks/shared/templates/entrypoint.sh +0 -4
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import { log } from '../../../../log.js';
|
|
3
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
4
|
-
import { detect_framework } from './frameworks.js';
|
|
5
|
-
|
|
6
|
-
const analyze_package = async (params) => {
|
|
7
|
-
const workdir = params.workdir;
|
|
8
|
-
const package_file = path__default.join(path__default.resolve(workdir), "package.json");
|
|
9
|
-
log.info(`Loading config from package.json`);
|
|
10
|
-
const pkg = read_json_file(package_file);
|
|
11
|
-
// Check if build is required to run
|
|
12
|
-
const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
13
|
-
? process.env.FAABLE_NPM_BUILD_SCRIPT
|
|
14
|
-
: pkg?.scripts?.["build"]
|
|
15
|
-
? "build"
|
|
16
|
-
: null;
|
|
17
|
-
if (!build_script) {
|
|
18
|
-
log.info(`No build script on package.json`);
|
|
19
|
-
}
|
|
20
|
-
const has_start = Boolean(pkg?.scripts?.["start"]);
|
|
21
|
-
const { type, start_command, inject_serve } = detect_framework({
|
|
22
|
-
pkg,
|
|
23
|
-
workdir,
|
|
24
|
-
has_start,
|
|
25
|
-
});
|
|
26
|
-
log.info(`⚡️ Detected deployment type=${type}`);
|
|
27
|
-
return {
|
|
28
|
-
build_script,
|
|
29
|
-
type,
|
|
30
|
-
start_command,
|
|
31
|
-
inject_serve,
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export { analyze_package };
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { log } from '../../../../log.js';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
|
|
4
|
-
const build_project = async (args) => {
|
|
5
|
-
const { command, cwd } = args;
|
|
6
|
-
if (command) {
|
|
7
|
-
log.info(`⚙️ Building project [${command}]...`);
|
|
8
|
-
const timeout = 1000 * 60 * 30; // 30 minute timeout
|
|
9
|
-
await cmd(command, {
|
|
10
|
-
timeout,
|
|
11
|
-
cwd,
|
|
12
|
-
enableOutput: true,
|
|
13
|
-
...(args?.env ? { env: args?.env } : {}),
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
else {
|
|
17
|
-
log.info(`⚡️ No build step`);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export { build_project };
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
5
|
-
|
|
6
|
-
// Installing can be slow on cold CI runners (no npm cache).
|
|
7
|
-
const INSTALL_TIMEOUT = 15 * 60 * 1000; // 15 minute timeout
|
|
8
|
-
const defaultRun = (command, cwd) => cmd(command, { cwd, timeout: INSTALL_TIMEOUT, enableOutput: true });
|
|
9
|
-
const defaultHasTool = async (name) => {
|
|
10
|
-
try {
|
|
11
|
-
await cmd(`command -v ${name}`);
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
catch {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Dependencies may live in the workdir or be hoisted to a parent directory
|
|
20
|
-
* (monorepos). Walk upwards checking each level, stopping at the repo root
|
|
21
|
-
* (first directory containing `.git`) or the filesystem root.
|
|
22
|
-
*/
|
|
23
|
-
const has_node_modules = (workdir, exists) => {
|
|
24
|
-
let dir = workdir;
|
|
25
|
-
while (true) {
|
|
26
|
-
if (exists(join(dir, "node_modules")))
|
|
27
|
-
return true;
|
|
28
|
-
if (exists(join(dir, ".git")))
|
|
29
|
-
return false;
|
|
30
|
-
const parent = dirname(dir);
|
|
31
|
-
if (parent === dir)
|
|
32
|
-
return false;
|
|
33
|
-
dir = parent;
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Install dependencies when `node_modules` is missing, so `faable deploy`
|
|
38
|
-
* works without a prior `npm ci` step — the generated GitHub Actions workflow
|
|
39
|
-
* is language-agnostic and no longer installs anything itself. The build
|
|
40
|
-
* (`npm run build`) and the image (`COPY . .` ships node_modules) both need
|
|
41
|
-
* dependencies present on the host.
|
|
42
|
-
*
|
|
43
|
-
* The install command honors the project's lockfile; yarn/pnpm fall back to
|
|
44
|
-
* `npm install` when the tool is missing, which may resolve slightly
|
|
45
|
-
* different versions.
|
|
46
|
-
*/
|
|
47
|
-
const ensure_dependencies = async (workdir, deps = {}) => {
|
|
48
|
-
const { run = defaultRun, hasTool = defaultHasTool, exists = existsSync } = deps;
|
|
49
|
-
if (has_node_modules(workdir, exists)) {
|
|
50
|
-
log.info(`📦 Dependencies already installed — skipping install (fresh dependencies)`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
const has = (file) => exists(join(workdir, file));
|
|
54
|
-
let install = "npm install --no-audit --no-fund";
|
|
55
|
-
if (has("package-lock.json") || has("npm-shrinkwrap.json")) {
|
|
56
|
-
install = "npm ci";
|
|
57
|
-
}
|
|
58
|
-
else if (has("yarn.lock")) {
|
|
59
|
-
if (await hasTool("yarn")) {
|
|
60
|
-
install = "yarn install --frozen-lockfile";
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
log.warn("yarn.lock found but yarn is not installed — using npm install");
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else if (has("pnpm-lock.yaml")) {
|
|
67
|
-
if (await hasTool("pnpm")) {
|
|
68
|
-
install = "pnpm install --frozen-lockfile";
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
log.warn("pnpm-lock.yaml found but pnpm is not installed — using npm install");
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
log.info(`📦 node_modules missing — installing dependencies [${install}]`);
|
|
75
|
-
await run(install, workdir);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export { ensure_dependencies };
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import * as R from 'ramda';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
5
|
-
|
|
6
|
-
const has_dep = (pkg, name) => Boolean(R.view(R.lensPath(["dependencies", name]), pkg) ||
|
|
7
|
-
R.view(R.lensPath(["devDependencies", name]), pkg));
|
|
8
|
-
/**
|
|
9
|
-
* Read Angular's build output path from angular.json. Defaults to `dist` when
|
|
10
|
-
* it can't be resolved. Angular ≥17 (application builder) emits into
|
|
11
|
-
* `<outputPath>/browser`, so we append it when the project uses that builder.
|
|
12
|
-
*/
|
|
13
|
-
const resolve_angular_output = (workdir) => {
|
|
14
|
-
const fallback = "dist";
|
|
15
|
-
try {
|
|
16
|
-
const angular_json = read_json_file(path__default.join(workdir, "angular.json"));
|
|
17
|
-
const projects = angular_json?.projects ?? {};
|
|
18
|
-
const project_name = angular_json?.defaultProject ?? Object.keys(projects)[0];
|
|
19
|
-
const build = projects?.[project_name]?.architect?.build;
|
|
20
|
-
const output = build?.options?.outputPath;
|
|
21
|
-
if (!output)
|
|
22
|
-
return fallback;
|
|
23
|
-
const builder = build?.builder ?? "";
|
|
24
|
-
const is_application_builder = builder.includes("application") || builder.includes("browser-esbuild");
|
|
25
|
-
return is_application_builder ? path__default.join(output, "browser") : output;
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return fallback;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Framework registry, evaluated in order. Order matters: Astro/SvelteKit/CRA
|
|
33
|
-
* pull Vite in transitively, so Vite must be the last static fallback.
|
|
34
|
-
*/
|
|
35
|
-
const FRAMEWORKS = [
|
|
36
|
-
// Next.js: handled by its own runtime_strategy/PVC, never static-served here.
|
|
37
|
-
{ type: "next", deps: ["next"] },
|
|
38
|
-
{
|
|
39
|
-
type: "astro",
|
|
40
|
-
deps: ["astro"],
|
|
41
|
-
outputDir: "dist",
|
|
42
|
-
serveCommand: (_dir) => `npx astro preview --host 0.0.0.0 --port $PORT`,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: "gatsby",
|
|
46
|
-
deps: ["gatsby"],
|
|
47
|
-
outputDir: "public",
|
|
48
|
-
serveCommand: (_dir) => `npx gatsby serve --host 0.0.0.0 --port $PORT`,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
type: "cra",
|
|
52
|
-
deps: ["react-scripts"],
|
|
53
|
-
outputDir: "build",
|
|
54
|
-
injectServe: true,
|
|
55
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
type: "vue",
|
|
59
|
-
deps: ["@vue/cli-service"],
|
|
60
|
-
outputDir: "dist",
|
|
61
|
-
injectServe: true,
|
|
62
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
type: "angular",
|
|
66
|
-
deps: ["@angular/cli", "@angular-devkit/build-angular"],
|
|
67
|
-
injectServe: true,
|
|
68
|
-
resolveOutput: resolve_angular_output,
|
|
69
|
-
serveCommand: (dir) => `npx serve -s ${dir} -l $PORT`,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
type: "vite",
|
|
73
|
-
deps: ["vite"],
|
|
74
|
-
outputDir: "dist",
|
|
75
|
-
serveCommand: (_dir) => `npx vite preview --host 0.0.0.0 --port $PORT`,
|
|
76
|
-
},
|
|
77
|
-
];
|
|
78
|
-
/**
|
|
79
|
-
* Detect the framework from package.json and compute how to serve it.
|
|
80
|
-
*
|
|
81
|
-
* When the project defines its own `start` script we never override it (the app
|
|
82
|
-
* ships a real server — custom SSR, Nuxt, Remix, SvelteKit node-adapter, etc.),
|
|
83
|
-
* so `start_command`/`inject_serve` stay neutral.
|
|
84
|
-
*/
|
|
85
|
-
const detect_framework = (params) => {
|
|
86
|
-
const { pkg, workdir, has_start } = params;
|
|
87
|
-
const framework = FRAMEWORKS.find((fw) => fw.deps.some((dep) => has_dep(pkg, dep)));
|
|
88
|
-
if (!framework) {
|
|
89
|
-
return { type: "node", start_command: null, inject_serve: false };
|
|
90
|
-
}
|
|
91
|
-
// Static frameworks only override the start command when the project doesn't
|
|
92
|
-
// ship its own server.
|
|
93
|
-
if (framework.serveCommand && !has_start) {
|
|
94
|
-
const output_dir = framework.resolveOutput
|
|
95
|
-
? framework.resolveOutput(workdir)
|
|
96
|
-
: framework.outputDir ?? "dist";
|
|
97
|
-
const start_command = framework.serveCommand(output_dir);
|
|
98
|
-
log.info(`No start script on package.json, serving ${framework.type} output (${output_dir}) with [${start_command}]`);
|
|
99
|
-
return {
|
|
100
|
-
type: framework.type,
|
|
101
|
-
start_command,
|
|
102
|
-
inject_serve: Boolean(framework.injectServe),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
return { type: framework.type, start_command: null, inject_serve: false };
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export { FRAMEWORKS, detect_framework, resolve_angular_output };
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import * as R from 'ramda';
|
|
2
|
-
import { log } from '../../../../log.js';
|
|
3
|
-
import { render_dockerfile, build_image } from '../shared/docker_image.js';
|
|
4
|
-
import { has_any_of_files } from '../shared/has_any_of_files.js';
|
|
5
|
-
import { analyze_package } from './analyze_package.js';
|
|
6
|
-
import { build_project } from './build_project.js';
|
|
7
|
-
import { ensure_dependencies } from './ensure_dependencies.js';
|
|
8
|
-
import { inject_serve } from './inject_serve.js';
|
|
9
|
-
import { resolve_node_version } from './node_version.js';
|
|
10
|
-
import { wrap_next_config } from './wrap_next_config.js';
|
|
11
|
-
|
|
12
|
-
const BANNER = `NODE_VERSION=$(node --version)
|
|
13
|
-
NPM_VERSION=$(npm --version)
|
|
14
|
-
YARN_VERSION=$(yarn --version 2>/dev/null || echo "n/a")
|
|
15
|
-
|
|
16
|
-
echo "Faable Cloud · [node $NODE_VERSION] [npm $NPM_VERSION] [yarn $YARN_VERSION]"`;
|
|
17
|
-
const node_buildpack = {
|
|
18
|
-
name: "node",
|
|
19
|
-
detect_files: ["package.json"],
|
|
20
|
-
async detect(ctx) {
|
|
21
|
-
if (!has_any_of_files(this.detect_files, ctx.workdir))
|
|
22
|
-
return null;
|
|
23
|
-
const version = await resolve_node_version(ctx.workdir);
|
|
24
|
-
const { build_script, type, start_command, inject_serve } = await analyze_package({ workdir: ctx.workdir });
|
|
25
|
-
// Precedence: explicit faable.json startCommand > framework-detected
|
|
26
|
-
// command (e.g. serving a static SPA) > default `npm run start`.
|
|
27
|
-
const start = ctx.config.startCommand ?? start_command ?? "npm run start";
|
|
28
|
-
return {
|
|
29
|
-
buildpack: "node",
|
|
30
|
-
runtime: { name: "node", version },
|
|
31
|
-
type,
|
|
32
|
-
start_command: start,
|
|
33
|
-
build_script,
|
|
34
|
-
inject_serve,
|
|
35
|
-
from: `node:${version}`,
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
async build(ctx, plan) {
|
|
39
|
-
const env = {
|
|
40
|
-
...R.fromPairs(ctx.env_vars.map((e) => [e.name, e.value])),
|
|
41
|
-
// Platform-injected build-time identity, mirroring the runtime env the
|
|
42
|
-
// controller sets on the pod. FAABLE_DEPLOY_ID (build id ≡ runtime id)
|
|
43
|
-
// is the deterministic Next.js buildId source — user secrets can't
|
|
44
|
-
// override either. See arch/deploy/version-skew-coexistence.md.
|
|
45
|
-
FAABLE_APP_ID: ctx.app.id,
|
|
46
|
-
FAABLE_DEPLOY_ID: ctx.deployment.id,
|
|
47
|
-
};
|
|
48
|
-
log.info(`Building with env variables ${Object.keys(env).join(",")}`);
|
|
49
|
-
// The workflow no longer runs `npm ci` — install here when needed, before
|
|
50
|
-
// the build and before `COPY . .` packages node_modules into the image.
|
|
51
|
-
await ensure_dependencies(ctx.workdir);
|
|
52
|
-
// Host build step: npm script from the plan, else faable.json buildCommand.
|
|
53
|
-
const build_command = plan.build_script
|
|
54
|
-
? `npm run ${plan.build_script}`
|
|
55
|
-
: ctx.config.buildCommand;
|
|
56
|
-
// Next: wrap next.config for the duration of the build so the buildId is
|
|
57
|
-
// the deployment id (transparent version-skew protection). Restored
|
|
58
|
-
// BEFORE the docker build — `COPY . .` must package the original config.
|
|
59
|
-
const wrapped = plan.type === "next" ? await wrap_next_config(ctx.workdir, env) : null;
|
|
60
|
-
try {
|
|
61
|
-
await build_project({ command: build_command, env, cwd: ctx.workdir });
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
await wrapped?.restore();
|
|
65
|
-
}
|
|
66
|
-
// Frameworks without a bundled static server (CRA/Vue/Angular) need
|
|
67
|
-
// `serve` installed into node_modules before packaging.
|
|
68
|
-
if (plan.inject_serve) {
|
|
69
|
-
await inject_serve(ctx.workdir);
|
|
70
|
-
}
|
|
71
|
-
log.info(`⚙️ Start command: ${plan.start_command}`);
|
|
72
|
-
log.info(`Using docker image ${plan.from}-slim`);
|
|
73
|
-
const dockerfile = render_dockerfile({
|
|
74
|
-
from: plan.from,
|
|
75
|
-
env: { NODE_ENV: "production" },
|
|
76
|
-
banner: BANNER,
|
|
77
|
-
start_command: plan.start_command,
|
|
78
|
-
});
|
|
79
|
-
await build_image({ app: ctx.app, workdir: ctx.workdir, dockerfile });
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export { node_buildpack };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { log } from '../../../../log.js';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
|
|
4
|
-
// Pinned for reproducible builds. `serve` is the standalone static server used
|
|
5
|
-
// for frameworks without a bundled preview tool (CRA, Vue, Angular).
|
|
6
|
-
const SERVE_VERSION = "14";
|
|
7
|
-
/**
|
|
8
|
-
* Install `serve` into the project's node_modules so it ships inside the image
|
|
9
|
-
* via `COPY . .` (the Dockerfile does no `npm install`). This lets `npx serve`
|
|
10
|
-
* resolve the local copy at container start — no runtime download needed.
|
|
11
|
-
*
|
|
12
|
-
* `--no-save` keeps the user's package.json/lockfile untouched.
|
|
13
|
-
*/
|
|
14
|
-
const inject_serve = async (workdir) => {
|
|
15
|
-
log.info(`📥 Injecting static server (serve@${SERVE_VERSION}) into image`);
|
|
16
|
-
const timeout = 5 * 60 * 1000; // 5 minute timeout
|
|
17
|
-
await cmd(`npm install serve@${SERVE_VERSION} --no-save --no-audit --no-fund`, { cwd: workdir, timeout, enableOutput: true });
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export { inject_serve };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import path__default from 'path';
|
|
2
|
-
import { cmd } from '../../../../lib/cmd.js';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { read_json_file } from '../shared/read_text_file.js';
|
|
5
|
-
|
|
6
|
-
const getCurrentNodeVersion = async () => {
|
|
7
|
-
const out = await cmd(`node --version`);
|
|
8
|
-
const [_, version] = out.stdout.toString().trim().split("v");
|
|
9
|
-
return version;
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
12
|
-
* Resolve the Node version for the base image: `engines.node` from
|
|
13
|
-
* package.json (resolved to a concrete release via `npm view`), else the
|
|
14
|
-
* version running the deploy. Also validates the package has a `name`
|
|
15
|
-
* (required since the beginning; kept for compatibility).
|
|
16
|
-
*/
|
|
17
|
-
const resolve_node_version = async (workdir) => {
|
|
18
|
-
const packageJSONFile = path__default.join(workdir, "package.json");
|
|
19
|
-
const { name, engines } = read_json_file(packageJSONFile);
|
|
20
|
-
if (!name) {
|
|
21
|
-
throw new Error("Missing name in package.json");
|
|
22
|
-
}
|
|
23
|
-
let runtime_version = await getCurrentNodeVersion();
|
|
24
|
-
if (engines?.node) {
|
|
25
|
-
try {
|
|
26
|
-
const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
|
|
27
|
-
const out = await cmd(check_cmd);
|
|
28
|
-
runtime_version = out.stdout.toString().trim();
|
|
29
|
-
log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
log.info(`Node version defined in engines in package.json is not valid (${engines.node}), using current version ${runtime_version}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
log.info(`Node version ${runtime_version}`);
|
|
37
|
-
}
|
|
38
|
-
return runtime_version;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export { resolve_node_version };
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { existsSync, rmSync, renameSync } from 'fs';
|
|
2
|
-
import { rename, writeFile, rm, readFile } from 'fs/promises';
|
|
3
|
-
import path__default from 'path';
|
|
4
|
-
import { log } from '../../../../log.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Transparent deterministic-buildId for Next.js apps
|
|
8
|
-
* (arch/deploy/version-skew-coexistence.md, Fase 1).
|
|
9
|
-
*
|
|
10
|
-
* Next reads no env var for its buildId — the only deterministic source is
|
|
11
|
-
* `generateBuildId` in the user's next.config. Rather than asking users to
|
|
12
|
-
* edit their config, the buildpack wraps it for the duration of `next build`:
|
|
13
|
-
* the original file is renamed aside and replaced by a generated wrapper that
|
|
14
|
-
* resolves it (object | function | promise | async) and adds a
|
|
15
|
-
* `generateBuildId` reading FAABLE_DEPLOY_ID — ONLY when the user doesn't
|
|
16
|
-
* define their own.
|
|
17
|
-
*
|
|
18
|
-
* The wrapper is self-contained (no imports beyond the original config): the
|
|
19
|
-
* user's node_modules has no faable packages.
|
|
20
|
-
*
|
|
21
|
-
* Restoration is guaranteed: `restore()` runs in the caller's finally (before
|
|
22
|
-
* the docker build, so `COPY . .` packages the original config — `next start`
|
|
23
|
-
* loads next.config at runtime too), and a signal handler covers Ctrl-C
|
|
24
|
-
* mid-build. `next.config.ts` is skipped with a warning (it needs Next's own
|
|
25
|
-
* TS loader; not wrappable from a JS wrapper).
|
|
26
|
-
*/
|
|
27
|
-
const CONFIG_FILES = ["next.config.js", "next.config.cjs", "next.config.mjs"];
|
|
28
|
-
const ORIGINAL_PREFIX = "next.config.faable-original";
|
|
29
|
-
const wrapperBody = (originalFile, flavor) => {
|
|
30
|
-
const resolver = `
|
|
31
|
-
const resolveConfig = async (original, phase, args) => {
|
|
32
|
-
let config = original && original.__esModule ? original.default : original;
|
|
33
|
-
if (typeof config === "function") config = config(phase, args);
|
|
34
|
-
config = await config;
|
|
35
|
-
config = config || {};
|
|
36
|
-
if (config.generateBuildId) return config; // user-defined wins, never override
|
|
37
|
-
return {
|
|
38
|
-
...config,
|
|
39
|
-
// Deterministic buildId = platform deployment id (build id ≡ runtime id).
|
|
40
|
-
generateBuildId: () => process.env.FAABLE_DEPLOY_ID ?? null,
|
|
41
|
-
};
|
|
42
|
-
};`;
|
|
43
|
-
if (flavor === "cjs") {
|
|
44
|
-
return `// Generated by faable deploy for this build only — auto-restored afterwards.
|
|
45
|
-
// Adds a deterministic buildId (FAABLE_DEPLOY_ID) unless the config defines
|
|
46
|
-
// its own generateBuildId. See faable.com docs on version skew.
|
|
47
|
-
${originalFile ? `const original = require("./${originalFile}");` : `const original = {};`}
|
|
48
|
-
${resolver}
|
|
49
|
-
module.exports = (phase, args) => resolveConfig(original, phase, args);
|
|
50
|
-
`;
|
|
51
|
-
}
|
|
52
|
-
return `// Generated by faable deploy for this build only — auto-restored afterwards.
|
|
53
|
-
// Adds a deterministic buildId (FAABLE_DEPLOY_ID) unless the config defines
|
|
54
|
-
// its own generateBuildId. See faable.com docs on version skew.
|
|
55
|
-
${originalFile ? `import original from "./${originalFile}";` : `const original = {};`}
|
|
56
|
-
${resolver}
|
|
57
|
-
export default (phase, args) => resolveConfig(original, phase, args);
|
|
58
|
-
`;
|
|
59
|
-
};
|
|
60
|
-
const packageIsESM = async (workdir) => {
|
|
61
|
-
try {
|
|
62
|
-
const pkg = JSON.parse(await readFile(path__default.join(workdir, "package.json"), "utf8"));
|
|
63
|
-
return pkg.type === "module";
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
/**
|
|
70
|
-
* Wraps the project's next.config for the build. Returns null (no-op) when
|
|
71
|
-
* there is nothing to do: FAABLE_DEPLOY_ID absent from `env`, a `.ts` config,
|
|
72
|
-
* or the user already handles the buildId. Callers MUST `await
|
|
73
|
-
* wrapped?.restore()` in a finally.
|
|
74
|
-
*/
|
|
75
|
-
const wrap_next_config = async (workdir, env) => {
|
|
76
|
-
if (!env.FAABLE_DEPLOY_ID)
|
|
77
|
-
return null;
|
|
78
|
-
if (existsSync(path__default.join(workdir, "next.config.ts"))) {
|
|
79
|
-
log.warn(`next.config.ts detected — skipping the deterministic buildId wrap (define generateBuildId with FAABLE_DEPLOY_ID yourself to opt in).`);
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
const configFile = CONFIG_FILES.find((f) => existsSync(path__default.join(workdir, f)));
|
|
83
|
-
// Wrapper flavor/extension: mirror the original; with no config at all,
|
|
84
|
-
// .mjs is unambiguous regardless of package.json type.
|
|
85
|
-
let wrapperFile;
|
|
86
|
-
let originalFile = null;
|
|
87
|
-
let flavor;
|
|
88
|
-
if (!configFile) {
|
|
89
|
-
wrapperFile = "next.config.mjs";
|
|
90
|
-
flavor = "esm";
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
const ext = path__default.extname(configFile); // .js | .cjs | .mjs
|
|
94
|
-
originalFile = `${ORIGINAL_PREFIX}${ext}`;
|
|
95
|
-
wrapperFile = configFile;
|
|
96
|
-
flavor =
|
|
97
|
-
ext === ".mjs" || (ext === ".js" && (await packageIsESM(workdir)))
|
|
98
|
-
? "esm"
|
|
99
|
-
: "cjs";
|
|
100
|
-
}
|
|
101
|
-
const wrapperPath = path__default.join(workdir, wrapperFile);
|
|
102
|
-
const originalPath = originalFile ? path__default.join(workdir, originalFile) : null;
|
|
103
|
-
if (configFile && originalPath) {
|
|
104
|
-
await rename(path__default.join(workdir, configFile), originalPath);
|
|
105
|
-
}
|
|
106
|
-
await writeFile(wrapperPath, wrapperBody(originalFile, flavor), "utf8");
|
|
107
|
-
log.info(`🧬 Deterministic buildId: wrapped ${configFile ?? "(no next.config)"} → generateBuildId from FAABLE_DEPLOY_ID`);
|
|
108
|
-
// Ctrl-C / SIGTERM mid-build must not leave the user's workdir mutated.
|
|
109
|
-
const restoreSync = () => {
|
|
110
|
-
try {
|
|
111
|
-
rmSync(wrapperPath, { force: true });
|
|
112
|
-
if (configFile && originalPath) {
|
|
113
|
-
renameSync(originalPath, path__default.join(workdir, configFile));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// Best effort on the signal path
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
process.once("SIGINT", restoreSync);
|
|
121
|
-
process.once("SIGTERM", restoreSync);
|
|
122
|
-
let restored = false;
|
|
123
|
-
return {
|
|
124
|
-
restore: async () => {
|
|
125
|
-
if (restored)
|
|
126
|
-
return;
|
|
127
|
-
restored = true;
|
|
128
|
-
process.removeListener("SIGINT", restoreSync);
|
|
129
|
-
process.removeListener("SIGTERM", restoreSync);
|
|
130
|
-
await rm(wrapperPath, { force: true });
|
|
131
|
-
if (configFile && originalPath) {
|
|
132
|
-
await rename(originalPath, path__default.join(workdir, configFile));
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export { wrap_next_config };
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path__default from 'path';
|
|
3
|
-
import { log } from '../../../../log.js';
|
|
4
|
-
import { render_dockerfile, build_image } from '../shared/docker_image.js';
|
|
5
|
-
import { has_any_of_files } from '../shared/has_any_of_files.js';
|
|
6
|
-
import { read_text_file } from '../shared/read_text_file.js';
|
|
7
|
-
import { cerebrium_provider } from './providers/cerebrium.js';
|
|
8
|
-
import { pipfile_provider } from './providers/pipfile.js';
|
|
9
|
-
import { pyproject_provider } from './providers/pyproject.js';
|
|
10
|
-
import { requirements_provider } from './providers/requirements.js';
|
|
11
|
-
import { resolve_python_version } from './python_version.js';
|
|
12
|
-
import { resolve_start, with_server_injection, read_dependencies_text } from './resolve_start.js';
|
|
13
|
-
|
|
14
|
-
const BANNER = `PYTHON_VERSION=$(python --version 2>&1)
|
|
15
|
-
PIP_VERSION=$(pip --version 2>&1)
|
|
16
|
-
|
|
17
|
-
echo "Faable Cloud · [$PYTHON_VERSION] [$PIP_VERSION]"`;
|
|
18
|
-
const DOCS_URL = "https://faable.com/docs/deploy/build-requirements";
|
|
19
|
-
// Evaluated in order: the first provider whose manifest exists supplies the
|
|
20
|
-
// install command. Classic manifests come before platform-specific ones, so
|
|
21
|
-
// a repo shipping both requirements.txt and cerebrium.toml builds from the
|
|
22
|
-
// standard manifest.
|
|
23
|
-
const PROVIDERS = [
|
|
24
|
-
requirements_provider,
|
|
25
|
-
pyproject_provider,
|
|
26
|
-
pipfile_provider,
|
|
27
|
-
cerebrium_provider,
|
|
28
|
-
];
|
|
29
|
-
const detect_with_provider = (ctx, provider) => {
|
|
30
|
-
const resolved = provider.resolve(ctx.workdir);
|
|
31
|
-
// Framework detection blob: all classic manifests plus whatever the winning
|
|
32
|
-
// provider contributes (e.g. cerebrium pip package names).
|
|
33
|
-
const deps = [read_dependencies_text(ctx.workdir), resolved.deps_text ?? ""]
|
|
34
|
-
.join("\n")
|
|
35
|
-
.toLowerCase();
|
|
36
|
-
let start_command;
|
|
37
|
-
let server;
|
|
38
|
-
try {
|
|
39
|
-
({ start_command, server } = resolve_start(ctx.workdir, deps, ctx.config, resolved.start_hint));
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (provider.name === "cerebrium") {
|
|
43
|
-
throw new Error("Detected a Cerebrium project (cerebrium.toml) but couldn't find a web " +
|
|
44
|
-
"entrypoint. Faable runs web services — expose a FastAPI/Flask app or " +
|
|
45
|
-
`set \`startCommand\` in faable.json. Docs: ${DOCS_URL}`, { cause: error });
|
|
46
|
-
}
|
|
47
|
-
throw error;
|
|
48
|
-
}
|
|
49
|
-
// faable.json buildCommand overrides the provider's install. The cacheable
|
|
50
|
-
// manifest layer only applies to the provider's own command — an arbitrary
|
|
51
|
-
// buildCommand may need the full source.
|
|
52
|
-
const base_install = ctx.config.buildCommand ?? resolved.install_command;
|
|
53
|
-
const install_command = with_server_injection(base_install, server, deps);
|
|
54
|
-
const install_files = ctx.config.buildCommand
|
|
55
|
-
? undefined
|
|
56
|
-
: resolved.install_files;
|
|
57
|
-
const version = resolve_python_version(ctx.workdir, resolved.python_version);
|
|
58
|
-
log.info(`Using python@${version}`);
|
|
59
|
-
log.info(`📦 Install command: ${install_command}`);
|
|
60
|
-
log.info(`⚙️ Start command: ${start_command}`);
|
|
61
|
-
return {
|
|
62
|
-
buildpack: "python",
|
|
63
|
-
runtime: { name: "python", version },
|
|
64
|
-
type: "python",
|
|
65
|
-
start_command,
|
|
66
|
-
install_command,
|
|
67
|
-
install_files,
|
|
68
|
-
from: `python:${version}`,
|
|
69
|
-
};
|
|
70
|
-
};
|
|
71
|
-
const python_buildpack = {
|
|
72
|
-
name: "python",
|
|
73
|
-
detect_files: PROVIDERS.flatMap((p) => p.files),
|
|
74
|
-
fallback_files: ["main.py", "app.py", "wsgi.py"],
|
|
75
|
-
async detect(ctx) {
|
|
76
|
-
const provider = PROVIDERS.find((p) => has_any_of_files(p.files, ctx.workdir));
|
|
77
|
-
if (!provider)
|
|
78
|
-
return null;
|
|
79
|
-
return detect_with_provider(ctx, provider);
|
|
80
|
-
},
|
|
81
|
-
// Weak-signal pass: a Python entrypoint with no dependency manifest at all.
|
|
82
|
-
// Registered as fallback so it loses against any strong trigger (Dockerfile).
|
|
83
|
-
async detect_fallback(ctx) {
|
|
84
|
-
const entries = this.fallback_files.filter((f) => fs.existsSync(path__default.join(ctx.workdir, f)));
|
|
85
|
-
if (entries.length === 0)
|
|
86
|
-
return null;
|
|
87
|
-
log.warn(`⚠️ No dependency manifest (requirements.txt/pyproject.toml/Pipfile) found — ` +
|
|
88
|
-
`building from ${entries.join(", ")} without installing dependencies. ` +
|
|
89
|
-
`Your app will likely need a requirements.txt; see ${DOCS_URL}`);
|
|
90
|
-
// Framework detection blob = the entry files themselves: an
|
|
91
|
-
// `from fastapi import FastAPI` line carries the token detection needs.
|
|
92
|
-
const deps = entries
|
|
93
|
-
.map((f) => read_text_file(path__default.join(ctx.workdir, f)))
|
|
94
|
-
.join("\n")
|
|
95
|
-
.toLowerCase();
|
|
96
|
-
const { start_command, server } = resolve_start(ctx.workdir, deps, ctx.config);
|
|
97
|
-
const install_command = with_server_injection(ctx.config.buildCommand, server, deps);
|
|
98
|
-
const version = resolve_python_version(ctx.workdir);
|
|
99
|
-
log.info(`Using python@${version}`);
|
|
100
|
-
log.info(`📦 Install command: ${install_command}`);
|
|
101
|
-
log.info(`⚙️ Start command: ${start_command}`);
|
|
102
|
-
return {
|
|
103
|
-
buildpack: "python",
|
|
104
|
-
runtime: { name: "python", version },
|
|
105
|
-
type: "python",
|
|
106
|
-
start_command,
|
|
107
|
-
install_command,
|
|
108
|
-
from: `python:${version}`,
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
async build(ctx, plan) {
|
|
112
|
-
// Dependencies install inside the image (unlike node); nothing runs on
|
|
113
|
-
// the host here.
|
|
114
|
-
log.info(`Using docker image ${plan.from}-slim`);
|
|
115
|
-
const dockerfile = render_dockerfile({
|
|
116
|
-
from: plan.from,
|
|
117
|
-
env: { PYTHONUNBUFFERED: "1" },
|
|
118
|
-
banner: BANNER,
|
|
119
|
-
start_command: plan.start_command,
|
|
120
|
-
install_command: plan.install_command,
|
|
121
|
-
install_files: plan.install_files,
|
|
122
|
-
});
|
|
123
|
-
await build_image({ app: ctx.app, workdir: ctx.workdir, dockerfile });
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export { python_buildpack };
|