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";