@faable/faable 1.4.10 → 1.4.11

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.
@@ -0,0 +1,10 @@
1
+ import path__default from 'path';
2
+ import fs from 'fs-extra';
3
+
4
+ const builder = async (ctx) => {
5
+ const { app, workdir } = ctx;
6
+ const f = path__default.join(path__default.resolve(workdir), "Dockerfile");
7
+ return { dockerfile: (await fs.readFile(f)).toString() };
8
+ };
9
+
10
+ export { builder, builder as default };
@@ -0,0 +1,17 @@
1
+ const builders = {
2
+ node: () => import('./node/index.js'),
3
+ python: () => import('./python/index.js'),
4
+ docker: () => import('./docker/index.js'),
5
+ };
6
+ const import_builder = async (runtime_name) => {
7
+ try {
8
+ let builder_module = await builders[runtime_name]();
9
+ return builder_module.default;
10
+ }
11
+ catch (e) {
12
+ console.log(e);
13
+ throw new Error(`Builder import error for ${runtime_name}`);
14
+ }
15
+ };
16
+
17
+ export { import_builder };
@@ -0,0 +1,25 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+
4
+ const analyze_package = async (ctx) => {
5
+ const { log, workdir } = ctx;
6
+ const node_modules_dir = path__default.join(path__default.resolve(workdir), "node_modules");
7
+ const installedModules = await fs.pathExists(node_modules_dir);
8
+ if (!installedModules) {
9
+ throw new Error("node_modules not found, please install packages first");
10
+ }
11
+ const package_file = path__default.join(path__default.resolve(workdir), "package.json");
12
+ const pkg = await fs.readJSON(package_file);
13
+ // Detect deployment type
14
+ let type = "node";
15
+ if (pkg.dependencies["next"]) {
16
+ type = "next";
17
+ log.info(`✅ Next.js ⚡️ project detected`);
18
+ }
19
+ return {
20
+ pkg,
21
+ type,
22
+ };
23
+ };
24
+
25
+ export { analyze_package };
@@ -0,0 +1,27 @@
1
+ import { cmd } from '../../lib/cmd.js';
2
+
3
+ const build_project = async (ctx, nodeCtx) => {
4
+ const { log, config, workdir } = ctx;
5
+ // Check if exists any type of builc command
6
+ let build_command = config.getConfigProperty("buildCommand", process.env.FAABLE_NPM_BUILD_COMMAND);
7
+ const build_script_name = config.getConfigProperty("buildScript", "build");
8
+ const build_script = nodeCtx.pkg.scripts[build_script_name];
9
+ // No build command but build script
10
+ if (!build_command && build_script) {
11
+ build_command = `npm run ${build_script_name}`;
12
+ }
13
+ if (build_command) {
14
+ log.info(`✅ Build command detected. Running "${build_command}"`);
15
+ const timeout = 1000 * 60 * 100; // 100 minute timeout
16
+ await cmd(build_command, {
17
+ timeout,
18
+ cwd: workdir,
19
+ enableOutput: true,
20
+ });
21
+ }
22
+ else {
23
+ log.info(`No build script in package.json`);
24
+ }
25
+ };
26
+
27
+ export { build_project };
@@ -0,0 +1,31 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+ import { cmd } from '../../lib/cmd.js';
4
+
5
+ const engine_version = async (ctx) => {
6
+ const { workdir, log } = ctx;
7
+ const packageJSONFile = path__default.join(workdir, "package.json");
8
+ // Check we have a valid name
9
+ const { name, engines } = fs.readJSONSync(packageJSONFile);
10
+ if (!name) {
11
+ throw new Error("Missing name in package.json");
12
+ }
13
+ // Use engines.node if found
14
+ let version = "18.19.0";
15
+ if (engines?.node) {
16
+ try {
17
+ const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
18
+ const out = await cmd(check_cmd);
19
+ version = out.stdout.toString().trim();
20
+ log.info(`✅ Engine "${engines.node}" from package.json resolved to node@${version}`);
21
+ }
22
+ catch (e) {
23
+ throw new Error(`Node version is not valid (${engines.node})`);
24
+ }
25
+ }
26
+ return {
27
+ version,
28
+ };
29
+ };
30
+
31
+ export { engine_version };
@@ -0,0 +1,26 @@
1
+ import { prepare_dockerfile } from './prepare_dockerfile.js';
2
+ import { analyze_package } from './analyze_package.js';
3
+ import { build_project } from './build_project.js';
4
+ import { engine_version } from './engine_version.js';
5
+
6
+ const builder = async (ctx) => {
7
+ // log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
8
+ const { version } = await engine_version(ctx);
9
+ // Analyze package.json to check if build is needed
10
+ const nodeCtx = await analyze_package(ctx);
11
+ await build_project(ctx, nodeCtx);
12
+ // Bundle project inside a docker image
13
+ const { dockerfile } = await prepare_dockerfile(ctx, {
14
+ template_context: {
15
+ from: `node:${version}`,
16
+ },
17
+ });
18
+ return {
19
+ dockerfile,
20
+ params: {
21
+ type: nodeCtx.type,
22
+ },
23
+ };
24
+ };
25
+
26
+ export { builder, builder as default };
@@ -1,10 +1,7 @@
1
- import { log } from '../../../log.js';
2
- import { cmd } from '../../../lib/cmd.js';
3
1
  import fs from 'fs-extra';
