@faable/faable 1.15.1 → 1.15.3

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,8 +1,8 @@
1
- import fs from 'fs-extra';
2
1
  import path__default from 'path';
3
2
  import { cmd } from '../../../../lib/cmd.js';
4
3
  import { log } from '../../../../log.js';
5
4
  import { has_any_of_files } from '../shared/has_any_of_files.js';
5
+ import { read_json_file } from '../shared/read_text_file.js';
6
6
 
7
7
  // A package.json with a next dependency beside the Dockerfile means this is a
8
8
  // Next.js app shipped with a custom image: emit type "next" so the backend
@@ -10,7 +10,7 @@ import { has_any_of_files } from '../shared/has_any_of_files.js';
10
10
  // --buildpack override, since the node buildpack claims package.json first.
11
11
  const detect_next = (workdir) => {
12
12
  try {
13
- const pkg = fs.readJSONSync(path__default.join(workdir, "package.json"));
13
+ const pkg = read_json_file(path__default.join(workdir, "package.json"));
14
14
  return Boolean(pkg?.dependencies?.next || pkg?.devDependencies?.next);
15
15
  }
16
16
  catch {
@@ -1,13 +1,13 @@
1
- import fs from 'fs-extra';
2
1
  import path__default from 'path';
3
2
  import { log } from '../../../../log.js';
3
+ import { read_json_file } from '../shared/read_text_file.js';
4
4
  import { detect_framework } from './frameworks.js';
5
5
 
6
6
  const analyze_package = async (params) => {
7
7
  const workdir = params.workdir;
8
8
  const package_file = path__default.join(path__default.resolve(workdir), "package.json");
9
9
  log.info(`Loading config from package.json`);
10
- const pkg = await fs.readJSON(package_file);
10
+ const pkg = read_json_file(package_file);
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
@@ -1,7 +1,7 @@
1
- import fs from 'fs-extra';
2
1
  import path__default from 'path';
3
2
  import * as R from 'ramda';
4
3
  import { log } from '../../../../log.js';
4
+ import { read_json_file } from '../shared/read_text_file.js';
5
5
 
6
6
  const has_dep = (pkg, name) => Boolean(R.view(R.lensPath(["dependencies", name]), pkg) ||
7
7
  R.view(R.lensPath(["devDependencies", name]), pkg));
@@ -13,7 +13,7 @@ const has_dep = (pkg, name) => Boolean(R.view(R.lensPath(["dependencies", name])
13
13
  const resolve_angular_output = (workdir) => {
14
14
  const fallback = "dist";
15
15
  try {
16
- const angular_json = fs.readJSONSync(path__default.join(workdir, "angular.json"));
16
+ const angular_json = read_json_file(path__default.join(workdir, "angular.json"));
17
17
  const projects = angular_json?.projects ?? {};
18
18
  const project_name = angular_json?.defaultProject ?? Object.keys(projects)[0];
19
19
  const build = projects?.[project_name]?.architect?.build;
@@ -1,7 +1,7 @@
1
- import fs from 'fs-extra';
2
1
  import path__default from 'path';
3
2
  import { cmd } from '../../../../lib/cmd.js';
4
3
  import { log } from '../../../../log.js';
4
+ import { read_json_file } from '../shared/read_text_file.js';
5
5
 
6
6
  const getCurrentNodeVersion = async () => {
7
7
  const out = await cmd(`node --version`);
@@ -16,7 +16,7 @@ const getCurrentNodeVersion = async () => {
16
16
  */
17
17
  const resolve_node_version = async (workdir) => {
18
18
  const packageJSONFile = path__default.join(workdir, "package.json");
19
- const { name, engines } = fs.readJSONSync(packageJSONFile);
19
+ const { name, engines } = read_json_file(packageJSONFile);
20
20
  if (!name) {
21
21
  throw new Error("Missing name in package.json");
22
22
  }
@@ -3,6 +3,7 @@ import path__default from 'path';
3
3
  import { log } from '../../../../log.js';
4
4
  import { render_dockerfile, build_image } from '../shared/docker_image.js';
5
5
  import { has_any_of_files } from '../shared/has_any_of_files.js';
6
+ import { read_text_file } from '../shared/read_text_file.js';
6
7
  import { cerebrium_provider } from './providers/cerebrium.js';
7
8
  import { pipfile_provider } from './providers/pipfile.js';
8
9
  import { pyproject_provider } from './providers/pyproject.js';
@@ -89,7 +90,7 @@ const python_buildpack = {
89
90
  // Framework detection blob = the entry files themselves: an
90
91
  // `from fastapi import FastAPI` line carries the token detection needs.
91
92
  const deps = entries
92
- .map((f) => fs.readFileSync(path__default.join(ctx.workdir, f)).toString())
93
+ .map((f) => read_text_file(path__default.join(ctx.workdir, f)))
93
94
  .join("\n")
94
95
  .toLowerCase();
95
96
  const { start_command, server } = resolve_start(ctx.workdir, deps, ctx.config);
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
+ import { read_text_file } from '../shared/read_text_file.js';
3
4
 
4
5
  /**
5
6
  * Read the `web:` process command from a Procfile, if present. Returns null when
@@ -10,7 +11,7 @@ const parse_procfile = (workdir) => {
10
11
  const procfile = path__default.join(workdir, "Procfile");
11
12
  if (!fs.existsSync(procfile))
12
13
  return null;
13
- const content = fs.readFileSync(procfile).toString();
14
+ const content = read_text_file(procfile);
14
15
  for (const line of content.split("\n")) {
15
16
  const match = line.match(/^\s*web\s*:\s*(.+?)\s*$/);
16
17
  if (match?.[1])
@@ -1,5 +1,5 @@
1
- import fs from 'fs-extra';
2
1
  import path__default from 'path';
2
+ import { read_text_file } from '../../shared/read_text_file.js';
3
3
  import { parse_cerebrium_toml } from './parse_cerebrium_toml.js';
4
4
 
5
5
  /**
@@ -12,7 +12,7 @@ const cerebrium_provider = {
12
12
  name: "cerebrium",
13
13
  files: ["cerebrium.toml"],
14
14
  resolve(workdir) {
15
- const manifest = parse_cerebrium_toml(fs.readFileSync(path__default.join(workdir, "cerebrium.toml")).toString());
15
+ const manifest = parse_cerebrium_toml(read_text_file(path__default.join(workdir, "cerebrium.toml")));
16
16
  let install_command;
17
17
  let install_files;
18
18
  if (manifest.pip_requirements_file) {
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
+ import { read_text_file } from '../shared/read_text_file.js';
3
4
 
4
5
  const DEFAULT_VERSION = "3.11.3";
5
6
  /**
@@ -13,7 +14,7 @@ const DEFAULT_VERSION = "3.11.3";
13
14
  const resolve_python_version = (workdir, provider_hint) => {
14
15
  const runtime_config = path__default.join(workdir, "runtime.txt");
15
16
  if (fs.existsSync(runtime_config)) {
16
- const runtime_data = fs.readFileSync(runtime_config).toString().trim();
17
+ const runtime_data = read_text_file(runtime_config).trim();
17
18
  if (!runtime_data.startsWith("python-")) {
18
19
  throw new Error("runtime.txt must have runtime format with python-<version>");
19
20
  }
@@ -21,7 +22,7 @@ const resolve_python_version = (workdir, provider_hint) => {
21
22
  }
22
23
  const python_version_file = path__default.join(workdir, ".python-version");
23
24
  if (fs.existsSync(python_version_file)) {
24
- const version = fs.readFileSync(python_version_file).toString().trim();
25
+ const version = read_text_file(python_version_file).trim();
25
26
  if (version)
26
27
  return version;
27
28
  }
@@ -29,7 +30,7 @@ const resolve_python_version = (workdir, provider_hint) => {
29
30
  return provider_hint;
30
31
  const pyproject = path__default.join(workdir, "pyproject.toml");
31
32
  if (fs.existsSync(pyproject)) {
32
- const toml = fs.readFileSync(pyproject).toString();
33
+ const toml = read_text_file(pyproject);
33
34
  const match = toml.match(/requires-python\s*=\s*["'][^0-9]*([0-9]+\.[0-9]+(?:\.[0-9]+)?)/);
34
35
  if (match?.[1])
35
36
  return match[1];
@@ -1,6 +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 { read_text_file } from '../shared/read_text_file.js';
4
5
  import { parse_procfile } from './parse_procfile.js';
5
6
 
6
7
  /** Combine all classic dependency manifests into one lowercased blob. */
@@ -9,7 +10,7 @@ const read_dependencies_text = (workdir) => {
9
10
  return files
10
11
  .map((f) => path__default.join(workdir, f))
11
12
  .filter((p) => fs.existsSync(p))
12
- .map((p) => fs.readFileSync(p).toString())
13
+ .map((p) => read_text_file(p))
13
14
  .join("\n")
14
15
  .toLowerCase();
15
16
  };
@@ -59,7 +60,7 @@ const find_app_module = (workdir, pattern) => {
59
60
  const module = rel.replace(/\.py$/, "").split(path__default.sep).join(".");
60
61
  if (!first_existing)
61
62
  first_existing = module;
62
- if (pattern.test(fs.readFileSync(abs).toString()))
63
+ if (pattern.test(read_text_file(abs)))
63
64
  return module;
64
65
  }
65
66
  return first_existing;
@@ -0,0 +1,31 @@
1
+ import fs from 'fs-extra';
2
+
3
+ /**
4
+ * Decode a manifest/source buffer to a string, honoring a leading BOM.
5
+ *
6
+ * Editors and shells on Windows (PowerShell `>` redirection, Notepad "Save As")
7
+ * frequently write files as UTF‑16 with a BOM. Reading those as UTF‑8 yields
8
+ * mojibake with interleaved NUL bytes (`f\x00a\x00s\x00t…`), which silently
9
+ * breaks token detection — e.g. `\bfastapi\b` never matches a UTF‑16
10
+ * `requirements.txt`, so the Python buildpack skips framework detection and
11
+ * fails with "Could not detect how to start this Python app". Detect the BOM and
12
+ * decode accordingly, stripping the BOM from the result.
13
+ */
14
+ const decode_buffer = (buf) => {
15
+ if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
16
+ return new TextDecoder("utf-16le").decode(buf.subarray(2));
17
+ }
18
+ if (buf.length >= 2 && buf[0] === 0xfe && buf[1] === 0xff) {
19
+ return new TextDecoder("utf-16be").decode(buf.subarray(2));
20
+ }
21
+ if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
22
+ return buf.subarray(3).toString("utf8");
23
+ }
24
+ return buf.toString("utf8");
25
+ };
26
+ /** Read a file to a string, decoding UTF‑16/UTF‑8 BOMs (see {@link decode_buffer}). */
27
+ const read_text_file = (file) => decode_buffer(fs.readFileSync(file));
28
+ /** Read and parse a JSON file, tolerating a UTF‑16/UTF‑8 BOM. */
29
+ const read_json_file = (file) => JSON.parse(read_text_file(file));
30
+
31
+ export { decode_buffer, read_json_file, read_text_file };
@@ -8,7 +8,7 @@ import { check_environment } from './check_environment.js';
8
8
  import { git_context } from './git_context.js';
9
9
  import { resolve_app_id } from './resolve_app_id.js';
10
10
  import { secrets } from './secrets/index.js';
11
- import { start_log_sync, mark_build_failure, upload_logs } from './upload_logs.js';
11
+ import { mark_build_failure, start_log_sync, upload_logs } from './upload_logs.js';
12
12
  import { upload_tag } from './upload_tag.js';
13
13
 
14
14
  const deploy = {
@@ -41,20 +41,43 @@ const deploy = {
41
41
  const workdir = args.workdir || process.cwd();
42
42
  const ctx = await requireApi();
43
43
  const { api } = ctx;
44
- // Resolve the buildpack plan (detection or forced override). All the
45
- // build thinking happens here; build() below just executes the plan.
46
44
  const config = Configuration.instance().deployConfig();
47
- const plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
48
45
  const app_id = await resolve_app_id(args.app_id, ctx.appId, api, workdir);
49
46
  const app = await api.getApp(app_id);
50
47
  // Capture the commit/ref/actor so the deployment records which commit
51
48
  // it came from and who pushed it (env in CI, git fallback locally).
52
49
  const git = await git_context({ workdir });
50
+ // Resolve the buildpack plan (detection or forced override). All the build
51
+ // thinking happens here; build() below just executes the plan. Detection can
52
+ // fail (e.g. an app whose start command can't be inferred) — and it runs
53
+ // before the create-first deployment exists, so without this a detection
54
+ // failure would leave NO deployment row: no deploy-failed email, nothing in
55
+ // the dashboard, just a red X in the CI logs the user may never see. Record
56
+ // it as a failed deployment instead.
57
+ let plan;
58
+ try {
59
+ plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
60
+ }
61
+ catch (error) {
62
+ // Log the reason into the build buffer first (so it rides along in the
63
+ // attached logs), then record a typeless BUILD_ERROR row — typeless so it
64
+ // can't rewrite the app's runtime_strategy. Best-effort: a create that
65
+ // itself fails (e.g. the free-plan quota gate) must not mask the original
66
+ // detection error.
67
+ log.error(error.message);
68
+ const failed = await api
69
+ .createDeployment({ app_id: app.id, ...git })
70
+ .catch(() => null);
71
+ if (failed) {
72
+ await mark_build_failure(api, { deployment_id: failed.id, app });
73
+ }
74
+ throw error;
75
+ }
53
76
  // Create-first: register the deployment BEFORE building. Gate rejections
54
77
  // (free-plan quota 429, disabled app 409) surface here, at second 0 —
55
78
  // before any build minutes are spent. The row is born QUEUED; the CLI
56
79
  // owns it until the built image lands (or the build fails). `type` rides
57
- // on the create as before — the buildpack plan is already resolved.
80
+ // on the create — the buildpack plan is already resolved.
58
81
  const deployment = await api.createDeployment({
59
82
  app_id: app.id,
60
83
  type: plan.type,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "1.15.1",
3
+ "version": "1.15.3",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",