diff --git a/.dagger/modules/e2e/init.dang b/.dagger/modules/e2e/init.dang index b7cd63a..ccfb298 100644 --- a/.dagger/modules/e2e/init.dang +++ b/.dagger/modules/e2e/init.dang @@ -142,7 +142,7 @@ type InitChecks { Asserts.stringContains(mergedPkg, "\"name\": \"user-pkg\"", "user-authored name key should be preserved") Asserts.stringContains(mergedPkg, "\"version\": \"1.2.3\"", "user-authored version key should be preserved") Asserts.stringContains(mergedPkg, "\"build\": \"tsc\"", "user-authored script should be preserved") - Asserts.stringContains(mergedPkg, "\"type\":\"module\"", "Dagger-required type=module should be added") + Asserts.stringContains(mergedPkg, "\"type\": \"module\"", "Dagger-required type=module should be added") Asserts.stringNotContains( mergedPkg, "\"@dagger.io/dagger\"", @@ -155,7 +155,7 @@ type InitChecks { Asserts.stringContains(mergedTsConfig, "\"@user/lib\"", "user-authored path mapping should be preserved") Asserts.stringContains( mergedTsConfig, - "\"experimentalDecorators\":true", + "\"experimentalDecorators\": true", "Dagger-required experimentalDecorators should be added", ) Asserts.stringContains(mergedTsConfig, "\"@dagger.io/dagger\"", "Dagger SDK path mapping should be added") @@ -177,10 +177,10 @@ type InitChecks { Asserts.stringContains(mergedDeno, "\"@user/lib\"", "user-authored deno import should be preserved") Asserts.stringContains(mergedDeno, "\"kv\"", "user-authored unstable flag should be preserved") Asserts.stringContains(mergedDeno, "\"@dagger.io/dagger\"", "Dagger SDK import should be added") - Asserts.stringContains(mergedDeno, "\"nodeModulesDir\":\"auto\"", "Dagger-required nodeModulesDir should be added") + Asserts.stringContains(mergedDeno, "\"nodeModulesDir\": \"auto\"", "Dagger-required nodeModulesDir should be added") Asserts.stringContains( mergedDeno, - "\"experimentalDecorators\":true", + "\"experimentalDecorators\": true", "Dagger-required experimentalDecorators should be added", ) Asserts.stringContains(mergedDeno, "\"bare-node-builtins\"", "Dagger-required unstable flag should be appended") diff --git a/helpers/config-updator/config-updator b/helpers/config-updator/config-updator new file mode 100755 index 0000000..299aad8 Binary files /dev/null and b/helpers/config-updator/config-updator differ diff --git a/helpers/config-updator/main.go b/helpers/config-updator/main.go index 03c67b2..243a600 100644 --- a/helpers/config-updator/main.go +++ b/helpers/config-updator/main.go @@ -82,13 +82,12 @@ func run(args []string) error { return fmt.Errorf("%s: %w", subcommand, err) } - out := []byte(updated) - // Client config is emitted fresh into a scoped package, so pretty-print it - // (indented, key order preserved) instead of a single minified line. Module - // variants edit user files in place and keep sjson's format-preserving output. - if strings.HasPrefix(subcommand, "client-") { - out = pretty.PrettyOptions(out, &pretty.Options{Indent: " "}) - } + // Indent every config we write, key order preserved. sjson edits in place, + // which reads as "preserve the user's formatting" but only holds for the + // parts it does not touch: keys it adds are appended compactly, and a file + // created from scratch comes out as a single line. Committed config that + // people read and edit is worth a whole-file reformat. + out := pretty.PrettyOptions([]byte(updated), &pretty.Options{Indent: " "}) return os.WriteFile(outputPath, out, 0o644) } @@ -114,6 +113,15 @@ func updatePackageJSON(packageJSON string) (string, error) { return "", fmt.Errorf("set type=module: %w", err) } + // Pin typescript unless the module already chose a version. The runtime + // mounts its own prebuilt copy — and skips dependency installation for an + // otherwise dependency-free module — only when the pin matches the engine's + // default, so drifting from it silently turns every call into an install. + packageJSON, err = setIfNotExists(packageJSON, "dependencies.typescript", defaultTypeScriptVersion) + if err != nil { + return "", fmt.Errorf("set typescript dependency: %w", err) + } + // Remove legacy in-tree @dagger.io/dagger deps so we transition cleanly to // the engine-managed bundle. Matches dagger/dagger UpdatePackageJSONForModule. for _, key := range []string{ @@ -307,7 +315,14 @@ func updateTSConfig(tsConfig string) (string, error) { } func updateDenoConfig(denoConfig string) (string, error) { - denoConfig, err := sjson.Set(denoConfig, "nodeModulesDir", "auto") + // Deno resolves dependencies through this map rather than node_modules, so + // the compiler the module's own code needs has to be declared here. + denoConfig, err := setIfNotExists(denoConfig, "imports.typescript", "npm:typescript@"+defaultTypeScriptVersion) + if err != nil { + return "", fmt.Errorf("set typescript import: %w", err) + } + + denoConfig, err = sjson.Set(denoConfig, "nodeModulesDir", "auto") if err != nil { return "", fmt.Errorf("set nodeModulesDir: %w", err) } diff --git a/helpers/config-updator/main_test.go b/helpers/config-updator/main_test.go index 85c9c9e..23335fc 100644 --- a/helpers/config-updator/main_test.go +++ b/helpers/config-updator/main_test.go @@ -18,7 +18,25 @@ func TestUpdatePackageJSON(t *testing.T) { { name: "empty package.json", packageJSON: `{}`, - expected: `{"type": "module"}`, + expected: `{"type": "module", "dependencies": {"typescript": "5.9.3"}}`, + }, + { + // The runtime mounts its prebuilt compiler only when the pin matches + // its default, so a module that chose its own version keeps it and + // accepts the install rather than being silently retargeted. + name: "a user's own typescript pin is preserved", + packageJSON: `{ + "type": "module", + "dependencies": { + "typescript": "5.4.0" + } +}`, + expected: `{ + "type": "module", + "dependencies": { + "typescript": "5.4.0" + } +}`, }, { name: "package.json with local dagger dependency is stripped", @@ -87,13 +105,16 @@ func TestUpdatePackageJSON(t *testing.T) { "scripts": { "build": "tsc" }, - "type": "module" + "type": "module", + "dependencies": { + "typescript": "5.9.3" + } }`, }, { - name: "type=module already set is a no-op", + name: "type=module already set still gains the typescript pin", packageJSON: `{"type": "module"}`, - expected: `{"type": "module"}`, + expected: `{"type": "module", "dependencies": {"typescript": "5.9.3"}}`, }, } { t.Run(tc.name, func(t *testing.T) { @@ -216,6 +237,7 @@ func TestUpdateDenoConfig(t *testing.T) { denoConfig: `{}`, expected: `{ "imports": { + "typescript": "npm:typescript@5.9.3", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" }, @@ -235,6 +257,7 @@ func TestUpdateDenoConfig(t *testing.T) { name: "deno.json with dagger imports already set is idempotent", denoConfig: `{ "imports": { + "typescript": "npm:typescript@5.9.3", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" }, @@ -251,6 +274,7 @@ func TestUpdateDenoConfig(t *testing.T) { }`, expected: `{ "imports": { + "typescript": "npm:typescript@5.9.3", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" }, @@ -273,6 +297,7 @@ func TestUpdateDenoConfig(t *testing.T) { }`, expected: `{ "imports": { + "typescript": "npm:typescript@5.9.3", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" }, @@ -287,6 +312,33 @@ func TestUpdateDenoConfig(t *testing.T) { "node-globals", "byonm" ] +}`, + }, + { + // Deno has no node_modules to fall back on, so the compiler has to be + // declared here — but a user who picked a version keeps it. + name: "a user's own typescript import is preserved", + denoConfig: `{ + "imports": { + "typescript": "npm:typescript@5.4.0" + } +}`, + expected: `{ + "imports": { + "typescript": "npm:typescript@5.4.0", + "@dagger.io/dagger": "./sdk/index.ts", + "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" + }, + "nodeModulesDir": "auto", + "compilerOptions": { + "experimentalDecorators": true + }, + "unstable": [ + "bare-node-builtins", + "sloppy-imports", + "node-globals", + "byonm" + ] }`, }, { @@ -304,6 +356,7 @@ func TestUpdateDenoConfig(t *testing.T) { "dev": "deno run main.ts" }, "imports": { + "typescript": "npm:typescript@5.9.3", "@user/lib": "./src/lib.ts", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" @@ -329,6 +382,7 @@ func TestUpdateDenoConfig(t *testing.T) { expected: `{ "url": "https://foo/bar/baz.html", "imports": { + "typescript": "npm:typescript@5.9.3", "@dagger.io/dagger": "./sdk/index.ts", "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" },