@faable/faable 1.15.4 → 1.17.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.
|
@@ -7,6 +7,7 @@ import { build_project } from './build_project.js';
|
|
|
7
7
|
import { ensure_dependencies } from './ensure_dependencies.js';
|
|
8
8
|
import { inject_serve } from './inject_serve.js';
|
|
9
9
|
import { resolve_node_version } from './node_version.js';
|
|
10
|
+
import { wrap_next_config } from './wrap_next_config.js';
|
|
10
11
|
|
|
11
12
|
const BANNER = `NODE_VERSION=$(node --version)
|
|
12
13
|
NPM_VERSION=$(npm --version)
|
|
@@ -35,7 +36,15 @@ const node_buildpack = {
|
|
|
35
36
|
};
|
|
36
37
|
},
|
|
37
38
|
async build(ctx, plan) {
|
|
38
|
-
const env =
|
|
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
|
+
};
|
|
39
48
|
log.info(`Building with env variables ${Object.keys(env).join(",")}`);
|
|
40
49
|
// The workflow no longer runs `npm ci` — install here when needed, before
|
|
41
50
|
// the build and before `COPY . .` packages node_modules into the image.
|
|
@@ -44,7 +53,16 @@ const node_buildpack = {
|
|
|
44
53
|
const build_command = plan.build_script
|
|
45
54
|
? `npm run ${plan.build_script}`
|
|
46
55
|
: ctx.config.buildCommand;
|
|
47
|
-
|
|
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
|
+
}
|
|
48
66
|
// Frameworks without a bundled static server (CRA/Vue/Angular) need
|
|
49
67
|
// `serve` installed into node_modules before packaging.
|
|
50
68
|
if (plan.inject_serve) {
|
|
@@ -0,0 +1,138 @@
|
|
|
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 };
|
|
@@ -104,7 +104,7 @@ const deploy = {
|
|
|
104
104
|
.catch((error) => log.warn(`Could not mark deployment BUILDING: ${error.message}`));
|
|
105
105
|
const stop_log_sync = start_log_sync(api, deployment.id);
|
|
106
106
|
try {
|
|
107
|
-
await buildpack.build({ workdir, config, app, env_vars }, plan);
|
|
107
|
+
await buildpack.build({ workdir, config, app, env_vars, deployment }, plan);
|
|
108
108
|
// Upload to Faable registry, tagged by version and pinned to digest
|
|
109
109
|
const { image_ref } = await upload_tag({
|
|
110
110
|
app,
|