@faable/faable 1.8.0 → 1.10.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 (38) hide show
  1. package/dist/commands/deploy/buildpacks/Buildpack.js +11 -0
  2. package/dist/commands/deploy/buildpacks/DetectError.js +78 -0
  3. package/dist/commands/deploy/buildpacks/docker/index.js +42 -0
  4. package/dist/commands/deploy/buildpacks/foreign_platforms.js +21 -0
  5. package/dist/commands/deploy/{node-pipeline → buildpacks/node}/analyze_package.js +1 -1
  6. package/dist/commands/deploy/buildpacks/node/build_project.js +21 -0
  7. package/dist/commands/deploy/{node-pipeline → buildpacks/node}/ensure_dependencies.js +2 -2
  8. package/dist/commands/deploy/{node-pipeline → buildpacks/node}/frameworks.js +4 -4
  9. package/dist/commands/deploy/buildpacks/node/index.js +65 -0
  10. package/dist/commands/deploy/{node-pipeline → buildpacks/node}/inject_serve.js +2 -2
  11. package/dist/commands/deploy/{runtime-detect/strategies/nodejs.js → buildpacks/node/node_version.js} +7 -15
  12. package/dist/commands/deploy/buildpacks/python/index.js +126 -0
  13. package/dist/commands/deploy/buildpacks/python/providers/cerebrium.js +43 -0
  14. package/dist/commands/deploy/buildpacks/python/providers/parse_cerebrium_toml.js +56 -0
  15. package/dist/commands/deploy/buildpacks/python/providers/pipfile.js +13 -0
  16. package/dist/commands/deploy/buildpacks/python/providers/pyproject.js +13 -0
  17. package/dist/commands/deploy/buildpacks/python/providers/requirements.js +14 -0
  18. package/dist/commands/deploy/{runtime-detect/strategies/python.js → buildpacks/python/python_version.js} +6 -14
  19. package/dist/commands/deploy/{python-pipeline/analyze_python.js → buildpacks/python/resolve_start.js} +28 -38
  20. package/dist/commands/deploy/buildpacks/registry.js +61 -0
  21. package/dist/commands/deploy/buildpacks/shared/docker_image.js +49 -0
  22. package/dist/commands/deploy/{python-pipeline → buildpacks/shared}/templates/Dockerfile +17 -3
  23. package/dist/commands/deploy/buildpacks/shared/templates/entrypoint.sh +4 -0
  24. package/dist/commands/deploy/index.js +22 -30
  25. package/dist/lib/Configuration.js +11 -0
  26. package/package.json +2 -1
  27. package/dist/commands/deploy/node-pipeline/build_docker.js +0 -52
  28. package/dist/commands/deploy/node-pipeline/build_project.js +0 -26
  29. package/dist/commands/deploy/node-pipeline/index.js +0 -44
  30. package/dist/commands/deploy/node-pipeline/templates/Dockerfile +0 -18
  31. package/dist/commands/deploy/node-pipeline/templates/entrypoint.sh +0 -8
  32. package/dist/commands/deploy/python-pipeline/build_docker.js +0 -42
  33. package/dist/commands/deploy/python-pipeline/index.js +0 -25
  34. package/dist/commands/deploy/python-pipeline/templates/entrypoint.sh +0 -7
  35. package/dist/commands/deploy/runtime-detect/runtime_detection.js +0 -25
  36. package/dist/commands/deploy/runtime-detect/strategies/docker.js +0 -9
  37. /package/dist/commands/deploy/{python-pipeline → buildpacks/python}/parse_procfile.js +0 -0
  38. /package/dist/commands/deploy/{runtime-detect/helpers → buildpacks/shared}/has_any_of_files.js +0 -0
@@ -1,10 +1,9 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
- import { log } from '../../../log.js';
4
- import { Configuration } from '../../../lib/Configuration.js';
3
+ import { log } from '../../../../log.js';
5
4
  import { parse_procfile } from './parse_procfile.js';
6
5
 
