@dockerforge/core 0.1.0 → 0.1.2
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.
- package/README.md +22 -24
- package/package.json +8 -8
- package/src/{_engine/backend/src/modules → engine}/analysis/analyser.js +35 -2
- package/src/{_engine/shared → engine}/constants.js +1 -1
- package/src/{_engine/backend/src/modules → engine}/explanation/explainer.js +2 -2
- package/src/{_engine/backend/src/modules → engine}/generation/composeGenerator.js +1 -1
- package/src/{_engine/backend/src/modules → engine}/generation/generator.js +102 -58
- package/src/{_engine/backend/src/modules/engine.js → engine/index.js} +1 -6
- package/src/{_engine/backend/src/modules → engine}/ingestion/ingestion.js +2 -2
- package/src/{_engine/backend/src/modules → engine}/optimisation/optimiser.js +1 -1
- package/src/{_engine/backend/src/modules → engine}/security/security.js +1 -1
- package/src/index.js +7 -14
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# @dockerforge/core
|
|
2
2
|
|
|
3
|
-
The engine behind DockerForge.
|
|
4
|
-
returns a Dockerfile, a `.dockerignore`, and a Compose file, along with a confidence score and a
|
|
5
|
-
list of suggested improvements. It also lints existing Dockerfiles.
|
|
3
|
+
The engine behind DockerForge. Generate and lint production-grade Dockerfiles from Node. Offline.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@dockerforge/core)
|
|
6
|
+
[](https://github.com/Mo-ASayed/DockerForge/blob/main/LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
and
|
|
9
|
+
Give it a path to a local project and it analyses the stack and returns a Dockerfile, a
|
|
10
|
+
`.dockerignore`, and a Compose file, along with a confidence score and suggested improvements. It
|
|
11
|
+
also lints existing Dockerfiles. The package makes no network calls; it only reads the local
|
|
12
|
+
filesystem under the path you give it.
|
|
13
|
+
|
|
14
|
+
Most people should use the [`@dockerforge/cli`](https://www.npmjs.com/package/@dockerforge/cli)
|
|
15
|
+
command line tool. Use this package when you want to call the engine from your own Node code.
|
|
12
16
|
|
|
13
17
|
## Install
|
|
14
18
|
|
|
@@ -40,8 +44,8 @@ console.log(result.improvements); // suggested changes
|
|
|
40
44
|
| `optimise` | Set to `false` to skip the optimisation pass. |
|
|
41
45
|
| `security` | Set to `false` to skip the security pass. |
|
|
42
46
|
|
|
43
|
-
`projectPath` is required because this package is offline. Ingesting a remote git URL or a zip
|
|
44
|
-
|
|
47
|
+
`projectPath` is required because this package is offline. Ingesting a remote git URL or a zip is
|
|
48
|
+
part of the hosted product, not this package.
|
|
45
49
|
|
|
46
50
|
## Lint
|
|
47
51
|
|
|
@@ -55,34 +59,28 @@ console.log(summary.counts); // { critical, high, medium, low, info }
|
|
|
55
59
|
console.log(summary.worst); // the highest severity found, or null
|
|
56
60
|
```
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
Lint a string instead of a file:
|
|
59
63
|
|
|
60
64
|
```js
|
|
61
65
|
await core.lint({ dockerfile: 'FROM node\nUSER root\n' });
|
|
62
66
|
```
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
| Id | Check | Severity |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| DF001 | Base image is not pinned (no tag, or `:latest`) | high |
|
|
69
|
-
| DF002 | Final stage runs as root | high |
|
|
70
|
-
| DF003 | `COPY . .` copies the whole build context | high |
|
|
71
|
-
| DF004 | `.dockerignore` is missing or does not exclude `.env` | medium |
|
|
72
|
-
| DF005 | A secret-like value is hardcoded in `ENV` or `ARG` | critical |
|
|
73
|
-
| DF006 | No `WORKDIR` is set in the final stage | low |
|
|
68
|
+
The six rules (`DF001`–`DF006`) are documented in the
|
|
69
|
+
[rules reference](https://github.com/Mo-ASayed/DockerForge/blob/main/docs/rules.md).
|
|
74
70
|
|
|
75
71
|
## Errors
|
|
76
72
|
|
|
77
|
-
The package throws typed errors, each carrying a `.code`:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`DockerForgeError`. They are exported from the package root:
|
|
73
|
+
The package throws typed errors, each carrying a `.code`: `PathNotFoundError`,
|
|
74
|
+
`NotADirectoryError`, `UnsupportedStackError`, `IngestError`, and the base `DockerForgeError`.
|
|
75
|
+
They are exported from the package root:
|
|
81
76
|
|
|
82
77
|
```js
|
|
83
78
|
const { PathNotFoundError } = require('@dockerforge/core');
|
|
84
79
|
```
|
|
85
80
|
|
|
81
|
+
See the [programmatic API guide](https://github.com/Mo-ASayed/DockerForge/blob/main/docs/programmatic.md)
|
|
82
|
+
for the full surface.
|
|
83
|
+
|
|
86
84
|
## License
|
|
87
85
|
|
|
88
86
|
Apache-2.0.
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dockerforge/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DockerForge engine: analyse a local project and generate production-grade Dockerfiles, .dockerignore, and Compose. Offline, no network.",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DockerForge engine: analyse a local project and generate production-grade Dockerfiles, .dockerignore, and Compose, and lint Dockerfiles. Offline, no network.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Docker Forge",
|
|
7
7
|
"homepage": "https://containerise.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/Mo-ASayed/
|
|
11
|
-
"directory": "
|
|
10
|
+
"url": "git+https://github.com/Mo-ASayed/DockerForge.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/Mo-ASayed/
|
|
14
|
+
"url": "https://github.com/Mo-ASayed/DockerForge/issues"
|
|
15
15
|
},
|
|
16
16
|
"main": "src/index.js",
|
|
17
17
|
"files": [
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node --test"
|
|
30
|
-
"prepack": "node scripts/vendor-engine.js"
|
|
29
|
+
"test": "node --test"
|
|
31
30
|
},
|
|
32
31
|
"keywords": [
|
|
33
32
|
"docker",
|
|
34
33
|
"dockerfile",
|
|
35
34
|
"containerize",
|
|
36
35
|
"generator",
|
|
37
|
-
"lint"
|
|
36
|
+
"lint",
|
|
37
|
+
"sarif"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"adm-zip": "^0.5.10",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Part of the @dockerforge/core engine.
|
|
2
2
|
// Walks the full project tree, finds every service root, analyses each one.
|
|
3
3
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
|
-
const { STACKS, DEFAULT_VERSIONS, DEFAULT_PORTS, ROOT_CONFIG_FILES } = require('
|
|
6
|
+
const { STACKS, DEFAULT_VERSIONS, DEFAULT_PORTS, ROOT_CONFIG_FILES } = require('../constants');
|
|
7
7
|
|
|
8
8
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
9
9
|
|
|
@@ -707,6 +707,36 @@ async function analyseNode(projectPath, files, hints, projectRootPath = null, is
|
|
|
707
707
|
// Role
|
|
708
708
|
const role = await detectRole(projectPath, pkg);
|
|
709
709
|
|
|
710
|
+
// Detect which conventional source directories actually exist on disk, so the
|
|
711
|
+
// generator copies real directories instead of guessing the layout (e.g. app/ vs src/).
|
|
712
|
+
const SOURCE_DIR_CANDIDATES = [
|
|
713
|
+
'src', 'app', 'pages', 'components', 'lib', 'styles',
|
|
714
|
+
'public', 'server', 'config', 'content', 'i18n', 'locales', 'hooks', 'utils',
|
|
715
|
+
];
|
|
716
|
+
const sourceDirs = [];
|
|
717
|
+
for (const d of SOURCE_DIR_CANDIDATES) {
|
|
718
|
+
try { if ((await fs.stat(path.join(projectPath, d))).isDirectory()) sourceDirs.push(d); }
|
|
719
|
+
catch { /* absent */ }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Detect framework build/runtime config files that exist, so we copy real filenames
|
|
723
|
+
// instead of guessing a single hard-coded one. Next.js also reads its config at runtime.
|
|
724
|
+
const FRAMEWORK_CONFIG_CANDIDATES = [
|
|
725
|
+
'next.config.js', 'next.config.mjs', 'next.config.cjs', 'next.config.ts',
|
|
726
|
+
'tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs', 'tailwind.config.mjs',
|
|
727
|
+
'components.json', 'jsconfig.json',
|
|
728
|
+
'remix.config.js', 'svelte.config.js', 'nuxt.config.ts', 'nuxt.config.js',
|
|
729
|
+
];
|
|
730
|
+
const frameworkConfigFiles = [];
|
|
731
|
+
for (const f of FRAMEWORK_CONFIG_CANDIDATES) {
|
|
732
|
+
try { if ((await fs.stat(path.join(projectPath, f))).isFile()) frameworkConfigFiles.push(f); }
|
|
733
|
+
catch { /* absent */ }
|
|
734
|
+
}
|
|
735
|
+
// The config Next.js reads on `next start` (first match wins) — must be in the runtime image.
|
|
736
|
+
const nextRuntimeConfig = framework === 'nextjs'
|
|
737
|
+
? (frameworkConfigFiles.find(f => f.startsWith('next.config.')) || null)
|
|
738
|
+
: null;
|
|
739
|
+
|
|
710
740
|
// Detect sibling directories referenced via require('../xxx') in the entry point.
|
|
711
741
|
// These are root-level dirs that sit alongside the service dir and must be COPYed
|
|
712
742
|
// into the image so cross-directory requires resolve at runtime.
|
|
@@ -731,6 +761,9 @@ async function analyseNode(projectPath, files, hints, projectRootPath = null, is
|
|
|
731
761
|
rootBuildDeps, // root-level dirs imported by vite config via '../dirname' — must be copied before build
|
|
732
762
|
hasScopedPackages,
|
|
733
763
|
siblingDeps, // root-level dirs referenced via require('../xxx') — must be COPYed into runtime image
|
|
764
|
+
sourceDirs, // conventional source dirs that actually exist on disk (real, not guessed)
|
|
765
|
+
frameworkConfigFiles, // framework build/runtime config files present (next.config, tailwind.config, ...)
|
|
766
|
+
nextRuntimeConfig, // the next.config.* file Next.js reads at runtime, or null
|
|
734
767
|
assumptions,
|
|
735
768
|
};
|
|
736
769
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Part of the @dockerforge/core engine.
|
|
2
2
|
// Turns analysis + result into a human-readable explanation
|
|
3
3
|
|
|
4
|
-
const { STACKS } = require('
|
|
4
|
+
const { STACKS } = require('../constants');
|
|
5
5
|
|
|
6
6
|
function buildExplanation(analysis, result, securityNotes) {
|
|
7
7
|
const stackNames = {
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Part of the @dockerforge/core engine.
|
|
2
2
|
// Accepts { services, sharedDirs } from the analyser.
|
|
3
3
|
// Produces one Dockerfile (multi-stage if needed) + .dockerignore.
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { STACKS, BASE_IMAGES } = require('
|
|
6
|
+
const { STACKS, BASE_IMAGES } = require('../constants');
|
|
7
7
|
const { isSecretLikeEnvKey, isSecretLikeEnvValue } = require('../security/security');
|
|
8
8
|
|
|
9
|
-
const STATIC_RUNTIME_IMAGE = 'nginx:1.27-alpine';
|
|
10
9
|
const PNPM_VERSION = '9';
|
|
10
|
+
const STATIC_SERVER_PACKAGE = 'serve@14.2.4';
|
|
11
|
+
|
|
12
|
+
// Frameworks that produce a long-running Node server (SSR, API routes, dynamic rendering).
|
|
13
|
+
// These run with `next start` / a framework server instead of a generic static file server.
|
|
14
|
+
const SERVER_RENDERED_FRAMEWORKS = new Set(['nextjs', 'remix', 'sveltekit', 'nuxt']);
|
|
11
15
|
|
|
12
16
|
function installPnpmGlobalCmd() {
|
|
13
17
|
return `npm install -g pnpm@${PNPM_VERSION}`;
|
|
@@ -32,39 +36,20 @@ function runtimeStartCmd(a) {
|
|
|
32
36
|
return ['node', `${buildOut}/${entry.replace(/^\.\//, '')}`];
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function
|
|
36
|
-
return
|
|
37
|
-
}
|
|
39
|
+
function staticServeRuntimeStage(baseImage, port, buildAssetCopyLine, outputDir) {
|
|
40
|
+
return `FROM ${baseImage}
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
return `server {
|
|
41
|
-
listen ${port};
|
|
42
|
-
root /usr/share/nginx/html;
|
|
43
|
-
index index.html;
|
|
42
|
+
WORKDIR /app
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
add_header X-Content-Type-Options "nosniff";
|
|
47
|
-
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
|
44
|
+
ENV NODE_ENV=production
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
46
|
+
RUN npm install -g ${STATIC_SERVER_PACKAGE} && npm cache clean --force
|
|
47
|
+
${buildAssetCopyLine}
|
|
48
|
+
USER node
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
gzip on;
|
|
58
|
-
gzip_types text/plain text/css application/json application/javascript
|
|
59
|
-
text/xml application/xml image/svg+xml font/woff2 application/manifest+json;
|
|
60
|
-
}`.trim();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function nginxNonRootPrepBlock() {
|
|
64
|
-
return [
|
|
65
|
-
'RUN mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp /var/cache/nginx/scgi_temp',
|
|
66
|
-
'RUN touch /var/run/nginx.pid && chown -R nginx:nginx /var/cache/nginx /var/run/nginx.pid /var/log/nginx /etc/nginx/conf.d /usr/share/nginx/html',
|
|
67
|
-
].join('\n');
|
|
50
|
+
EXPOSE ${port}
|
|
51
|
+
${simpleHttpHealthcheck(port)}
|
|
52
|
+
CMD ["serve", "-s", "${outputDir}", "-l", "${port}"]`;
|
|
68
53
|
}
|
|
69
54
|
|
|
70
55
|
function simpleHttpHealthcheck(port) {
|
|
@@ -189,6 +174,22 @@ function nodeSourceCopyBlock(a, configCopyLine = '', frontendAssetCopyLine = '')
|
|
|
189
174
|
return blocks.join('\n');
|
|
190
175
|
}
|
|
191
176
|
|
|
177
|
+
// Build-stage source copy for Next.js. Copies the config files and source directories that
|
|
178
|
+
// actually exist on disk (detected by the analyser) instead of guessing app/ vs src/.
|
|
179
|
+
function nextSourceCopyBlock(a, rootConfigFiles = []) {
|
|
180
|
+
const lines = [];
|
|
181
|
+
const configFiles = [...new Set([
|
|
182
|
+
...rootConfigFiles.filter(f => !['package.json', a.lockFile].includes(f)),
|
|
183
|
+
...(a.frameworkConfigFiles || []),
|
|
184
|
+
])];
|
|
185
|
+
if (configFiles.length > 0) lines.push(`COPY ${configFiles.join(' ')} ./`);
|
|
186
|
+
const dirs = (Array.isArray(a.sourceDirs) && a.sourceDirs.length > 0)
|
|
187
|
+
? a.sourceDirs
|
|
188
|
+
: ['app', 'pages', 'components', 'public']; // fallback only if nothing was detected
|
|
189
|
+
for (const d of dirs) lines.push(`COPY ${d}/ ./${d}/`);
|
|
190
|
+
return lines.join('\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
192
193
|
function pythonBuilderImage(a) {
|
|
193
194
|
return a.hasNativeDeps ? `python:${a.version}` : BASE_IMAGES[STACKS.PYTHON](a.version);
|
|
194
195
|
}
|
|
@@ -273,6 +274,9 @@ function buildEnvBlock(envVars) {
|
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
function buildCommandPrefix(service) {
|
|
277
|
+
// Server-rendered frameworks (Next.js, Remix, ...) run their own build; the CRA-specific
|
|
278
|
+
// env prefix (DISABLE_ESLINT_PLUGIN etc.) does not apply and just adds noise.
|
|
279
|
+
if (SERVER_RENDERED_FRAMEWORKS.has(service.framework)) return '';
|
|
276
280
|
// Fire for CRA regardless of how detection landed — framework='cra', role='frontend',
|
|
277
281
|
// or any build command that invokes react-scripts directly.
|
|
278
282
|
const isReactScripts =
|
|
@@ -668,8 +672,7 @@ ENTRYPOINT ["dotnet", "${be.projectName}.dll"]`.trim());
|
|
|
668
672
|
}
|
|
669
673
|
improvements.push('HEALTHCHECK should probe /health for each runtime service; add a /health endpoint returning 2xx where missing');
|
|
670
674
|
|
|
671
|
-
|
|
672
|
-
return { dockerfile, dockerignore, improvements, nginxConf: collectedNginxConf };
|
|
675
|
+
return { dockerfile, dockerignore, improvements };
|
|
673
676
|
}
|
|
674
677
|
|
|
675
678
|
// ── Node at root (original behaviour) ───────────────────────────────────────
|
|
@@ -690,10 +693,18 @@ function generateNodeRoot(a, envVars = [], rootConfigFiles = []) {
|
|
|
690
693
|
const buildOut = a.buildOutputDir || 'dist';
|
|
691
694
|
const sourceCopyBlock = nodeSourceCopyBlock(a, configCopyLine, frontendAssetCopyLine);
|
|
692
695
|
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
//
|
|
696
|
-
if (a.
|
|
696
|
+
// Next.js (and other SSR frameworks): a Node server, NOT a static site.
|
|
697
|
+
// Build with `next build`, then run `next start` on a lean Node runtime. Serving the
|
|
698
|
+
// .next output via a generic static server would break SSR, API routes, and dynamic rendering.
|
|
699
|
+
if (a.framework === 'nextjs') {
|
|
700
|
+
const prodInstall = nodeInstallLine(a.packageManager, true, { hasScopedPackages: a.hasScopedPackages });
|
|
701
|
+
const nextSrcCopy = nextSourceCopyBlock(a, rootConfigFiles);
|
|
702
|
+
const hasPublic = Array.isArray(a.sourceDirs) && a.sourceDirs.includes('public');
|
|
703
|
+
const publicCopy = hasPublic ? `COPY --from=builder /app/public ./public\n` : '';
|
|
704
|
+
const runtimeConfigCopy = a.nextRuntimeConfig
|
|
705
|
+
? `COPY --from=builder /app/${a.nextRuntimeConfig} ./${a.nextRuntimeConfig}\n`
|
|
706
|
+
: '';
|
|
707
|
+
|
|
697
708
|
dockerfile = `
|
|
698
709
|
# ─── Stage 1: Build ───────────────────────────────────────
|
|
699
710
|
${builderFrom(baseImage, 'builder')}
|
|
@@ -703,23 +714,62 @@ WORKDIR /app
|
|
|
703
714
|
COPY ${a.lockFile} package.json ./
|
|
704
715
|
${buildInstall}
|
|
705
716
|
|
|
706
|
-
# Copy source
|
|
707
|
-
${
|
|
717
|
+
# Copy source and build-time config
|
|
718
|
+
${nextSrcCopy}
|
|
708
719
|
RUN ${buildPrefix}${a.buildCommand}
|
|
709
720
|
|
|
710
721
|
# ─── Stage 2: Runtime ─────────────────────────────────────
|
|
711
|
-
FROM ${
|
|
722
|
+
FROM ${baseImage}
|
|
723
|
+
|
|
724
|
+
WORKDIR /app
|
|
725
|
+
|
|
726
|
+
${buildEnvBlock(envVars)}
|
|
712
727
|
|
|
713
|
-
COPY
|
|
714
|
-
${
|
|
728
|
+
COPY ${a.lockFile} package.json ./
|
|
729
|
+
${prodInstall}
|
|
730
|
+
|
|
731
|
+
# Next.js runs as a Node server; copy the build output, static assets, and the config it reads at runtime
|
|
732
|
+
COPY --from=builder /app/${buildOut} ./${buildOut}
|
|
733
|
+
${publicCopy}${runtimeConfigCopy}${runtimeUserBlock(true)}
|
|
715
734
|
|
|
735
|
+
${nodeStopSignalBlock()}
|
|
716
736
|
EXPOSE ${a.port}
|
|
717
737
|
${simpleHttpHealthcheck(a.port)}
|
|
718
|
-
CMD
|
|
738
|
+
CMD ${startCmdJson}`.trim();
|
|
739
|
+
|
|
740
|
+
improvements.push('Next.js is built and run as a Node server (next start) so SSR and API routes work — a generic static server image would break them.');
|
|
741
|
+
improvements.push('Smaller image option: set `output: "standalone"` in next.config, then copy .next/standalone + .next/static instead of installing node_modules.');
|
|
742
|
+
improvements.push('Only known framework config files are auto-copied — verify any project-specific build config is also COPYed (e.g. sentry.*.config.ts, next-i18next.config).');
|
|
743
|
+
improvements.push('HEALTHCHECK probes /health, which Next.js does not expose by default — add a health route or point the healthcheck at an existing path.');
|
|
744
|
+
return { dockerfile, dockerignore: nodeDockerignore(), improvements };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// CRA / pure-frontend SPA: runtime is a static file server, not a Node app.
|
|
748
|
+
// react-scripts is a devDep — it won't be present after --production install,
|
|
749
|
+
// and react-scripts start is a dev server anyway. Server-rendered frameworks are
|
|
750
|
+
// explicitly excluded so they never get the static-file-server treatment.
|
|
751
|
+
const isStaticSpa = (a.role === 'frontend' || a.framework === 'cra')
|
|
752
|
+
&& !SERVER_RENDERED_FRAMEWORKS.has(a.framework);
|
|
753
|
+
if (isStaticSpa) {
|
|
754
|
+
dockerfile = `
|
|
755
|
+
# ─── Stage 1: Build ───────────────────────────────────────
|
|
756
|
+
${builderFrom(baseImage, 'builder')}
|
|
757
|
+
|
|
758
|
+
WORKDIR /app
|
|
759
|
+
|
|
760
|
+
COPY ${a.lockFile} package.json ./
|
|
761
|
+
${buildInstall}
|
|
762
|
+
|
|
763
|
+
# Copy source
|
|
764
|
+
${sourceCopyBlock}
|
|
765
|
+
RUN ${buildPrefix}${a.buildCommand}
|
|
766
|
+
|
|
767
|
+
# ─── Stage 2: Runtime ─────────────────────────────────────
|
|
768
|
+
${staticServeRuntimeStage(baseImage, a.port, `COPY --from=builder /app/${buildOut} ./${buildOut}`, buildOut)}`.trim();
|
|
719
769
|
|
|
720
770
|
const validationDockerfile = dockerfile;
|
|
721
|
-
improvements.push(`Frontend-only: built then served with ${
|
|
722
|
-
return withValidationVariant({ dockerfile, dockerignore: nodeDockerignore(),
|
|
771
|
+
improvements.push(`Frontend-only: built then served with ${STATIC_SERVER_PACKAGE} as the non-root node user.`);
|
|
772
|
+
return withValidationVariant({ dockerfile, dockerignore: nodeDockerignore(), improvements }, validationDockerfile);
|
|
723
773
|
|
|
724
774
|
} else {
|
|
725
775
|
const pruneCmd = nodePruneLine(a.packageManager);
|
|
@@ -807,8 +857,9 @@ function generateNodeSub(a, dir, sharedDirs, staticAssets = [], rootConfigFiles
|
|
|
807
857
|
|
|
808
858
|
let dockerfile;
|
|
809
859
|
|
|
810
|
-
if (a.hasBuild && a.role === 'frontend') {
|
|
811
|
-
//
|
|
860
|
+
if (a.hasBuild && a.role === 'frontend' && !SERVER_RENDERED_FRAMEWORKS.has(a.framework)) {
|
|
861
|
+
// Static SPA build → serve the compiled output. Server-rendered frameworks (Next.js, Remix, ...)
|
|
862
|
+
// are excluded here and fall through to the Node-server runtime branch below.
|
|
812
863
|
const stageName = 'build';
|
|
813
864
|
|
|
814
865
|
// When lock file is at project root, copy root files + service package.json and
|
|
@@ -883,18 +934,11 @@ ${sourceCopy}
|
|
|
883
934
|
${sharedConfigCopy}${buildRun}
|
|
884
935
|
|
|
885
936
|
# ─── Stage 2: Serve ───────────────────────────────────────
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
COPY --from=${stageName} ${buildOutput} /usr/share/nginx/html
|
|
889
|
-
${nginxStaticServerBlock(a.port)}
|
|
890
|
-
|
|
891
|
-
EXPOSE ${a.port}
|
|
892
|
-
${simpleHttpHealthcheck(a.port)}
|
|
893
|
-
CMD ["nginx", "-g", "daemon off;"]`.trim();
|
|
937
|
+
${staticServeRuntimeStage(baseImage, a.port, `COPY --from=${stageName} ${buildOutput} ./${a.buildOutputDir || 'build'}`, a.buildOutputDir || 'build')}`.trim();
|
|
894
938
|
|
|
895
939
|
const validationDockerfile = dockerfile;
|
|
896
|
-
improvements.push(`Frontend-only: built then served with ${
|
|
897
|
-
return withValidationVariant({ dockerfile, dockerignore: nodeDockerignore(),
|
|
940
|
+
improvements.push(`Frontend-only: built then served with ${STATIC_SERVER_PACKAGE} as the non-root node user. If a backend also serves the files, merge into one multi-service build.`);
|
|
941
|
+
return withValidationVariant({ dockerfile, dockerignore: nodeDockerignore(), improvements }, validationDockerfile);
|
|
898
942
|
|
|
899
943
|
|
|
900
944
|
} else {
|
|
@@ -132,7 +132,7 @@ function applyValidationEvidence(confidenceResult, validation) {
|
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Strip BuildKit-specific features from a Dockerfile to produce the simple default output.
|
|
135
|
-
*
|
|
135
|
+
* Build environment prefixes stay in both outputs because they are part of the recipe.
|
|
136
136
|
*/
|
|
137
137
|
function toSimpleDockerfile(dockerfile) {
|
|
138
138
|
return dockerfile.split('\n').map(line => {
|
|
@@ -140,11 +140,6 @@ function toSimpleDockerfile(dockerfile) {
|
|
|
140
140
|
if (!isRun) return line;
|
|
141
141
|
// Strip --mount=type=cache,... and --mount=type=secret,...
|
|
142
142
|
line = line.replace(/--mount=type=\w+,\S+\s+/g, '');
|
|
143
|
-
// Strip CRA build environment prefix (CI=false GENERATE_SOURCEMAP=false ...)
|
|
144
|
-
line = line.replace(
|
|
145
|
-
/^(RUN\s+)CI=false GENERATE_SOURCEMAP=false NODE_OPTIONS=--max-old-space-size=\d+ DISABLE_ESLINT_PLUGIN=true\s+/,
|
|
146
|
-
'$1'
|
|
147
|
-
);
|
|
148
143
|
return line;
|
|
149
144
|
}).join('\n');
|
|
150
145
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Part of the @dockerforge/core engine.
|
|
2
2
|
// Fetches repo file tree + key files via provider APIs — no git binary needed.
|
|
3
3
|
// Works on Vercel, Railway, Render, etc.
|
|
4
4
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs-extra');
|
|
7
|
-
const { IGNORED_DIRS, ROOT_CONFIG_FILES } = require('
|
|
7
|
+
const { IGNORED_DIRS, ROOT_CONFIG_FILES } = require('../constants');
|
|
8
8
|
|
|
9
9
|
const IGNORED_SET = new Set(IGNORED_DIRS);
|
|
10
10
|
|
package/src/index.js
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// @dockerforge/core -
|
|
3
|
+
// @dockerforge/core - the offline DockerForge engine, public surface.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// backend/src/modules paths.
|
|
5
|
+
// Given a path to a local project, it analyses the stack and generates a Dockerfile,
|
|
6
|
+
// a .dockerignore, and a Compose file, and it lints existing Dockerfiles. This file is the
|
|
7
|
+
// stable public API (see docs/contracts/core-contract.md). The engine itself lives under
|
|
8
|
+
// ./engine.
|
|
10
9
|
//
|
|
11
10
|
// No-network guarantee: this module performs zero outbound network calls. It only reads the
|
|
12
11
|
// local filesystem under the resolved project path. Remote ingestion (git URL / zip URL) is
|
|
13
|
-
// intentionally NOT exposed here - that adapter lives
|
|
12
|
+
// intentionally NOT exposed here - that adapter lives in the proprietary cloud.
|
|
14
13
|
|
|
15
14
|
const path = require('path');
|
|
16
15
|
const fs = require('fs-extra');
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
// scripts/vendor-engine.js, run at prepack). In the monorepo (dev/test) that dir does not
|
|
20
|
-
// exist, so we fall back to the canonical backend/ source. Same code either way.
|
|
21
|
-
const _vendoredEngine = require('path').join(__dirname, '_engine', 'backend', 'src', 'modules', 'engine.js');
|
|
22
|
-
const engine = require('fs').existsSync(_vendoredEngine)
|
|
23
|
-
? require(_vendoredEngine)
|
|
24
|
-
: require('../../../backend/src/modules/engine');
|
|
17
|
+
const engine = require('./engine');
|
|
25
18
|
const errors = require('./errors');
|
|
26
19
|
const { lint } = require('./lint');
|
|
27
20
|
|