From 45e20ebad53717e7f856cd26966f84a466f086da Mon Sep 17 00:00:00 2001 From: stack72 Date: Thu, 26 Feb 2026 17:35:51 +0000 Subject: [PATCH] fix: prevent deno.lock creation during extension bundling (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add --no-lock to the deno bundle subprocess to prevent Deno from auto-discovering and creating/modifying a lockfile in the user's CWD. Without this flag, every time swamp bundles an extension model at startup, Deno's default lockfile behavior creates or updates a deno.lock in whatever directory the user runs swamp from — polluting their repo with an unexpected file. The --no-lock flag disables lockfile auto-discovery entirely, which is the correct behavior for swamp's use case: extensions are bundled as a build step, not as a user-facing dependency install. npm resolution still works normally — packages are fetched from the registry and inlined into the bundle. The only tradeoff is that without a lockfile, version resolution is not pinned across runs, which is addressed by updating the extension model skill docs to require explicit version pinning on npm imports (e.g., npm:[email protected] instead of npm:lodash-es). Changes: - Add --no-lock flag to deno bundle args in bundle.ts - Add test verifying npm imports resolve without creating deno.lock - Add version pinning guidance to extension model skill docs - Update example imports to use pinned versions Co-authored-by: Blake Irvin Co-Authored-By: Claude Opus 4.6 --- .claude/skills/swamp-extension-model/SKILL.md | 10 ++-- .../references/examples.md | 6 +-- src/domain/models/bundle.ts | 1 + src/domain/models/bundle_test.ts | 46 +++++++++++++++++++ 4 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.claude/skills/swamp-extension-model/SKILL.md b/.claude/skills/swamp-extension-model/SKILL.md index 6bffe6a6..e83e7076 100644 --- a/.claude/skills/swamp-extension-model/SKILL.md +++ b/.claude/skills/swamp-extension-model/SKILL.md @@ -311,9 +311,13 @@ Files are classified by export name: `export const model` defines new types, Deno-compatible import (`npm:`, `jsr:`, `https://`) can also be used — swamp bundles all dependencies automatically (see [references/examples.md](references/examples.md#using-external-dependencies)) -3. **Type naming**: Use `@/` format (e.g., `@user/my-model`) -4. **No type annotations**: Avoid TypeScript types in execute parameters -5. **File naming**: Use snake_case (`my_model.ts`) +3. **Pin npm versions**: Always pin explicit versions for npm imports (e.g., + `npm:[email protected]`, not `npm:lodash-es`). Swamp does not use a lockfile + during bundling, so unpinned versions may resolve differently across runs. + `npm:zod@4` is the one exception — it is externalized and provided by swamp. +4. **Type naming**: Use `@/` format (e.g., `@user/my-model`) +5. **No type annotations**: Avoid TypeScript types in execute parameters +6. **File naming**: Use snake_case (`my_model.ts`) ## Namespace Rules diff --git a/.claude/skills/swamp-extension-model/references/examples.md b/.claude/skills/swamp-extension-model/references/examples.md index 17f40e4f..e35beda6 100644 --- a/.claude/skills/swamp-extension-model/references/examples.md +++ b/.claude/skills/swamp-extension-model/references/examples.md @@ -25,7 +25,7 @@ with swamp to preserve schema `instanceof` checks. ```typescript // extensions/models/text_analyzer.ts import { z } from "npm:zod@4"; -import { countBy, sortBy, words } from "npm:lodash-es"; +import { countBy, sortBy, words } from "npm:[email protected]"; const GlobalArgsSchema = z.object({ text: z.string().describe("Text to analyze"), @@ -93,8 +93,8 @@ export const model = { | Import | Bundled? | Notes | | ---------------------------------------- | -------- | -------------------------------- | | `npm:zod@4` | No | Shared with swamp (externalized) | -| `npm:lodash-es` | Yes | Resolved and inlined | -| `npm:@aws-sdk/client-s3` | Yes | Resolved and inlined | +| `npm:[email protected]` | Yes | Resolved and inlined | +| `npm:@aws-sdk/[email protected]` | Yes | Resolved and inlined | | `jsr:@std/path` | Yes | Resolved and inlined | | `https://deno.land/[email protected]/async/..` | Yes | Resolved and inlined | | Any other Deno-compatible import | Yes | Resolved and inlined | diff --git a/src/domain/models/bundle.ts b/src/domain/models/bundle.ts index 4efa0ac9..7071778e 100644 --- a/src/domain/models/bundle.ts +++ b/src/domain/models/bundle.ts @@ -47,6 +47,7 @@ export async function bundleExtension( const command = new Deno.Command(denoPath, { args: [ "bundle", + "--no-lock", "--external", "npm:zod@4", "--external", diff --git a/src/domain/models/bundle_test.ts b/src/domain/models/bundle_test.ts index b29f5679..a4b2da18 100644 --- a/src/domain/models/bundle_test.ts +++ b/src/domain/models/bundle_test.ts @@ -96,6 +96,52 @@ export const schema = z.object({ name: z.string() }); }); }); +Deno.test("bundleExtension resolves npm imports without creating deno.lock", async () => { + const tsCode = ` +import { z } from "npm:zod@4"; +import { parse, stringify } from "npm:[email protected]"; + +const ConfigSchema = z.object({ + data: z.string(), +}); + +export const model = { + type: "@test/yaml-model", + version: "2026.01.01.1", + schema: ConfigSchema, + transform: (input: string) => { + const parsed = parse(input); + return stringify(parsed); + }, +}; +`; + + const dir = await Deno.makeTempDir({ prefix: "swamp_bundle_test_" }); + const path = join(dir, "test_ext.ts"); + await Deno.writeTextFile(path, tsCode); + try { + const js = await bundleExtension(path, DENO_PATH); + + // Bundle should succeed and contain inlined yaml library code + assertEquals(js.length > 0, true); + + // No deno.lock should be created in the source directory + let lockExists = true; + try { + await Deno.stat(join(dir, "deno.lock")); + } catch { + lockExists = false; + } + assertEquals( + lockExists, + false, + "deno.lock should not be created in the source directory", + ); + } finally { + await Deno.remove(dir, { recursive: true }); + } +}); + Deno.test("bundleExtension produces importable module with working zod instanceof", async () => { const tsCode = ` import { z } from "npm:zod@4";