4
2
  import Handlebars from 'handlebars';
5
3
  import * as path from 'path';
6
4
  import { fileURLToPath } from 'url';
7
- import { Configuration } from '../../../lib/Configuration.js';
8
5
 
9
6
  const __filename = fileURLToPath(import.meta.url);
10
7
  const __dirname = path.dirname(__filename);
@@ -24,20 +21,18 @@ Handlebars.registerHelper("escape", function (variable) {
24
21
  // Docker template file
25
22
  const docker_template = Handlebars.compile(dockerfile);
26
23
  const entrypoint_template = Handlebars.compile(entrypoint);
27
- const bundle_docker = async (props) => {
28
- const { app, workdir, template_context } = props;
24
+ const prepare_dockerfile = async (ctx, params) => {
25
+ const { app, workdir, log, config } = ctx;
26
+ const { template_context } = params;
29
27
  const entrypoint_custom = entrypoint_template(template_context);
30
- const start_command = Configuration.instance().startCommand;
31
- log.info(`⚙️ Start command: ${start_command}`);
28
+ const start_command = config.getConfigProperty("startCommand", "npm run start");
29
+ log.info(`✅ Start command set to "${start_command}"`);
32
30
  const dockerfile = docker_template({
33
31
  from: template_context.from,
34
32
  entry_script: entrypoint_custom,
35
33
  start_command,
36
34
  });
37
- log.info(`📦 Packaging inside a docker image`);
38
- // Build options
39
- const timeout = 10 * 60 * 1000; // 10 minute timeout
40
- await cmd(`docker build -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
35
+ return { dockerfile };
41
36
  };
42
37
 
43
- export { bundle_docker };
38
+ export { prepare_dockerfile };
@@ -0,0 +1,5 @@
1
+ const builder = async (ctx) => {
2
+ throw new Error(`Python builder is not implemented`);
3
+ };
4
+
5
+ export { builder, builder as default };
@@ -5,7 +5,6 @@ const check_environment = async () => {
5
5
  await cmd("docker ps");
6
6
  }
7
7
  catch (error) {
8
- console.log(error);
9
8
  throw new Error(`Docker is not running`);
10
9
  }
11
10
  };
@@ -2,13 +2,19 @@ import { log } from '../../log.js';
2
2
  import { upload_tag } from './upload_tag.js';
3
3
  import { check_environment } from './check_environment.js';
4
4
  import { context } from '../../api/context.js';
5
- import { build_node } from './node-pipeline/index.js';
6
5
  import { runtime_detection } from './runtime-detect/runtime_detection.js';
6
+ import { import_builder } from '../../builder/index.js';
7
+ import { Configuration } from '../../lib/Configuration.js';
7
8
  import { cmd } from '../../lib/cmd.js';
8
9
 
9
10
  const deploy_command = async (args) => {
10
11
  const workdir = args.workdir || process.cwd();
12
+ const flags = {
13
+ upload: args.onlybuild ? false : true,
14
+ };
11
15
  const { api } = await context();
16
+ // Check if we can build docker images
17
+ await check_environment();
12
18
  // Resolve runtime
13
19
  const { app_name, runtime } = await runtime_detection(workdir);
14
20
  const name = args.app_slug || app_name;
@@ -17,31 +23,35 @@ const deploy_command = async (args) => {
17
23
  }
18
24
  // Get app from Faable API
19
25
  const app = await api.getBySlug(name);
20
- // Check if we can build docker images
21
- await check_environment();
22
- log.info(`🚀 Deploying ${app.name} (${app.id}) runtime=${runtime.name}-${runtime.version} `);
23
- let type;
24
- if (runtime.name == "node") {
25
- const node_result = await build_node(app, {
26
- workdir,
27
- runtime,
28
- });
29
- type = node_result.type;
30
- }
31
- else if (runtime.name == "docker") {
32
- type = "node";
33
- await cmd(`docker build -t ${app.id} .`, {
34
- enableOutput: true,
26
+ log.info(`🚀 Deploying ${app.name} (${app.id}) runtime=${runtime}`);
27
+ // Select builder
28
+ let builder = await import_builder(runtime);
29
+ const ctx = {
30
+ app,
31
+ workdir,
32
+ log: log.child({ runtime }),
33
+ config: Configuration.instance(),
34
+ };
35
+ // Do build
36
+ const { dockerfile, params } = await builder(ctx);
37
+ // Compile docker image
38
+ log.info(`📦 Packaging...`);
39
+ const timeout = 10 * 60 * 1000; // 10 minute timeout
40
+ await cmd(`docker build -t ${app.id} ${workdir} -f -<<EOF\n${dockerfile}\nEOF`, { timeout, enableOutput: true });
41
+ // Upload to Faable registry
42
+ if (flags.upload) {
43
+ const { upload_tagname } = await upload_tag({ app, api });
44
+ // Create a deployment for this image
45
+ await api.createDeployment({
46
+ app_id: app.id,
47
+ image: upload_tagname,
48
+ ...(params || {}),
35
49
  });
50
+ log.info(`🌍 Deployment created -> https://${app.url}`);
36
51
  }
37
52
  else {
38
- throw new Error(`No build pipeline for runtime=${runtime.name}`);
53
+ log.info(`❌ Upload canceled, remove --onlybuild otherwise`);
39
54
  }
40
- // Upload to Faable registry
41
- const { upload_tagname } = await upload_tag({ app, api });
42
- // Create a deployment for this image
43
- await api.createDeployment({ app_id: app.id, image: upload_tagname, type });
44
- log.info(`🌍 Deployment created -> https://${app.url}`);
45
55
  };
46
56
 
47
57
  export { deploy_command };
@@ -13,6 +13,11 @@ const deploy = {
13
13
  alias: "w",
14
14
  type: "string",
15
15
  description: "Working directory",
16
+ })
17
+ .option("onlybuild", {
18
+ type: "boolean",
19
+ default: false,
20
+ description: "Just build",
16
21
  })
17
22
  .showHelpOnFail(false);
18
23
  },
@@ -1,13 +1,14 @@
1
1
  import { strategy_nodejs } from './strategies/nodejs.js';
2
2
  import * as R from 'ramda';
3
3
  import { has_any_of_files } from './helpers/has_any_of_files.js';
4
+ import { strategy_python } from './strategies/python.js';
4
5
  import { strategy_docker } from './strategies/docker.js';
5
6
 
6
7
  const runtime_detection = async (workdir) => {
7
8
  const has = R.curry(has_any_of_files);
8
9
  const strategy = R.cond([
9
10
  [has(["package.json"]), R.always(strategy_nodejs)],
10
- // [has(["requirements.txt"]), R.always(strategy_python)],
11
+ [has(["requirements.txt"]), R.always(strategy_python)],
11
12
  [has(["Dockerfile"]), R.always(strategy_docker)],
12
13
  ])(workdir);
13
14
  if (!strategy) {
@@ -1,8 +1,6 @@
1
1
  const strategy_docker = async (workdir) => {
2
2
  return {
3
- runtime: {
4
- name: "docker",
5
- },
3
+ runtime: "docker",
6
4
  };
7
5
  };
8
6
 
@@ -1,7 +1,5 @@
1
1
  import fs from 'fs-extra';
2
2
  import path__default from 'path';
3
- import { cmd } from '../../../../lib/cmd.js';
4
- import { log } from '../../../../log.js';
5
3
 
6
4
  /**
7
5
  * Strategy to detect app name from package.json
@@ -12,29 +10,13 @@ import { log } from '../../../../log.js';
12
10
  const strategy_nodejs = async (workdir) => {
13
11
  const packageJSONFile = path__default.join(workdir, "package.json");
14
12
  // Check we have a valid name
15
- const { name, engines } = fs.readJSONSync(packageJSONFile);
13
+ const { name } = fs.readJSONSync(packageJSONFile);
16
14
  if (!name) {
17
15
  throw new Error("Missing name in package.json");
18
16
  }
19
- // Use engines.node if found
20
- let runtime_version = "18.19.0";
21
- if (engines?.node) {
22
- try {
23
- const check_cmd = `npm view node@"${engines.node}" version | tail -n 1 | cut -d "'" -f2`;
24
- const out = await cmd(check_cmd);
25
- runtime_version = out.stdout.toString();
26
- log.info(`Using node@${runtime_version} from engines in package.json (${engines.node})`);
27
- }
28
- catch (e) {
29
- throw new Error(`Node version is not valid (${engines.node})`);
30
- }
31
- }
32
17
  return {
33
18
  app_name: name,
34
- runtime: {
35
- name: "node",
36
- version: runtime_version,
37
- },
19
+ runtime: "node",
38
20
  };
39
21
  };
40
22
 
@@ -0,0 +1,19 @@
1
+ import fs from 'fs-extra';
2
+ import path__default from 'path';
3
+
4
+ const strategy_python = async (workdir) => {
5
+ const runtime_config = path__default.join(workdir, "runtime.txt");
6
+ // Select runtime based on config
7
+ if (fs.existsSync(runtime_config)) {
8
+ const runtime_data = fs.readFileSync(runtime_config).toString();
9
+ if (!runtime_data.startsWith("python-")) {
10
+ throw new Error("runtime.txt must have runtime format with python-<version>");
11
+ }
12
+ runtime_data.split("-")[1];
13
+ }
14
+ return {
15
+ runtime: "python",
16
+ };
17
+ };
18
+
19
+ export { strategy_python };
@@ -27,11 +27,8 @@ class Configuration {
27
27
  }
28
28
  return Configuration._instance;
29
29
  }
30
- get startCommand() {
31
- return this.config.startCommand || "npm run start";
32
- }
33
- get buildCommand() {
34
- return this.config.buildCommand;
30
+ getConfigProperty(name, defaultValue) {
31
+ return this.config[name] || defaultValue;
35
32
  }
36
33
  }
37
34
 
package/dist/log.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import pino from 'pino';
2
- import 'pino-pretty';
2
+ import pretty from 'pino-pretty';
3
3
 
4
- const log = pino({
5
- transport: {
6
- target: "pino-pretty",
7
- options: {
8
- colorize: true,
9
- },
10
- },
11
- });
4
+ const log = pino(pretty({
5
+ colorize: true,
6
+ messageFormat: "{timestamp}{if runtime}⚙️:{runtime} - {end}{msg}",
7
+ ignore: "runtime",
8
+ singleLine: true,
9
+ }));
12
10
 
13
11
  export { log };
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "fs-extra": "^11.1.0",
7
7
  "handlebars": "^4.7.7",
8
8
  "pino": "^8.11.0",
9
- "pino-pretty": "^9.4.0",
9
+ "pino-pretty": "^11.0.0",
10
10
  "promisify-child-process": "^4.1.1",
11
11
  "prompts": "^2.4.2",
12
12
  "ramda": "^0.29.0",
@@ -14,7 +14,7 @@
14
14
  "yaml": "^2.2.2",
15
15
  "yargs": "^17.6.2"
16
16
  },
17
- "version": "1.4.10",
17
+ "version": "1.4.11",
18
18
  "bin": {
19
19
  "faable": "bin/faable.js"
20
20
  },
@@ -1,31 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path__default from 'path';
3
- import { log } from '../../../log.js';
4
-
5
- const analyze_package = async (params) => {
6
- const workdir = params.workdir;
7
- const package_file = path__default.join(path__default.resolve(workdir), "package.json");
8
- log.info(`Loading config from package.json...`);
9
- const pkg = await fs.readJSON(package_file);
10
- // Check if build is required to run
11
- const build_script = process.env.FAABLE_NPM_BUILD_SCRIPT
12
- ? process.env.FAABLE_NPM_BUILD_SCRIPT
13
- : pkg?.scripts["build"]
14
- ? "build"
15
- : null;
16
- if (!build_script) {
17
- log.info(`No build script on package.json`);
18
- }
19
- // Detect deployment type
20
- let type = "node";
21
- if (pkg.dependencies["next"]) {
22
- type = "next";
23
- }
24
- log.info(`⚡️ Detected deployment type=${type}`);
25
- return {
26
- build_script,
27
- type,
28
- };
29
- };
30
-
31
- export { analyze_package };
@@ -1,25 +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
- ? `yarn 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 * 100; // 100 minute timeout
14
- await cmd(build_command, {
15
- timeout,
16
- cwd,
17
- enableOutput: true,
18
- });
19
- }
20
- else {
21
- log.info(`⚡️ No build step`);
22
- }
23
- };
24
-
25
- export { build_project };
@@ -1,25 +0,0 @@
1
- import { bundle_docker } from './bundle_docker.js';
2
- import { analyze_package } from './analyze_package.js';
3
- import { build_project } from './build_project.js';
4
-
5
- const build_node = async (app, options) => {
6
- // log.info(`🚀 Build Toolchain ${app.name} [${app.id}]`);
7
- const { workdir, runtime } = options;
8
- if (!runtime.version) {
9
- throw new Error("Runtime version not specified for node");
10
- }
11
- // Analyze package.json to check if build is needed
12
- const { build_script, type } = await analyze_package({ workdir });
13
- await build_project({ app, build_script });
14
- // Bundle project inside a docker image
15
- await bundle_docker({
16
- app,
17
- workdir,
18
- template_context: {
19
- from: `node:${runtime.version}`,
20
- },
21
- });
22
- return { type };
23
- };
24
-
25
- export { build_node };