@faable/faable 1.5.27 → 1.5.28

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.
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
3
  import { log } from '../../../log.js';
4
- import * as R from 'ramda';
4
+ import { detect_framework } from './frameworks.js';
5
5
 
6
6
  const analyze_package = async (params) => {
7
7
  const workdir = params.workdir;
@@ -11,23 +11,24 @@ const analyze_package = async (params) => {
11
11
  // Check if build is required to run
12
12
  const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
13
13
  ? process.env.FAABLE_NPM_BUILD_SCRIPT
14
- : pkg?.scripts["build"]
14
+ : pkg?.scripts?.["build"]
15
15
  ? "build"
16
16
  : null;
17
17
  if (!build_script) {
18
18
  log.info(`No build script on package.json`);
19
19
  }
20
- let type = "node";
21
- // Detect nextjs deployment type
22
- const next_dep = R.lensPath(["dependencies", "next"]);
23
- const next_devdep = R.lensPath(["devDependencies", "next"]);
24
- if (R.view(next_dep, pkg) || R.view(next_devdep, pkg)) {
25
- type = "next";
26
- }
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
+ });
27
26
  log.info(`⚡️ Detected deployment type=${type}`);
28
27
  return {
29
28
  build_script,
30
29
  type,
30
+ start_command,
31
+ inject_serve,
31
32
  };
32
33
  };
33
34
 
@@ -27,7 +27,11 @@ const entrypoint_template = Handlebars.compile(entrypoint);
27
27
  const build_docker = async (props) => {
28
28
  const { app, workdir, template_context } = props;
29
29
  const entrypoint_custom = entrypoint_template(template_context);
30
- const start_command = Configuration.instance().startCommand;
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";
31
35
  log.info(`⚙️ Start command: ${start_command}`);
32
36
  // NOTE: use slim to build projects
33
37
  const linux_distro = "slim";
@@ -0,0 +1,108 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+ import * as R from 'ramda';
4
+ import { log } from '../../../log.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 = fs.readJSONSync(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,6 +1,7 @@
1
1
  import { build_docker } from './build_docker.js';
2
2
  import { analyze_package } from './analyze_package.js';
3
3
  import { build_project } from './build_project.js';
4
+ import { inject_serve } from './inject_serve.js';
4
5
  import * as R from 'ramda';
5
6
  import { log } from '../../../log.js';
6
7
 
@@ -11,16 +12,24 @@ const build_node = async (app, options) => {
11
12
  throw new Error("Runtime version not specified for node");
12
13
  }
13
14
  // Analyze package.json to check if build is needed
14
- const { build_script, type } = await analyze_package({ workdir });
15
+ const { build_script, type, start_command, inject_serve: needs_serve } = await analyze_package({
16
+ workdir,
17
+ });
15
18
  // Environment variables
16
19
  const env = R.fromPairs(env_vars.map((e) => [e.name, e.value]));
17
20
  log.info(`Building with env variables ${Object.keys(env).join(",")}`);
18
21
  // Do build
19
22
  await build_project({ build_script, env });
23
+ // Frameworks without a bundled static server (CRA/Vue/Angular) need `serve`
24
+ // installed into node_modules before packaging, so it ships in the image.
25
+ if (needs_serve) {
26
+ await inject_serve(workdir);
27
+ }
20
28
  // Bundle project inside a docker image
21
29
  await build_docker({
22
30
  app,
23
31
  workdir,
32
+ start_command,
24
33
  template_context: {
25
34
  from: `node:${runtime.version}`,
26
35
  },
@@ -0,0 +1,20 @@
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 };
@@ -38,6 +38,10 @@ class Configuration {
38
38
  get startCommand() {
39
39
  return this.config.startCommand || "npm run start";
40
40
  }
41
+ /** Start command explicitly set in faable.json, or undefined when relying on the default. */
42
+ get configuredStartCommand() {
43
+ return this.config.startCommand;
44
+ }
41
45
  get buildCommand() {
42
46
  return this.config.buildCommand;
43
47
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.5.27",
3
+ "version": "1.5.28",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",