@alchemy.run/docker 2.0.0-beta.85 → 2.0.0-beta.86

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.
Files changed (93) hide show
  1. package/README.md +16 -0
  2. package/lib/Bootstrap.d.ts.map +1 -1
  3. package/lib/Bootstrap.js +2 -1
  4. package/lib/Bootstrap.js.map +1 -1
  5. package/lib/BuildCache.d.ts +87 -0
  6. package/lib/BuildCache.d.ts.map +1 -0
  7. package/lib/BuildCache.js +39 -0
  8. package/lib/BuildCache.js.map +1 -0
  9. package/lib/BuildHash.d.ts +9 -1
  10. package/lib/BuildHash.d.ts.map +1 -1
  11. package/lib/BuildHash.js +51 -19
  12. package/lib/BuildHash.js.map +1 -1
  13. package/lib/BuildPlan.d.ts +202 -0
  14. package/lib/BuildPlan.d.ts.map +1 -0
  15. package/lib/BuildPlan.js +514 -0
  16. package/lib/BuildPlan.js.map +1 -0
  17. package/lib/Container.d.ts +24 -1
  18. package/lib/Container.d.ts.map +1 -1
  19. package/lib/Container.js +14 -0
  20. package/lib/Container.js.map +1 -1
  21. package/lib/Context.d.ts +20 -1
  22. package/lib/Context.d.ts.map +1 -1
  23. package/lib/Context.js +15 -0
  24. package/lib/Context.js.map +1 -1
  25. package/lib/Docker.d.ts +33 -0
  26. package/lib/Docker.d.ts.map +1 -1
  27. package/lib/Docker.js +23 -2
  28. package/lib/Docker.js.map +1 -1
  29. package/lib/Dockerfile.d.ts +0 -2
  30. package/lib/Dockerfile.d.ts.map +1 -1
  31. package/lib/Dockerfile.js.map +1 -1
  32. package/lib/DockerfileParser.d.ts +125 -0
  33. package/lib/DockerfileParser.d.ts.map +1 -0
  34. package/lib/DockerfileParser.js +1040 -0
  35. package/lib/DockerfileParser.js.map +1 -0
  36. package/lib/HealthcheckCommand.d.ts.map +1 -1
  37. package/lib/HealthcheckCommand.js.map +1 -1
  38. package/lib/Image.d.ts +40 -12
  39. package/lib/Image.d.ts.map +1 -1
  40. package/lib/Image.js +41 -9
  41. package/lib/Image.js.map +1 -1
  42. package/lib/ImageArchive.d.ts +41 -0
  43. package/lib/ImageArchive.d.ts.map +1 -0
  44. package/lib/ImageArchive.js +54 -0
  45. package/lib/ImageArchive.js.map +1 -0
  46. package/lib/ImageSource.d.ts +100 -11
  47. package/lib/ImageSource.d.ts.map +1 -1
  48. package/lib/ImageSource.js +219 -73
  49. package/lib/ImageSource.js.map +1 -1
  50. package/lib/Network.d.ts +175 -3
  51. package/lib/Network.d.ts.map +1 -1
  52. package/lib/Network.js +296 -8
  53. package/lib/Network.js.map +1 -1
  54. package/lib/Providers.d.ts.map +1 -1
  55. package/lib/Providers.js.map +1 -1
  56. package/lib/Registry.d.ts.map +1 -1
  57. package/lib/Registry.js.map +1 -1
  58. package/lib/RegistryError.d.ts.map +1 -1
  59. package/lib/RegistryError.js.map +1 -1
  60. package/lib/RemoteImage.d.ts.map +1 -1
  61. package/lib/RemoteImage.js.map +1 -1
  62. package/lib/Service.d.ts.map +1 -1
  63. package/lib/Service.js.map +1 -1
  64. package/lib/ServiceImage.d.ts.map +1 -1
  65. package/lib/ServiceImage.js.map +1 -1
  66. package/lib/ShellWord.d.ts +28 -0
  67. package/lib/ShellWord.d.ts.map +1 -0
  68. package/lib/ShellWord.js +301 -0
  69. package/lib/ShellWord.js.map +1 -0
  70. package/lib/Swarm.d.ts.map +1 -1
  71. package/lib/Swarm.js.map +1 -1
  72. package/lib/Volume.d.ts +24 -1
  73. package/lib/Volume.d.ts.map +1 -1
  74. package/lib/Volume.js +16 -0
  75. package/lib/Volume.js.map +1 -1
  76. package/lib/index.d.ts.map +1 -1
  77. package/lib/index.js.map +1 -1
  78. package/package.json +9 -9
  79. package/src/Bootstrap.ts +2 -0
  80. package/src/BuildCache.ts +111 -0
  81. package/src/BuildHash.ts +73 -24
  82. package/src/BuildPlan.ts +758 -0
  83. package/src/Container.ts +31 -1
  84. package/src/Context.ts +27 -1
  85. package/src/Docker.ts +56 -2
  86. package/src/Dockerfile.ts +0 -2
  87. package/src/DockerfileParser.ts +1152 -0
  88. package/src/Image.ts +45 -14
  89. package/src/ImageArchive.ts +77 -0
  90. package/src/ImageSource.ts +322 -103
  91. package/src/Network.ts +432 -10
  92. package/src/ShellWord.ts +326 -0
  93. package/src/Volume.ts +33 -1
