@git.zone/tsdocker 1.3.0 → 1.4.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.
@@ -1,3 +1,4 @@
1
+ import * as lik from '@push.rocks/lik';
1
2
  import * as npmextra from '@push.rocks/npmextra';
2
3
  import * as path from 'path';
3
4
  import * as projectinfo from '@push.rocks/projectinfo';
@@ -12,4 +13,4 @@ import * as smartopen from '@push.rocks/smartopen';
12
13
  import * as smartshell from '@push.rocks/smartshell';
13
14
  import * as smartstring from '@push.rocks/smartstring';
14
15
  export declare const smartfs: SmartFs;
15
- export { npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
16
+ export { lik, npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
@@ -1,4 +1,5 @@
1
1
  // push.rocks scope
2
+ import * as lik from '@push.rocks/lik';
2
3
  import * as npmextra from '@push.rocks/npmextra';
3
4
  import * as path from 'path';
4
5
  import * as projectinfo from '@push.rocks/projectinfo';
@@ -14,5 +15,5 @@ import * as smartshell from '@push.rocks/smartshell';
14
15
  import * as smartstring from '@push.rocks/smartstring';
15
16
  // Create smartfs instance
16
17
  export const smartfs = new SmartFs(new SmartFsProviderNode());
17
- export { npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
18
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkb2NrZXIucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3RzZG9ja2VyLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxRQUFRLE1BQU0sc0JBQXNCLENBQUM7QUFDakQsT0FBTyxLQUFLLElBQUksTUFBTSxNQUFNLENBQUM7QUFDN0IsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssWUFBWSxNQUFNLDBCQUEwQixDQUFDO0FBQ3pELE9BQU8sS0FBSyxJQUFJLE1BQU0sa0JBQWtCLENBQUM7QUFDekMsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEVBQUUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDbkUsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLGdCQUFnQixNQUFNLGlDQUFpQyxDQUFDO0FBQ3BFLE9BQU8sS0FBSyxTQUFTLE1BQU0sdUJBQXVCLENBQUM7QUFDbkQsT0FBTyxLQUFLLFVBQVUsTUFBTSx3QkFBd0IsQ0FBQztBQUNyRCxPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBRXZELDBCQUEwQjtBQUMxQixNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsSUFBSSxPQUFPLENBQUMsSUFBSSxtQkFBbUIsRUFBRSxDQUFDLENBQUM7QUFFOUQsT0FBTyxFQUNMLFFBQVEsRUFDUixJQUFJLEVBQ0osV0FBVyxFQUNYLFlBQVksRUFDWixJQUFJLEVBQ0osUUFBUSxFQUNSLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsZ0JBQWdCLEVBQ2hCLFNBQVMsRUFDVCxVQUFVLEVBQ1YsV0FBVyxFQUNaLENBQUMifQ==
18
+ export { lik, npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
19
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkb2NrZXIucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3RzZG9ja2VyLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUM3QixPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLElBQUksTUFBTSxrQkFBa0IsQ0FBQztBQUN6QyxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNuRSxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sS0FBSyx3QkFBd0IsTUFBTSx3Q0FBd0MsQ0FBQztBQUNuRixPQUFPLEtBQUssZ0JBQWdCLE1BQU0saUNBQWlDLENBQUM7QUFDcEUsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsMEJBQTBCO0FBQzFCLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxJQUFJLE9BQU8sQ0FBQyxJQUFJLG1CQUFtQixFQUFFLENBQUMsQ0FBQztBQUU5RCxPQUFPLEVBQ0wsR0FBRyxFQUNILFFBQVEsRUFDUixJQUFJLEVBQ0osV0FBVyxFQUNYLFlBQVksRUFDWixJQUFJLEVBQ0osUUFBUSxFQUNSLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsZ0JBQWdCLEVBQ2hCLFNBQVMsRUFDVCxVQUFVLEVBQ1YsV0FBVyxFQUNaLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@git.zone/tsdocker",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "description": "develop npm modules cross platform with docker",
6
6
  "main": "dist_ts/index.js",
@@ -13,7 +13,6 @@
13
13
  "build": "(tsbuild)",
14
14
  "testIntegration": "(npm run clean && npm run setupCheck && npm run testStandard)",
15
15
  "testStandard": "(cd test/ && tsx ../ts/index.ts)",
16
- "testSpeed": "(cd test/ && tsx ../ts/index.ts speedtest)",
17
16
  "testClean": "(cd test/ && tsx ../ts/index.ts clean --all)",
18
17
  "testVscode": "(cd test/ && tsx ../ts/index.ts vscode)",
19
18
  "clean": "(rm -rf test/)",
@@ -41,6 +40,7 @@
41
40
  "@types/node": "^25.0.9"
42
41
  },
43
42
  "dependencies": {
43
+ "@push.rocks/lik": "^6.2.2",
44
44
  "@push.rocks/npmextra": "^5.3.3",
45
45
  "@push.rocks/projectinfo": "^5.0.2",
46
46
  "@push.rocks/qenv": "^6.1.3",
package/readme.hints.md CHANGED
@@ -2,39 +2,108 @@
2
2
 
3
3
  ## Module Purpose
4
4
 
5
- tsdocker is a tool for developing npm modules cross-platform using Docker. It allows testing in clean, reproducible Linux environments locally.
5
+ tsdocker is a comprehensive Docker development and building tool. It provides:
6
+ - Testing npm modules in clean Docker environments (legacy feature)
7
+ - Building Dockerfiles with dependency ordering
8
+ - Multi-registry push/pull support
9
+ - Multi-architecture builds (amd64/arm64)
6
10
 
7
- ## Recent Upgrades (2025-11-22)
11
+ ## New CLI Commands (2026-01-19)
8
12
 
9
- - Updated all @git.zone/_ dependencies to @git.zone/_ scope (latest versions)
10
- - Updated all @pushrocks/_ dependencies to @push.rocks/_ scope (latest versions)
11
- - Migrated from smartfile v8 to smartfs v1.1.0
12
- - All filesystem operations now use smartfs fluent API
13
- - Operations are now async (smartfs is async-only)
14
- - Updated dev dependencies:
15
- - @git.zone/tsbuild: ^3.1.0
16
- - @git.zone/tsrun: ^2.0.0
17
- - @git.zone/tstest: ^3.1.3
18
- - Removed @pushrocks/tapbundle (now use @git.zone/tstest/tapbundle)
19
- - Updated @types/node to ^22.10.2
20
- - Removed tslint and tslint-config-prettier (no longer needed)
13
+ | Command | Description |
14
+ |---------|-------------|
15
+ | `tsdocker` | Run tests in container (legacy default behavior) |
16
+ | `tsdocker build` | Build all Dockerfiles with dependency ordering |
17
+ | `tsdocker push [registry]` | Push images to configured registries |
18
+ | `tsdocker pull <registry>` | Pull images from registry |
19
+ | `tsdocker test` | Run container tests (test scripts) |
20
+ | `tsdocker login` | Login to configured registries |
21
+ | `tsdocker list` | List discovered Dockerfiles and dependencies |
22
+ | `tsdocker clean --all` | Clean up Docker environment |
23
+ | `tsdocker vscode` | Start VS Code in Docker |
21
24
 
22
- ## SmartFS Migration Details
25
+ ## Configuration
23
26
 
24
- The following operations were converted:
27
+ Configure in `package.json` under `@git.zone/tsdocker`:
25
28
 
26
- - `smartfile.fs.fileExistsSync()` → Node.js `fs.existsSync()` (for sync needs)
27
- - `smartfile.fs.ensureDirSync()` → Node.js `fs.mkdirSync(..., { recursive: true })`
28
- - `smartfile.memory.toFsSync()` → `smartfs.file(path).write(content)` (async)
29
- - `smartfile.fs.removeSync()` → `smartfs.file(path).delete()` (async)
29
+ ```json
30
+ {
31
+ "@git.zone/tsdocker": {
32
+ "registries": ["registry.gitlab.com", "docker.io"],
33
+ "registryRepoMap": {
34
+ "registry.gitlab.com": "host.today/ht-docker-node"
35
+ },
36
+ "buildArgEnvMap": {
37
+ "NODE_VERSION": "NODE_VERSION"
38
+ },
39
+ "platforms": ["linux/amd64", "linux/arm64"],
40
+ "push": false,
41
+ "testDir": "./test"
42
+ }
43
+ }
44
+ ```
30
45
 
31
- ## Test Status
46
+ ### Configuration Options
32
47
 
33
- - Build: Passes
34
- - The integration test requires cloning an external test repository (sandbox-npmts)
35
- - The external test repo uses top-level await which requires ESM module handling
36
- - This is not a tsdocker issue but rather the test repository's structure
48
+ - `baseImage`: Base Docker image for testing (legacy)
49
+ - `command`: Command to run in container (legacy)
50
+ - `dockerSock`: Mount Docker socket (legacy)
51
+ - `registries`: Array of registry URLs to push to
52
+ - `registryRepoMap`: Map registry URLs to different repo paths
53
+ - `buildArgEnvMap`: Map Docker build ARGs to environment variables
54
+ - `platforms`: Target architectures for buildx
55
+ - `push`: Auto-push after build
56
+ - `testDir`: Directory containing test scripts
57
+
58
+ ## Registry Authentication
59
+
60
+ Set environment variables for registry login:
61
+
62
+ ```bash
63
+ # Pipe-delimited format (numbered 1-10)
64
+ export DOCKER_REGISTRY_1="registry.gitlab.com|username|password"
65
+ export DOCKER_REGISTRY_2="docker.io|username|password"
66
+
67
+ # Or individual registry format
68
+ export DOCKER_REGISTRY_URL="registry.gitlab.com"
69
+ export DOCKER_REGISTRY_USER="username"
70
+ export DOCKER_REGISTRY_PASSWORD="password"
71
+ ```
72
+
73
+ ## File Structure
74
+
75
+ ```
76
+ ts/
77
+ ├── index.ts (entry point)
78
+ ├── tsdocker.cli.ts (CLI commands)
79
+ ├── tsdocker.config.ts (configuration)
80
+ ├── tsdocker.plugins.ts (plugin imports)
81
+ ├── tsdocker.docker.ts (legacy test runner)
82
+ ├── tsdocker.snippets.ts (Dockerfile generation)
83
+ ├── classes.dockerfile.ts (Dockerfile management)
84
+ ├── classes.dockerregistry.ts (registry authentication)
85
+ ├── classes.registrystorage.ts (registry storage)
86
+ ├── classes.tsdockermanager.ts (orchestrator)
87
+ └── interfaces/
88
+ └── index.ts (type definitions)
89
+ ```
37
90
 
38
91
  ## Dependencies
39
92
 
40
- All dependencies are now at their latest versions compatible with Node.js without introducing new Node.js-specific dependencies.
93
+ - `@push.rocks/lik`: Object mapping utilities
94
+ - `@push.rocks/smartfs`: Filesystem operations
95
+ - `@push.rocks/smartshell`: Shell command execution
96
+ - `@push.rocks/smartcli`: CLI framework
97
+ - `@push.rocks/projectinfo`: Project metadata
98
+
99
+ ## Build Status
100
+
101
+ - Build: ✅ Passes
102
+ - Legacy test functionality preserved
103
+ - New Docker build functionality added
104
+
105
+ ## Previous Upgrades (2025-11-22)
106
+
107
+ - Updated all @git.zone/_ dependencies to @git.zone/_ scope
108
+ - Updated all @pushrocks/_ dependencies to @push.rocks/_ scope
109
+ - Migrated from smartfile v8 to smartfs v1.1.0
package/readme.md CHANGED
@@ -128,14 +128,6 @@ tsdocker vscode
128
128
 
129
129
  Launches a containerized VS Code instance accessible via browser at `testing-vscode.git.zone:8443`.
130
130
 
131
- ### Speed Test
132
-
133
- ```bash
134
- tsdocker speedtest
135
- ```
136
-
137
- Runs a network speed test inside a Docker container.
138
-
139
131
  ## Advanced Usage
140
132
 
141
133
  ### Docker-in-Docker Testing
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/tsdocker',
6
- version: '1.3.0',
6
+ version: '1.4.0',
7
7
  description: 'develop npm modules cross platform with docker'
8
8
  }
@@ -0,0 +1,462 @@
1
+ import * as plugins from './tsdocker.plugins.js';
2
+ import * as paths from './tsdocker.paths.js';
3
+ import { logger } from './tsdocker.logging.js';
4
+ import { DockerRegistry } from './classes.dockerregistry.js';
5
+ import type { IDockerfileOptions, ITsDockerConfig } from './interfaces/index.js';
6
+ import type { TsDockerManager } from './classes.tsdockermanager.js';
7
+
8
+ const smartshellInstance = new plugins.smartshell.Smartshell({
9
+ executor: 'bash',
10
+ });
11
+
12
+ /**
13
+ * Class Dockerfile represents a Dockerfile on disk
14
+ */
15
+ export class Dockerfile {
16
+ // STATIC METHODS
17
+
18
+ /**
19
+ * Creates instances of class Dockerfile for all Dockerfiles in cwd
20
+ */
21
+ public static async readDockerfiles(managerRef: TsDockerManager): Promise<Dockerfile[]> {
22
+ const entries = await plugins.smartfs.directory(paths.cwd).filter('Dockerfile*').list();
23
+ const fileTree = entries
24
+ .filter(entry => entry.isFile)
25
+ .map(entry => plugins.path.join(paths.cwd, entry.name));
26
+
27
+ const readDockerfilesArray: Dockerfile[] = [];
28
+ logger.log('info', `found ${fileTree.length} Dockerfiles:`);
29
+ console.log(fileTree);
30
+
31
+ for (const dockerfilePath of fileTree) {
32
+ const myDockerfile = new Dockerfile(managerRef, {
33
+ filePath: dockerfilePath,
34
+ read: true,
35
+ });
36
+ readDockerfilesArray.push(myDockerfile);
37
+ }
38
+
39
+ return readDockerfilesArray;
40
+ }
41
+
42
+ /**
43
+ * Sorts Dockerfiles into a build order based on dependencies (topological sort)
44
+ */
45
+ public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise<Dockerfile[]> {
46
+ logger.log('info', 'Sorting Dockerfiles based on dependencies...');
47
+
48
+ // Map from cleanTag to Dockerfile instance for quick lookup
49
+ const tagToDockerfile = new Map<string, Dockerfile>();
50
+ dockerfiles.forEach((dockerfile) => {
51
+ tagToDockerfile.set(dockerfile.cleanTag, dockerfile);
52
+ });
53
+
54
+ // Build the dependency graph
55
+ const graph = new Map<Dockerfile, Dockerfile[]>();
56
+ dockerfiles.forEach((dockerfile) => {
57
+ const dependencies: Dockerfile[] = [];
58
+ const baseImage = dockerfile.baseImage;
59
+
60
+ // Check if the baseImage is among the local Dockerfiles
61
+ if (tagToDockerfile.has(baseImage)) {
62
+ const baseDockerfile = tagToDockerfile.get(baseImage)!;
63
+ dependencies.push(baseDockerfile);
64
+ dockerfile.localBaseImageDependent = true;
65
+ dockerfile.localBaseDockerfile = baseDockerfile;
66
+ }
67
+
68
+ graph.set(dockerfile, dependencies);
69
+ });
70
+
71
+ // Perform topological sort
72
+ const sortedDockerfiles: Dockerfile[] = [];
73
+ const visited = new Set<Dockerfile>();
74
+ const tempMarked = new Set<Dockerfile>();
75
+
76
+ const visit = (dockerfile: Dockerfile) => {
77
+ if (tempMarked.has(dockerfile)) {
78
+ throw new Error(`Circular dependency detected involving ${dockerfile.cleanTag}`);
79
+ }
80
+ if (!visited.has(dockerfile)) {
81
+ tempMarked.add(dockerfile);
82
+ const dependencies = graph.get(dockerfile) || [];
83
+ dependencies.forEach((dep) => visit(dep));
84
+ tempMarked.delete(dockerfile);
85
+ visited.add(dockerfile);
86
+ sortedDockerfiles.push(dockerfile);
87
+ }
88
+ };
89
+
90
+ try {
91
+ dockerfiles.forEach((dockerfile) => {
92
+ if (!visited.has(dockerfile)) {
93
+ visit(dockerfile);
94
+ }
95
+ });
96
+ } catch (error) {
97
+ logger.log('error', (error as Error).message);
98
+ throw error;
99
+ }
100
+
101
+ // Log the sorted order
102
+ sortedDockerfiles.forEach((dockerfile, index) => {
103
+ logger.log(
104
+ 'info',
105
+ `Build order ${index + 1}: ${dockerfile.cleanTag} with base image ${dockerfile.baseImage}`
106
+ );
107
+ });
108
+
109
+ return sortedDockerfiles;
110
+ }
111
+
112
+ /**
113
+ * Maps local Dockerfiles dependencies to the corresponding Dockerfile class instances
114
+ */
115
+ public static async mapDockerfiles(sortedDockerfileArray: Dockerfile[]): Promise<Dockerfile[]> {
116
+ sortedDockerfileArray.forEach((dockerfileArg) => {
117
+ if (dockerfileArg.localBaseImageDependent) {
118
+ sortedDockerfileArray.forEach((dockfile2: Dockerfile) => {
119
+ if (dockfile2.cleanTag === dockerfileArg.baseImage) {
120
+ dockerfileArg.localBaseDockerfile = dockfile2;
121
+ }
122
+ });
123
+ }
124
+ });
125
+ return sortedDockerfileArray;
126
+ }
127
+
128
+ /**
129
+ * Builds the corresponding real docker image for each Dockerfile class instance
130
+ */
131
+ public static async buildDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
132
+ for (const dockerfileArg of sortedArrayArg) {
133
+ await dockerfileArg.build();
134
+ }
135
+ return sortedArrayArg;
136
+ }
137
+
138
+ /**
139
+ * Tests all Dockerfiles by calling Dockerfile.test()
140
+ */
141
+ public static async testDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
142
+ for (const dockerfileArg of sortedArrayArg) {
143
+ await dockerfileArg.test();
144
+ }
145
+ return sortedArrayArg;
146
+ }
147
+
148
+ /**
149
+ * Returns a version for a docker file
150
+ * Dockerfile_latest -> latest
151
+ * Dockerfile_v1.0.0 -> v1.0.0
152
+ * Dockerfile -> latest
153
+ */
154
+ public static dockerFileVersion(
155
+ dockerfileInstanceArg: Dockerfile,
156
+ dockerfileNameArg: string
157
+ ): string {
158
+ let versionString: string;
159
+ const versionRegex = /Dockerfile_(.+)$/;
160
+ const regexResultArray = versionRegex.exec(dockerfileNameArg);
161
+ if (regexResultArray && regexResultArray.length === 2) {
162
+ versionString = regexResultArray[1];
163
+ } else {
164
+ versionString = 'latest';
165
+ }
166
+
167
+ // Replace ##version## placeholder with actual package version if available
168
+ if (dockerfileInstanceArg.managerRef?.projectInfo?.npm?.version) {
169
+ versionString = versionString.replace(
170
+ '##version##',
171
+ dockerfileInstanceArg.managerRef.projectInfo.npm.version
172
+ );
173
+ }
174
+
175
+ return versionString;
176
+ }
177
+
178
+ /**
179
+ * Extracts the base image from a Dockerfile content
180
+ * Handles ARG substitution for variable base images
181
+ */
182
+ public static dockerBaseImage(dockerfileContentArg: string): string {
183
+ const lines = dockerfileContentArg.split(/\r?\n/);
184
+ const args: { [key: string]: string } = {};
185
+
186
+ for (const line of lines) {
187
+ const trimmedLine = line.trim();
188
+
189
+ // Skip empty lines and comments
190
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
191
+ continue;
192
+ }
193
+
194
+ // Match ARG instructions
195
+ const argMatch = trimmedLine.match(/^ARG\s+([^\s=]+)(?:=(.*))?$/i);
196
+ if (argMatch) {
197
+ const argName = argMatch[1];
198
+ const argValue = argMatch[2] !== undefined ? argMatch[2] : process.env[argName] || '';
199
+ args[argName] = argValue;
200
+ continue;
201
+ }
202
+
203
+ // Match FROM instructions
204
+ const fromMatch = trimmedLine.match(/^FROM\s+(.+?)(?:\s+AS\s+[^\s]+)?$/i);
205
+ if (fromMatch) {
206
+ let baseImage = fromMatch[1].trim();
207
+
208
+ // Substitute variables in the base image name
209
+ baseImage = Dockerfile.substituteVariables(baseImage, args);
210
+
211
+ return baseImage;
212
+ }
213
+ }
214
+
215
+ throw new Error('No FROM instruction found in Dockerfile');
216
+ }
217
+
218
+ /**
219
+ * Substitutes variables in a string, supporting default values like ${VAR:-default}
220
+ */
221
+ private static substituteVariables(str: string, vars: { [key: string]: string }): string {
222
+ return str.replace(/\${([^}:]+)(:-([^}]+))?}/g, (_, varName, __, defaultValue) => {
223
+ if (vars[varName] !== undefined) {
224
+ return vars[varName];
225
+ } else if (defaultValue !== undefined) {
226
+ return defaultValue;
227
+ } else {
228
+ return '';
229
+ }
230
+ });
231
+ }
232
+
233
+ /**
234
+ * Returns the docker tag string for a given registry and repo
235
+ */
236
+ public static getDockerTagString(
237
+ managerRef: TsDockerManager,
238
+ registryArg: string,
239
+ repoArg: string,
240
+ versionArg: string,
241
+ suffixArg?: string
242
+ ): string {
243
+ // Determine whether the repo should be mapped according to the registry
244
+ const config = managerRef.config;
245
+ const mappedRepo = config.registryRepoMap?.[registryArg];
246
+ const repo = mappedRepo || repoArg;
247
+
248
+ // Determine whether the version contains a suffix
249
+ let version = versionArg;
250
+ if (suffixArg) {
251
+ version = versionArg + '_' + suffixArg;
252
+ }
253
+
254
+ const tagString = `${registryArg}/${repo}:${version}`;
255
+ return tagString;
256
+ }
257
+
258
+ /**
259
+ * Gets build args from environment variable mapping
260
+ */
261
+ public static async getDockerBuildArgs(managerRef: TsDockerManager): Promise<string> {
262
+ logger.log('info', 'checking for env vars to be supplied to the docker build');
263
+ let buildArgsString: string = '';
264
+ const config = managerRef.config;
265
+
266
+ if (config.buildArgEnvMap) {
267
+ for (const dockerArgKey of Object.keys(config.buildArgEnvMap)) {
268
+ const dockerArgOuterEnvVar = config.buildArgEnvMap[dockerArgKey];
269
+ logger.log(
270
+ 'note',
271
+ `docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"`
272
+ );
273
+ const targetValue = process.env[dockerArgOuterEnvVar];
274
+ if (targetValue) {
275
+ buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`;
276
+ }
277
+ }
278
+ }
279
+ return buildArgsString;
280
+ }
281
+
282
+ // INSTANCE PROPERTIES
283
+ public managerRef: TsDockerManager;
284
+ public filePath!: string;
285
+ public repo: string;
286
+ public version: string;
287
+ public cleanTag: string;
288
+ public buildTag: string;
289
+ public pushTag!: string;
290
+ public containerName: string;
291
+ public content!: string;
292
+ public baseImage: string;
293
+ public localBaseImageDependent: boolean;
294
+ public localBaseDockerfile!: Dockerfile;
295
+
296
+ constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
297
+ this.managerRef = managerRefArg;
298
+ this.filePath = options.filePath!;
299
+
300
+ // Build repo name from project info or directory name
301
+ const projectInfo = this.managerRef.projectInfo;
302
+ if (projectInfo?.npm?.name) {
303
+ // Use package name, removing scope if present
304
+ const packageName = projectInfo.npm.name.replace(/^@[^/]+\//, '');
305
+ this.repo = packageName;
306
+ } else {
307
+ // Fallback to directory name
308
+ this.repo = plugins.path.basename(paths.cwd);
309
+ }
310
+
311
+ this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(this.filePath).base);
312
+ this.cleanTag = this.repo + ':' + this.version;
313
+ this.buildTag = this.cleanTag;
314
+ this.containerName = 'dockerfile-' + this.version;
315
+
316
+ if (options.filePath && options.read) {
317
+ const fs = require('fs');
318
+ this.content = fs.readFileSync(plugins.path.resolve(options.filePath), 'utf-8');
319
+ } else if (options.fileContents) {
320
+ this.content = options.fileContents;
321
+ }
322
+
323
+ this.baseImage = Dockerfile.dockerBaseImage(this.content);
324
+ this.localBaseImageDependent = false;
325
+ }
326
+
327
+ /**
328
+ * Builds the Dockerfile
329
+ */
330
+ public async build(): Promise<void> {
331
+ logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
332
+ const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef);
333
+ const config = this.managerRef.config;
334
+
335
+ let buildCommand: string;
336
+
337
+ // Check if multi-platform build is needed
338
+ if (config.platforms && config.platforms.length > 1) {
339
+ // Multi-platform build using buildx
340
+ const platformString = config.platforms.join(',');
341
+ buildCommand = `docker buildx build --platform ${platformString} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
342
+
343
+ if (config.push) {
344
+ buildCommand += ' --push';
345
+ } else {
346
+ buildCommand += ' --load';
347
+ }
348
+ } else {
349
+ // Standard build
350
+ const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
351
+ buildCommand = `docker build --label="version=${versionLabel}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
352
+ }
353
+
354
+ const result = await smartshellInstance.exec(buildCommand);
355
+ if (result.exitCode !== 0) {
356
+ logger.log('error', `Build failed for ${this.cleanTag}`);
357
+ console.log(result.stdout);
358
+ throw new Error(`Build failed for ${this.cleanTag}`);
359
+ }
360
+
361
+ logger.log('ok', `Built ${this.cleanTag}`);
362
+ }
363
+
364
+ /**
365
+ * Pushes the Dockerfile to a registry
366
+ */
367
+ public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string): Promise<void> {
368
+ this.pushTag = Dockerfile.getDockerTagString(
369
+ this.managerRef,
370
+ dockerRegistryArg.registryUrl,
371
+ this.repo,
372
+ this.version,
373
+ versionSuffix
374
+ );
375
+
376
+ await smartshellInstance.exec(`docker tag ${this.buildTag} ${this.pushTag}`);
377
+ const pushResult = await smartshellInstance.exec(`docker push ${this.pushTag}`);
378
+
379
+ if (pushResult.exitCode !== 0) {
380
+ logger.log('error', `Push failed for ${this.pushTag}`);
381
+ throw new Error(`Push failed for ${this.pushTag}`);
382
+ }
383
+
384
+ // Get image digest
385
+ const inspectResult = await smartshellInstance.exec(
386
+ `docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`
387
+ );
388
+
389
+ if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) {
390
+ const imageDigest = inspectResult.stdout.split('@')[1]?.trim();
391
+ console.log(`The image ${this.pushTag} has digest ${imageDigest}`);
392
+ }
393
+
394
+ logger.log('ok', `Pushed ${this.pushTag}`);
395
+ }
396
+
397
+ /**
398
+ * Pulls the Dockerfile from a registry
399
+ */
400
+ public async pull(registryArg: DockerRegistry, versionSuffixArg?: string): Promise<void> {
401
+ const pullTag = Dockerfile.getDockerTagString(
402
+ this.managerRef,
403
+ registryArg.registryUrl,
404
+ this.repo,
405
+ this.version,
406
+ versionSuffixArg
407
+ );
408
+
409
+ await smartshellInstance.exec(`docker pull ${pullTag}`);
410
+ await smartshellInstance.exec(`docker tag ${pullTag} ${this.buildTag}`);
411
+
412
+ logger.log('ok', `Pulled and tagged ${pullTag} as ${this.buildTag}`);
413
+ }
414
+
415
+ /**
416
+ * Tests the Dockerfile by running a test script if it exists
417
+ */
418
+ public async test(): Promise<void> {
419
+ const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test');
420
+ const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh');
421
+
422
+ const fs = require('fs');
423
+ const testFileExists = fs.existsSync(testFile);
424
+
425
+ if (testFileExists) {
426
+ logger.log('info', `Running tests for ${this.cleanTag}`);
427
+
428
+ // Run tests in container
429
+ await smartshellInstance.exec(
430
+ `docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"`
431
+ );
432
+ await smartshellInstance.exec(`docker cp ${testFile} tsdocker_test_container:/tsdocker_test/test.sh`);
433
+ await smartshellInstance.exec(`docker commit tsdocker_test_container tsdocker_test_image`);
434
+
435
+ const testResult = await smartshellInstance.exec(
436
+ `docker run --entrypoint="bash" tsdocker_test_image -x /tsdocker_test/test.sh`
437
+ );
438
+
439
+ // Cleanup
440
+ await smartshellInstance.exec(`docker rm tsdocker_test_container`);
441
+ await smartshellInstance.exec(`docker rmi --force tsdocker_test_image`);
442
+
443
+ if (testResult.exitCode !== 0) {
444
+ throw new Error(`Tests failed for ${this.cleanTag}`);
445
+ }
446
+
447
+ logger.log('ok', `Tests passed for ${this.cleanTag}`);
448
+ } else {
449
+ logger.log('warn', `Skipping tests for ${this.cleanTag} because no test file was found at ${testFile}`);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Gets the ID of a built Docker image
455
+ */
456
+ public async getId(): Promise<string> {
457
+ const result = await smartshellInstance.exec(
458
+ 'docker inspect --type=image --format="{{.Id}}" ' + this.buildTag
459
+ );
460
+ return result.stdout.trim();
461
+ }
462
+ }