7
- /** Combine all dependency manifests into one lowercased blob for cheap lookups. */
6
+ /** Combine all classic dependency manifests into one lowercased blob. */
8
7
  const read_dependencies_text = (workdir) => {
9
8
  const files = ["requirements.txt", "pyproject.toml", "Pipfile"];
10
9
  return files
@@ -65,13 +64,20 @@ const find_app_module = (workdir, pattern) => {
65
64
  }
66
65
  return first_existing;
67
66
  };
68
- /** Resolve the container start command and which server it needs installed. */
69
- const resolve_start = (workdir, deps) => {
67
+ /**
68
+ * Resolve the container start command and which server it needs installed.
69
+ * Precedence: faable.json startCommand → Procfile `web:` → provider start
70
+ * hint (e.g. cerebrium entrypoint) → framework detection (Django → FastAPI/
71
+ * ASGI → Flask).
72
+ */
73
+ const resolve_start = (workdir, deps, config, start_hint) => {
70
74
  // 1. Explicit override in faable.json
71
- const configured = Configuration.instance().configuredStartCommand;
72
- if (configured) {
75
+ if (config.startCommand) {
73
76
  log.info(`Using start command from faable.json`);
74
- return { start_command: configured, server: server_from_command(configured) };
77
+ return {
78
+ start_command: config.startCommand,
79
+ server: server_from_command(config.startCommand),
80
+ };
75
81
  }
76
82
  // 2. Procfile `web:` line
77
83
  const procfile = parse_procfile(workdir);
@@ -79,7 +85,12 @@ const resolve_start = (workdir, deps) => {
79
85
  log.info(`Using start command from Procfile`);
80
86
  return { start_command: procfile, server: server_from_command(procfile) };
81
87
  }
82
- // 3. Framework detection
88
+ // 3. Provider hint (e.g. cerebrium.toml runtime entrypoint)
89
+ if (start_hint) {
90
+ log.info(`Using start command from the project manifest`);
91
+ return { start_command: start_hint, server: server_from_command(start_hint) };
92
+ }
93
+ // 4. Framework detection
83
94
  // Django: manage.py + the package holding wsgi.py
84
95
  if (fs.existsSync(path__default.join(workdir, "manage.py"))) {
85
96
  const pkg = find_django_package(workdir);
@@ -115,23 +126,13 @@ const resolve_start = (workdir, deps) => {
115
126
  }
116
127
  throw new Error("Could not detect how to start this Python app. Set `startCommand` in faable.json or add a Procfile with a `web:` line.");
117
128
  };
118
- /** Build the dependency-install command run during the Docker build. */
119
- const build_install_command = (workdir, server, deps) => {
120
- const steps = [];
121
- const configured_build = Configuration.instance().buildCommand;
122
- if (configured_build) {
123
- steps.push(configured_build);
124
- }
125
- else if (fs.existsSync(path__default.join(workdir, "requirements.txt"))) {
126
- steps.push("pip install --no-cache-dir -r requirements.txt");
127
- }
128
- else if (fs.existsSync(path__default.join(workdir, "pyproject.toml"))) {
129
- steps.push("pip install --no-cache-dir .");
130
- }
131
- else if (fs.existsSync(path__default.join(workdir, "Pipfile"))) {
132
- steps.push("pip install --no-cache-dir pipenv && pipenv install --system --deploy");
133
- }
134
- // Ensure the WSGI/ASGI server is available when not already declared.
129
+ /**
130
+ * Append the WSGI/ASGI server install when the start command needs one that
131
+ * isn't already declared in the dependencies. Returns "true" (no-op) when
132
+ * there is nothing to install at all.
133
+ */
134
+ const with_server_injection = (base, server, deps) => {
135
+ const steps = base ? [base] : [];
135
136
  if (server === "gunicorn" && !has_token(deps, "gunicorn")) {
136
137
  steps.push("pip install --no-cache-dir gunicorn");
137
138
  }
@@ -139,20 +140,9 @@ const build_install_command = (workdir, server, deps) => {
139
140
  steps.push("pip install --no-cache-dir uvicorn[standard]");
140
141
  }
141
142
  if (steps.length === 0) {
142
- // No manifest found — nothing to install, but warn the user.
143
- log.warn("No requirements.txt/pyproject.toml/Pipfile found");
144
143
  return "true";
145
144
  }
146
145
  return steps.join(" && ");
147
146
  };
148
- const analyze_python = async (params) => {
149
- const { workdir } = params;
150
- const deps = read_dependencies_text(workdir);
151
- const { start_command, server } = resolve_start(workdir, deps);
152
- const install_command = build_install_command(workdir, server, deps);
153
- log.info(`📦 Install command: ${install_command}`);
154
- log.info(`⚙️ Start command: ${start_command}`);
155
- return { install_command, start_command };
156
- };
157
147
 
158
- export { analyze_python };
148
+ export { find_app_module, has_token, read_dependencies_text, resolve_start, with_server_injection };
@@ -0,0 +1,61 @@
1
+ import { DetectError } from './DetectError.js';
2
+ import { docker_buildpack } from './docker/index.js';
3
+ import { node_buildpack } from './node/index.js';
4
+ import { python_buildpack } from './python/index.js';
5
+
6
+ /**
7
+ * Ordered registry, first claim wins. Order is load-bearing:
8
+ * 1. node before python — full-stack repos shipping both a package.json and
9
+ * Python manifests build as node (historical rule).
10
+ * 2. python (all its manifest providers) before docker — a dependency
11
+ * manifest beats a Dockerfile; the Dockerfile is the explicit escape
12
+ * hatch evaluated last among strong triggers.
13
+ * Weak signals (python's entrypoint fallback) run in a second pass so they
14
+ * lose against ANY strong trigger without buildpacks knowing about each other.
15
+ */
16
+ const BUILDPACKS = [
17
+ node_buildpack,
18
+ python_buildpack,
19
+ docker_buildpack,
20
+ ];
21
+ const buildpack_names = () => BUILDPACKS.map((b) => b.name);
22
+ const get_buildpack = (name) => BUILDPACKS.find((b) => b.name === name);
23
+ /**
24
+ * Resolve the buildpack plan for a workdir. `override` (from --buildpack or
25
+ * faable.json) skips detection order and forces one buildpack; it still runs
26
+ * that buildpack's detect so the plan is computed from real project files.
27
+ */
28
+ const detect_buildpack = async (ctx, override) => {
29
+ if (override) {
30
+ const buildpack = get_buildpack(override);
31
+ if (!buildpack) {
32
+ throw new Error(`Unknown buildpack "${override}". Valid buildpacks: ${buildpack_names().join(", ")}.`);
33
+ }
34
+ const plan = (await buildpack.detect(ctx)) ??
35
+ (await buildpack.detect_fallback?.(ctx)) ??
36
+ null;
37
+ if (!plan) {
38
+ throw new Error(`Buildpack "${override}" was forced but none of its trigger files ` +
39
+ `(${[...buildpack.detect_files, ...(buildpack.fallback_files ?? [])].join(", ")}) ` +
40
+ `exist in ${ctx.workdir}.`);
41
+ }
42
+ return plan;
43
+ }
44
+ // Strong pass: manifests and Dockerfiles.
45
+ for (const buildpack of BUILDPACKS) {
46
+ const plan = await buildpack.detect(ctx);
47
+ if (plan)
48
+ return plan;
49
+ }
50
+ // Fallback pass: weak signals, only when nothing claimed the project.
51
+ for (const buildpack of BUILDPACKS) {
52
+ if (!buildpack.detect_fallback)
53
+ continue;
54
+ const plan = await buildpack.detect_fallback(ctx);
55
+ if (plan)
56
+ return plan;
57
+ }
58
+ throw new DetectError(ctx.workdir, BUILDPACKS);
59
+ };
60
+
61
+ export { BUILDPACKS, buildpack_names, detect_buildpack, get_buildpack };
@@ -0,0 +1,49 @@
1
+ import fs from 'fs-extra';
2
+ import Handlebars from 'handlebars';
3
+ import * as path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { cmd } from '../../../../lib/cmd.js';
6
+ import { log } from '../../../../log.js';
7
+
8
+ const __filename$1 = fileURLToPath(import.meta.url);
9
+ const __dirname$1 = path.dirname(__filename$1);
10
+ const templates_dir = path.join(__dirname$1, "templates");
11
+ const dockerfile_source = fs
12
+ .readFileSync(path.join(templates_dir, "Dockerfile"))
13
+ .toString();
14
+ const entrypoint_source = fs
15
+ .readFileSync(path.join(templates_dir, "entrypoint.sh"))
16
+ .toString("utf-8");
17
+ // Backslash-escape content so it survives BOTH the unquoted bash heredoc the
18
+ // Dockerfile is piped through (docker build -f -<<EOF) and, for the entry
19
+ // script, the single-quoted `RUN echo '...'`.
20
+ Handlebars.registerHelper("escape", function (variable) {
21
+ const escaped_lines = variable
22
+ .replace(/(['`\\])/g, "\\$1")
23
+ .replace(/([$])/g, "\\$1");
24
+ return escaped_lines.split("\n").join("\\n");
25
+ });
26
+ const docker_template = Handlebars.compile(dockerfile_source);
27
+ const entrypoint_template = Handlebars.compile(entrypoint_source);
28
+ /** Pure render of the final Dockerfile contents — snapshot-tested. */
29
+ const render_dockerfile = (params) => {
30
+ // NOTE: use slim to build projects
31
+ const from = [params.from, "slim"].filter((e) => e).join("-");
32
+ const entry_script = entrypoint_template({ banner: params.banner });
33
+ return docker_template({
34
+ from,
35
+ env: params.env,
36
+ entry_script,
37
+ start_command: params.start_command,
38
+ install_command: params.install_command,
39
+ install_files: params.install_files,
40
+ });
41
+ };
42
+ const build_image = async (props) => {
43
+ const { app, workdir, dockerfile } = props;
44
+ log.info(`📦 Packaging inside a docker image`);
45
+ const timeout = 10 * 60 * 1000; // 10 minute timeout
46
+ await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
47
+ };
48
+
49
+ export { build_image, render_dockerfile };
@@ -6,17 +6,31 @@ WORKDIR /faable/app
6
6
 
7
7
  # Environment variables for runtime
8
8
  ENV PORT=80
9
- ENV PYTHONUNBUFFERED=1
9
+ {{#each env}}
10
+ ENV {{@key}}={{this}}
11
+ {{/each}}
10
12
  # `escape` keeps `$PORT` literal so the build heredoc (unquoted bash) doesn't
11
13
  # eat it; Docker then expands $PORT (=80) when building the ENV value.
12
14
  ENV START_COMMAND="{{{escape start_command}}}"
13
15
 
16
+ {{#if install_files}}
17
+ # Copy dependency manifests first so the install layer stays cached until
18
+ # they change (any source change no longer reinstalls dependencies).
19
+ {{#each install_files}}
20
+ COPY {{this}} ./
21
+ {{/each}}
22
+ RUN {{{escape install_command}}}
23
+ {{/if}}
24
+
14
25
  # Copy Usercode
15
26
  COPY . .
16
27
 
17
- # Install dependencies (Python needs them built into the image, unlike node_modules).
18
- # Triple-stache + escape: keep `&&` intact and any `$` heredoc-safe.
28
+ {{#unless install_files}}
29
+ {{#if install_command}}
30
+ # Install needs the full source (e.g. `pip install .`), so it runs after COPY.
19
31
  RUN {{{escape install_command}}}
32
+ {{/if}}
33
+ {{/unless}}
20
34
 
21
35
  # Entrypoint stript
22
36
  RUN echo '{{{escape entry_script}}}' >> entrypoint.sh
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+
3
+ {{{banner}}}
4
+ eval $START_COMMAND
@@ -1,12 +1,10 @@
1
1
  import { requireApi } from '../../api/context.js';
2
- import { cmd } from '../../lib/cmd.js';
3
2
  import { Configuration } from '../../lib/Configuration.js';
4
3
  import { log } from '../../log.js';
4
+ import { plan_summary } from './buildpacks/Buildpack.js';
5
+ import { detect_buildpack, get_buildpack, buildpack_names } from './buildpacks/registry.js';
5
6
  import { check_environment } from './check_environment.js';
6
7
  import { git_context } from './git_context.js';
7
- import { build_node } from './node-pipeline/index.js';
8
- import { build_python } from './python-pipeline/index.js';
9
- import { runtime_detection } from './runtime-detect/runtime_detection.js';
10
8
  import { upload_tag } from './upload_tag.js';
11
9
 
12
10
  const deploy = {
@@ -22,6 +20,12 @@ const deploy = {
22
20
  alias: 'w',
23
21
  type: 'string',
24
22
  description: 'Working directory'
23
+ })
24
+ .option('buildpack', {
25
+ alias: 'b',
26
+ type: 'string',
27
+ choices: buildpack_names(),
28
+ description: 'Force a specific buildpack (overrides auto-detection and faable.json)'
25
29
  })
26
30
  .showHelpOnFail(false);
27
31
  },
@@ -29,8 +33,10 @@ const deploy = {
29
33
  const workdir = args.workdir || process.cwd();
30
34
  const ctx = await requireApi();
31
35
  const { api } = ctx;
32
- // Resolve runtime
33
- const { runtime } = await runtime_detection(workdir);
36
+ // Resolve the buildpack plan (detection or forced override). All the
37
+ // build thinking happens here; build() below just executes the plan.
38
+ const config = Configuration.instance().deployConfig();
39
+ const plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
34
40
  // app_id resolution (the user never has to look one up):
35
41
  // 1. explicit positional (monorepo escape hatch)
36
42
  // 2. OIDC in CI — the backend resolves the app from the linked repository
@@ -42,33 +48,19 @@ const deploy = {
42
48
  const app = await api.getApp(app_id);
43
49
  // Check if we can build docker images
44
50
  await check_environment();
45
- log.info(`🚀 Deploying "${app.name}" (${app.id}) runtime=${runtime.name}-${runtime.version}`);
51
+ const runtime_label = plan.runtime.version
52
+ ? `${plan.runtime.name}-${plan.runtime.version}`
53
+ : plan.runtime.name;
54
+ log.info(`🚀 Deploying "${app.name}" (${app.id}) runtime=${runtime_label}`);
55
+ log.info(`🧩 Build plan ${plan_summary(plan)}`);
46
56
  // get environment variables
47
57
  const env_vars = await api.getAppSecrets(app.id);
48
- let type;
49
- if (runtime.name == 'node') {
50
- const node_result = await build_node(app, {
51
- workdir,
52
- runtime,
53
- env_vars
54
- });
55
- type = node_result.type;
56
- }
57
- else if (runtime.name == 'python') {
58
- const python_result = await build_python(app, {
59
- workdir,
60
- runtime});
61
- type = python_result.type;
62
- }
63
- else if (runtime.name == 'docker') {
64
- type = 'node';
65
- await cmd(`docker build -t ${app.id} .`, {
66
- enableOutput: true
67
- });
68
- }
69
- else {
70
- throw new Error(`No build pipeline for runtime=${runtime.name}`);
58
+ const buildpack = get_buildpack(plan.buildpack);
59
+ if (!buildpack) {
60
+ throw new Error(`No buildpack registered for plan=${plan.buildpack}`);
71
61
  }
62
+ await buildpack.build({ workdir, config, app, env_vars }, plan);
63
+ const type = plan.type;
72
64
  // Upload to Faable registry
73
65
  const { upload_tagname } = await upload_tag({ app, api });
74
66
  // Capture the commit/ref/actor so the deployment records which commit it
@@ -45,6 +45,17 @@ class Configuration {
45
45
  get buildCommand() {
46
46
  return this.config.buildCommand;
47
47
  }
48
+ /**
49
+ * faable.json subset consumed by the deploy buildpacks. Buildpacks receive
50
+ * this via their context and never touch the singleton directly.
51
+ */
52
+ deployConfig() {
53
+ return {
54
+ startCommand: this.config.startCommand,
55
+ buildCommand: this.config.buildCommand,
56
+ buildpack: this.config.buildpack,
57
+ };
58
+ }
48
59
  get app_slug() {
49
60
  return this.config.app_slug;
50
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",
@@ -37,6 +37,7 @@
37
37
  "promisify-child-process": "^4.1.2",
38
38
  "prompts": "^2.4.2",
39
39
  "ramda": "^0.32.0",
40
+ "smol-toml": "^1.7.0",
40
41
  "tslib": "^2.8.1",
41
42
  "yaml": "^2.8.2",
42
43
  "yargs": "^18.0.0"
@@ -1,52 +0,0 @@
1
- import { log } from '../../../log.js';
2
- import { cmd } from '../../../lib/cmd.js';
3
- import fs from 'fs-extra';
4
- import Handlebars from 'handlebars';
5
- import * as path from 'path';
6
- import { fileURLToPath } from 'url';
7
- import { Configuration } from '../../../lib/Configuration.js';
8
-
9
- const __filename$1 = fileURLToPath(import.meta.url);
10
- const __dirname$1 = path.dirname(__filename$1);
11
- const templates_dir = path.join(__dirname$1, "templates");
12
- const dockerfile = fs.readFileSync(`${templates_dir}/Dockerfile`).toString();
13
- const entrypoint = fs
14
- .readFileSync(`${templates_dir}/entrypoint.sh`)
15
- .toString("utf-8");
16
- Handlebars.registerHelper("escape", function (variable) {
17
- //const escaped_quotes = variable.replace(/(['"])/g, "\\$1");
18
- const escaped_lines = variable
19
- .replace(/(['`\\])/g, "\\$1")
20
- .replace(/([$])/g, "\\$1");
21
- return escaped_lines.split("\n").join("\\n");
22
- //return escaped_lines.split("\n").join("\\n");
23
- });
24
- // Docker template file
25
- const docker_template = Handlebars.compile(dockerfile);
26
- const entrypoint_template = Handlebars.compile(entrypoint);
27
- const build_docker = async (props) => {
28
- const { app, workdir, template_context } = props;
29
- const entrypoint_custom = entrypoint_template(template_context);
30
- // Precedence: explicit faable.json startCommand > framework-detected command
31
- // (e.g. serving a static SPA) > default `npm run start`.
32
- const start_command = Configuration.instance().configuredStartCommand ??
33
- props.start_command ??
34
- "npm run start";
35
- log.info(`⚙️ Start command: ${start_command}`);
36
- // NOTE: use slim to build projects
37
- const linux_distro = "slim";
38
- const from = [template_context.from, linux_distro].filter((e) => e).join("-");
39
- log.info(`Using docker image ${from}`);
40
- // run template
41
- const dockerfile = docker_template({
42
- from,
43
- entry_script: entrypoint_custom,
44
- start_command,
45
- });
46
- log.info(`📦 Packaging inside a docker image`);
47
- // Build options
48
- const timeout = 10 * 60 * 1000; // 10 minute timeout
49
- await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
50
- };
51
-
52
- export { build_docker };
@@ -1,26 +0,0 @@
1
- import { log } from '../../../log.js';
2
- import { cmd } from '../../../lib/cmd.js';
3
- import { Configuration } from '../../../lib/Configuration.js';
4
-
5
- const build_project = async (args) => {
6
- const build_script = args.build_script;
7
- const build_command = build_script
8
- ? `npm run ${build_script}`
9
- : Configuration.instance().buildCommand;
10
- if (build_command) {
11
- const cwd = args.cwd || process.cwd();
12
- log.info(`⚙️ Building project [${build_command}]...`);
13
- const timeout = 1000 * 60 * 30; // 30 minute timeout
14
- await cmd(build_command, {
15
- timeout,
16
- cwd,
17
- enableOutput: true,
18
- ...(args?.env ? { env: args?.env } : {}),
19
- });
20
- }
21
- else {
22
- log.info(`⚡️ No build step`);
23
- }
24
- };
25
-
26
- export { build_project };
@@ -1,44 +0,0 @@
1
- import { build_docker } from './build_docker.js';
2
- import { analyze_package } from './analyze_package.js';
3
- import { build_project } from './build_project.js';
4
- import { ensure_dependencies } from './ensure_dependencies.js';
5
- import { inject_serve } from './inject_serve.js';
6
- import * as R from 'ramda';
7
- import { log } from '../../../log.js';
8
-
9
- const build_node = async (app, options) => {
10
- // log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
11
- const { workdir, runtime, env_vars = [] } = options;
12
- if (!runtime.version) {
13
- throw new Error("Runtime version not specified for node");
14
- }
15
- // Analyze package.json to check if build is needed
16
- const { build_script, type, start_command, inject_serve: needs_serve } = await analyze_package({
17
- workdir,
18
- });
19
- // Environment variables
20
- const env = R.fromPairs(env_vars.map((e) => [e.name, e.value]));
21
- log.info(`Building with env variables ${Object.keys(env).join(",")}`);
22
- // The workflow no longer runs `npm ci` — install here when needed, before
23
- // the build and before `COPY . .` packages node_modules into the image.
24
- await ensure_dependencies(workdir);
25
- // Do build
26
- await build_project({ build_script, env, cwd: workdir });
27
- // Frameworks without a bundled static server (CRA/Vue/Angular) need `serve`
28
- // installed into node_modules before packaging, so it ships in the image.
29
- if (needs_serve) {
30
- await inject_serve(workdir);
31
- }
32
- // Bundle project inside a docker image
33
- await build_docker({
34
- app,
35
- workdir,
36
- start_command,
37
- template_context: {
38
- from: `node:${runtime.version}`,
39
- },
40
- });
41
- return { type };
42
- };
43
-
44
- export { build_node };
@@ -1,18 +0,0 @@
1
- FROM {{from}}
2
- LABEL com.faable.cloud="FaableCloud"
3
- LABEL description="Faablecloud automatic deployment"
4
-
5
- WORKDIR /faable/app
6
-
7
- # Environment variables for runtime
8
- ENV PORT=80
9
- ENV NODE_ENV=production
10
- ENV START_COMMAND="{{start_command}}"
11
-
12
- # Copy Usercode
13
- COPY . .
14
-
15
- # Entrypoint stript
16
- RUN echo '{{{escape entry_script}}}' >> entrypoint.sh
17
-
18
- CMD ["/bin/sh", "./entrypoint.sh"]
@@ -1,8 +0,0 @@
1
- #!/bin/sh
2
-
3
- NODE_VERSION=$(node --version)
4
- NPM_VERSION=$(npm --version)
5
- YARN_VERSION=$(yarn --version)
6
-
7
- echo "Faable Cloud · [node $NODE_VERSION] [npm $NPM_VERSION] [yarn $YARN_VERSION]"
8
- eval $START_COMMAND
@@ -1,42 +0,0 @@
1
- import { log } from '../../../log.js';
2
- import { cmd } from '../../../lib/cmd.js';
3
- import fs from 'fs-extra';
4
- import Handlebars from 'handlebars';
5
- import * as path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename$1 = fileURLToPath(import.meta.url);
9
- const __dirname$1 = path.dirname(__filename$1);
10
- const templates_dir = path.join(__dirname$1, "templates");
11
- const dockerfile = fs.readFileSync(`${templates_dir}/Dockerfile`).toString();
12
- const entrypoint = fs
13
- .readFileSync(`${templates_dir}/entrypoint.sh`)
14
- .toString("utf-8");
15
- Handlebars.registerHelper("escape", function (variable) {
16
- const escaped_lines = variable
17
- .replace(/(['`\\])/g, "\\$1")
18
- .replace(/([$])/g, "\\$1");
19
- return escaped_lines.split("\n").join("\\n");
20
- });
21
- const docker_template = Handlebars.compile(dockerfile);
22
- const entrypoint_template = Handlebars.compile(entrypoint);
23
- const build_docker = async (props) => {
24
- const { app, workdir, install_command, start_command, template_context } = props;
25
- const entrypoint_custom = entrypoint_template({});
26
- log.info(`⚙️ Start command: ${start_command}`);
27
- // NOTE: use slim to build projects
28
- const linux_distro = "slim";
29
- const from = [template_context.from, linux_distro].filter((e) => e).join("-");
30
- log.info(`Using docker image ${from}`);
31
- const dockerfile = docker_template({
32
- from,
33
- entry_script: entrypoint_custom,
34
- install_command,
35
- start_command,
36
- });
37
- log.info(`📦 Packaging inside a docker image`);
38
- const timeout = 10 * 60 * 1000; // 10 minute timeout
39
- await cmd(`docker build --platform linux/amd64 -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
40
- };
41
-
42
- export { build_docker };
@@ -1,25 +0,0 @@
1
- import { analyze_python } from './analyze_python.js';
2
- import { build_docker } from './build_docker.js';
3
-
4
- const build_python = async (app, options) => {
5
- const { workdir, runtime } = options;
6
- if (!runtime.version) {
7
- throw new Error("Runtime version not specified for python");
8
- }
9
- // Resolve how to install deps and start the app. Unlike node, there is no
10
- // separate local build step: dependency install happens inside the Docker
11
- // build (where network is available), so we go straight to packaging.
12
- const { install_command, start_command } = await analyze_python({ workdir });
13
- await build_docker({
14
- app,
15
- workdir,
16
- install_command,
17
- start_command,
18
- template_context: {
19
- from: `python:${runtime.version}`,
20
- },
21
- });
22
- return { type: "python" };
23
- };
24
-
25
- export { build_python };
@@ -1,7 +0,0 @@
1
- #!/bin/sh
2
-
3
- PYTHON_VERSION=$(python --version 2>&1)
4
- PIP_VERSION=$(pip --version 2>&1)
5
-
6
- echo "Faable Cloud · [$PYTHON_VERSION] [$PIP_VERSION]"
7
- eval $START_COMMAND
@@ -1,25 +0,0 @@
1
- import * as R from 'ramda';
2
- import { has_any_of_files } from './helpers/has_any_of_files.js';
3
- import { strategy_docker } from './strategies/docker.js';
4
- import { strategy_nodejs } from './strategies/nodejs.js';
5
- import { strategy_python } from './strategies/python.js';
6
-
7
- const runtime_detection = async (workdir) => {
8
- const has = R.curry(has_any_of_files);
9
- // Order matters: node wins for full-stack apps that ship both a package.json
10
- // and Python deps; Dockerfile is the explicit escape hatch evaluated last.
11
- const strategy = R.cond([
12
- [has(['package.json']), R.always(strategy_nodejs)],
13
- [
14
- has(['requirements.txt', 'pyproject.toml', 'Pipfile']),
15
- R.always(strategy_python)
16
- ],
17
- [has(['Dockerfile']), R.always(strategy_docker)]
18
- ])(workdir);
19
- if (!strategy) {
20
- throw new Error('Cannot detect project type');
21
- }
22
- return strategy(workdir);
23
- };
24
-
25
- export { runtime_detection };
@@ -1,9 +0,0 @@
1
- const strategy_docker = async (_workdir) => {
2
- return {
3
- runtime: {
4
- name: "docker",
5
- },
6
- };
7
- };
8
-
9
- export { strategy_docker };