package/src/BuildHash.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as crypto from "node:crypto";
10
+ import { dotAlchemyDirectory } from "alchemy/AlchemyContext";
10
11
  import {
11
12
  type IgnoreRules,
12
13
  listFileSystemDirectory,
@@ -14,11 +15,11 @@ import {
14
15
  parseIgnoreRules,
15
16
  walkIgnoring,
16
17
  } from "alchemy/Util/Ignore";
18
+ import { initialCwd } from "alchemy/Util/Node";
17
19
  import * as Data from "effect/Data";
18
20
  import * as Effect from "effect/Effect";
19
21
  import * as FileSystem from "effect/FileSystem";
20
22
  import * as Path from "effect/Path";
21
- import * as Result from "effect/Result";
22
23
  import * as Stream from "effect/Stream";
23
24
 
24
25
  export class BuildHashInvalidProps extends Data.TaggedError("BuildHashInvalidProps")<{
@@ -149,6 +150,25 @@ export const selectDockerBuildContext = Effect.fn(function* (
149
150
  } satisfies DockerBuildContextSelection;
150
151
  });
151
152
 
153
+ /**
154
+ * The context-relative path of Alchemy's `.alchemy` directory when it lies
155
+ * strictly inside `context`, otherwise `undefined`.
156
+ */
157
+ const containedRuntimeDirectory = Effect.fn(function* (context: string) {
158
+ const path = yield* Path.Path;
159
+ const runtime = path.resolve(initialCwd, yield* dotAlchemyDirectory);
160
+ const relative = normalizeRelativePath(path.relative(context, runtime));
161
+ return relative === "" ||
162
+ relative === ".." ||
163
+ relative.startsWith("../") ||
164
+ path.isAbsolute(relative)
165
+ ? undefined
166
+ : relative;
167
+ });
168
+
169
+ /** Files below this size are read in one call instead of streamed. */
170
+ const SMALL_FILE_BYTES = 1024 * 1024;
171
+
152
172
  /**
153
173
  * Hash a Docker build context together with the Dockerfile, platform, and
154
174
  * build arguments. In `effective` mode, the selected `.dockerignore` is
@@ -158,6 +178,15 @@ export const selectDockerBuildContext = Effect.fn(function* (
158
178
  export const hashDockerBuildInputs = Effect.fn(function* (
159
179
  source: DockerBuildSource,
160
180
  mode: DockerBuildHashMode,
181
+ options: {
182
+ /**
183
+ * Directory names left out of the hash at any depth, on top of
184
+ * `.dockerignore` and `.alchemy` — e.g. `.git` and `node_modules` for a
185
+ * dev watcher that must not restart on git's index writes or re-hash
186
+ * installed dependencies on every edit.
187
+ */
188
+ readonly excludeNames?: ReadonlyArray<string>;
189
+ } = {},
161
190
  ) {
162
191
  const fs = yield* FileSystem.FileSystem;
163
192
  const path = yield* Path.Path;
@@ -197,15 +226,35 @@ export const hashDockerBuildInputs = Effect.fn(function* (
197
226
  (entry): entry is string =>
198
227
  entry !== undefined && entry !== ".." && !entry.startsWith("../") && !path.isAbsolute(entry),
199
228
  );
229
+ // Alchemy writes run logs and local state to `.alchemy` on every run. When a
230
+ // context contains it (e.g. `context: "."` at the project root), hashing it
231
+ // would give the image a new tag on every deploy. A context inside
232
+ // `.alchemy` (a generated one) is still hashed in full.
233
+ const runtimeDirectory = yield* containedRuntimeDirectory(context);
234
+ const excludedNames = new Set(options.excludeNames ?? []);
235
+ const listDirectory = yield* listFileSystemDirectory(context);
200
236
  // Walk one level at a time so an excluded directory is discarded before
201
237
  // its contents are read; entries come back sorted, preserving hash order.
202
238
  const entries = yield* walkIgnoring({
203
- list: yield* listFileSystemDirectory(context),
239
+ list:
240
+ runtimeDirectory === undefined && excludedNames.size === 0
241
+ ? listDirectory
242
+ : (directory: string) =>
243
+ listDirectory(directory).pipe(
244
+ Effect.map((children) =>
245
+ children.filter(
246
+ (child) =>
247
+ !(child.type === "Directory" && excludedNames.has(child.name)) &&
248
+ (directory.length === 0 ? child.name : `${directory}/${child.name}`) !==
249
+ runtimeDirectory,
250
+ ),
251
+ ),
252
+ ),
204
253
  rules: dockerignore?.rules,
205
254
  force: forcedPaths,
206
255
  });
207
256
 
208
- for (const { path: entry } of entries) {
257
+ for (const { path: entry, type } of entries) {
209
258
  // The ignore file and Dockerfile contents are hashed above; excluded
210
259
  // entries were only walked so re-included descendants could be found.
211
260
  if (
@@ -216,20 +265,14 @@ export const hashDockerBuildInputs = Effect.fn(function* (
216
265
  }
217
266
 
218
267
  const fullPath = path.join(context, entry);
219
- const hashedEntry = entry;
220
- // FileSystem.stat follows symbolic links, so probe the link itself first.
221
- // Docker preserves the link in the build context; hashing the target file
222
- // would miss retargets between files with identical contents and metadata.
223
- const link = yield* Effect.result(fs.readLink(fullPath));
224
- if (Result.isSuccess(link)) {
268
+ // Docker preserves a symbolic link in the build context; hashing the
269
+ // target file would miss retargets between files with identical
270
+ // contents and metadata. The walk reports the entry's own type, so only
271
+ // links are read as links.
272
+ if (type === "SymbolicLink") {
273
+ const target = yield* fs.readLink(fullPath);
225
274
  yield* Effect.sync(() =>
226
- hasher.update(
227
- `${JSON.stringify({
228
- path: hashedEntry,
229
- type: "SymbolicLink",
230
- target: link.success,
231
- })}\0`,
232
- ),
275
+ hasher.update(`${JSON.stringify({ path: entry, type: "SymbolicLink", target })}\0`),
233
276
  );
234
277
  continue;
235
278
  }
@@ -242,7 +285,7 @@ export const hashDockerBuildInputs = Effect.fn(function* (
242
285
  yield* Effect.sync(() =>
243
286
  hasher.update(
244
287
  `${JSON.stringify({
245
- path: hashedEntry,
288
+ path: entry,
246
289
  type: info.type,
247
290
  mode: info.mode & 0o7777,
248
291
  size: info.type === "File" ? String(info.size) : undefined,
@@ -251,13 +294,19 @@ export const hashDockerBuildInputs = Effect.fn(function* (
251
294
  );
252
295
 
253
296
  if (info.type === "File") {
254
- yield* fs.stream(fullPath).pipe(
255
- Stream.runForEach((chunk) =>
256
- Effect.sync(() => {
257
- hasher.update(chunk);
258
- }),
259
- ),
260
- );
297
+ // Read small files whole (one call); stream large ones so memory stays flat.
298
+ if (info.size < SMALL_FILE_BYTES) {
299
+ const bytes = yield* fs.readFile(fullPath);
300
+ yield* Effect.sync(() => hasher.update(bytes));
301
+ } else {
302
+ yield* fs.stream(fullPath).pipe(
303
+ Stream.runForEach((chunk) =>
304
+ Effect.sync(() => {
305
+ hasher.update(chunk);
306
+ }),
307
+ ),
308
+ );
309
+ }
261
310
  }
262
311
  }
263
312