@faable/faable 1.17.0 → 1.18.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.
Files changed (34) hide show
  1. package/dist/api/FaableApi.js +15 -0
  2. package/dist/commands/deploy/index.js +93 -54
  3. package/dist/commands/deploy/remote/follow.js +61 -0
  4. package/dist/commands/deploy/remote/index.js +48 -0
  5. package/dist/commands/deploy/remote/manifest.js +77 -0
  6. package/dist/commands/deploy/remote/upload.js +61 -0
  7. package/package.json +2 -1
  8. package/dist/commands/deploy/buildpacks/Buildpack.js +0 -11
  9. package/dist/commands/deploy/buildpacks/DetectError.js +0 -78
  10. package/dist/commands/deploy/buildpacks/docker/index.js +0 -42
  11. package/dist/commands/deploy/buildpacks/foreign_platforms.js +0 -21
  12. package/dist/commands/deploy/buildpacks/node/analyze_package.js +0 -35
  13. package/dist/commands/deploy/buildpacks/node/build_project.js +0 -21
  14. package/dist/commands/deploy/buildpacks/node/ensure_dependencies.js +0 -78
  15. package/dist/commands/deploy/buildpacks/node/frameworks.js +0 -108
  16. package/dist/commands/deploy/buildpacks/node/index.js +0 -83
  17. package/dist/commands/deploy/buildpacks/node/inject_serve.js +0 -20
  18. package/dist/commands/deploy/buildpacks/node/node_version.js +0 -41
  19. package/dist/commands/deploy/buildpacks/node/wrap_next_config.js +0 -138
  20. package/dist/commands/deploy/buildpacks/python/index.js +0 -127
  21. package/dist/commands/deploy/buildpacks/python/parse_procfile.js +0 -23
  22. package/dist/commands/deploy/buildpacks/python/providers/cerebrium.js +0 -43
  23. package/dist/commands/deploy/buildpacks/python/providers/parse_cerebrium_toml.js +0 -56
  24. package/dist/commands/deploy/buildpacks/python/providers/pipfile.js +0 -13
  25. package/dist/commands/deploy/buildpacks/python/providers/pyproject.js +0 -13
  26. package/dist/commands/deploy/buildpacks/python/providers/requirements.js +0 -14
  27. package/dist/commands/deploy/buildpacks/python/python_version.js +0 -41
  28. package/dist/commands/deploy/buildpacks/python/resolve_start.js +0 -149
  29. package/dist/commands/deploy/buildpacks/registry.js +0 -61
  30. package/dist/commands/deploy/buildpacks/shared/docker_image.js +0 -49
  31. package/dist/commands/deploy/buildpacks/shared/has_any_of_files.js +0 -14
  32. package/dist/commands/deploy/buildpacks/shared/read_text_file.js +0 -31
  33. package/dist/commands/deploy/buildpacks/shared/templates/Dockerfile +0 -38
  34. 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 };