|
1 | 1 | import { assertEquals } from "jsr:@std/assert@^1.0.0"; |
| 2 | +import { fromFileUrl } from "jsr:@std/path@^1.0.0/from-file-url"; |
2 | 3 | import { join } from "node:path"; |
3 | 4 | import { pathToFileURL } from "node:url"; |
4 | 5 |
|
5 | 6 | const workspaceRoot = new URL(".", import.meta.url); |
6 | | -const workspacePath = workspaceRoot.pathname; |
| 7 | +const workspacePath = fromFileUrl(workspaceRoot); |
| 8 | +const packagingRunPermissions = await Promise.all([ |
| 9 | + Deno.permissions.query({ name: "run", command: "deno" }), |
| 10 | + Deno.permissions.query({ name: "run", command: "node" }), |
| 11 | + Deno.permissions.query({ |
| 12 | + name: "run", |
| 13 | + command: Deno.build.os === "windows" ? "where" : "which", |
| 14 | + }), |
| 15 | + Deno.permissions.query({ name: "run", command: "bun" }), |
| 16 | +]); |
| 17 | +const packagingRunPermissionGranted = packagingRunPermissions.every( |
| 18 | + (permission, index) => index === 3 || permission.state === "granted", |
| 19 | +); |
| 20 | +const bunRunPermissionGranted = packagingRunPermissions[3]?.state === "granted"; |
7 | 21 |
|
8 | 22 | const decodeText = (value: Uint8Array): string => |
9 | 23 | new TextDecoder().decode(value); |
@@ -40,91 +54,114 @@ const run = async ( |
40 | 54 | }; |
41 | 55 | }; |
42 | 56 |
|
43 | | -Deno.test("built npm package loads in node through the published ESM entrypoint", async () => { |
44 | | - const build = await run("deno", ["task", "build"]); |
45 | | - assertEquals(build.code, 0, build.stderr || build.stdout); |
| 57 | +Deno.test({ |
| 58 | + name: "built npm package loads in node through the published ESM entrypoint", |
| 59 | + ignore: !packagingRunPermissionGranted, |
| 60 | + fn: async () => { |
| 61 | + const build = await run("deno", ["task", "build"]); |
| 62 | + assertEquals(build.code, 0, build.stderr || build.stdout); |
46 | 63 |
|
47 | | - const builtPackage = JSON.parse( |
48 | | - await Deno.readTextFile(join(workspacePath, "dist/package.json")), |
49 | | - ) as { |
50 | | - dependencies?: Record<string, string>; |
51 | | - devDependencies?: Record<string, string>; |
52 | | - }; |
53 | | - assertEquals( |
54 | | - builtPackage.dependencies?.cosmiconfig, |
55 | | - "^9.0.0", |
56 | | - "generated npm package must declare cosmiconfig for runtime config loading", |
57 | | - ); |
58 | | - assertEquals( |
59 | | - typeof builtPackage.devDependencies?.["@types/node"], |
60 | | - "string", |
61 | | - "generated npm package must declare Node typings for dnt typecheck", |
62 | | - ); |
| 64 | + const builtPackage = JSON.parse( |
| 65 | + await Deno.readTextFile(join(workspacePath, "dist/package.json")), |
| 66 | + ) as { |
| 67 | + dependencies?: Record<string, string>; |
| 68 | + devDependencies?: Record<string, string>; |
| 69 | + }; |
| 70 | + const builtConfig = await Deno.readTextFile( |
| 71 | + join(workspacePath, "dist/esm/src/config.js"), |
| 72 | + ); |
| 73 | + assertEquals( |
| 74 | + builtPackage.dependencies?.cosmiconfig, |
| 75 | + "^9.0.0", |
| 76 | + "generated npm package must declare cosmiconfig for runtime config loading", |
| 77 | + ); |
| 78 | + assertEquals( |
| 79 | + typeof builtPackage.devDependencies?.["@types/node"], |
| 80 | + "string", |
| 81 | + "generated npm package must declare Node typings for dnt typecheck", |
| 82 | + ); |
| 83 | + assertEquals( |
| 84 | + builtConfig.includes("import-meta-ponyfill-esmodule"), |
| 85 | + false, |
| 86 | + "generated config loader should not depend on DNT import-meta ponyfill", |
| 87 | + ); |
63 | 88 |
|
64 | | - const tempDir = await Deno.makeTempDir(); |
65 | | - try { |
66 | | - const esmRunnerPath = join(tempDir, "load-esm.mjs"); |
67 | | - const bunRunnerPath = join(tempDir, "load-bun.mjs"); |
68 | | - const esmEntrypoint = |
69 | | - pathToFileURL(join(workspacePath, "dist/esm/mod.js")).href; |
70 | | - const packageDir = join(tempDir, "node_modules", "opencode-graphiti"); |
71 | | - const isolatedHome = join(tempDir, "home"); |
72 | | - const isolatedConfig = join(isolatedHome, ".config", "opencode"); |
| 89 | + const tempDir = await Deno.makeTempDir(); |
| 90 | + try { |
| 91 | + let optionalOpenCodePath: string | undefined; |
| 92 | + try { |
| 93 | + optionalOpenCodePath = Deno.env.get("OPENCODE_BIN") ?? undefined; |
| 94 | + } catch { |
| 95 | + optionalOpenCodePath = undefined; |
| 96 | + } |
73 | 97 |
|
74 | | - await Deno.mkdir(join(tempDir, "node_modules"), { recursive: true }); |
75 | | - await Deno.mkdir(isolatedConfig, { recursive: true }); |
76 | | - await Deno.symlink(join(workspacePath, "dist"), packageDir, { |
77 | | - type: "dir", |
78 | | - }); |
| 98 | + const esmRunnerPath = join(tempDir, "load-esm.mjs"); |
| 99 | + const bunRunnerPath = join(tempDir, "load-bun.mjs"); |
| 100 | + const esmEntrypoint = |
| 101 | + pathToFileURL(join(workspacePath, "dist/esm/mod.js")).href; |
| 102 | + const packageDir = join(tempDir, "node_modules", "opencode-graphiti"); |
| 103 | + const isolatedHome = join(tempDir, "home"); |
| 104 | + const isolatedConfig = join(isolatedHome, ".config", "opencode"); |
79 | 105 |
|
80 | | - await Deno.writeTextFile( |
81 | | - esmRunnerPath, |
82 | | - `import * as plugin from ${ |
83 | | - JSON.stringify(esmEntrypoint) |
84 | | - };\nconsole.log(JSON.stringify(Object.keys(plugin).sort()));\n`, |
85 | | - ); |
86 | | - await Deno.writeTextFile( |
87 | | - bunRunnerPath, |
88 | | - 'import * as plugin from "opencode-graphiti";\n' + |
89 | | - "console.log(JSON.stringify(Object.keys(plugin).sort()));\n", |
90 | | - ); |
| 106 | + await Deno.mkdir(join(tempDir, "node_modules"), { recursive: true }); |
| 107 | + await Deno.mkdir(isolatedConfig, { recursive: true }); |
| 108 | + await Deno.symlink(join(workspacePath, "dist"), packageDir, { |
| 109 | + type: "dir", |
| 110 | + }); |
91 | 111 |
|
92 | | - const esmLoad = await run("node", [esmRunnerPath]); |
93 | | - assertEquals(esmLoad.code, 0, esmLoad.stderr || esmLoad.stdout); |
94 | | - assertEquals(esmLoad.stdout.trim(), '["graphiti"]'); |
| 112 | + await Deno.writeTextFile( |
| 113 | + esmRunnerPath, |
| 114 | + `import * as plugin from ${ |
| 115 | + JSON.stringify(esmEntrypoint) |
| 116 | + };\nconsole.log(JSON.stringify(Object.keys(plugin).sort()));\n`, |
| 117 | + ); |
| 118 | + await Deno.writeTextFile( |
| 119 | + bunRunnerPath, |
| 120 | + 'import * as plugin from "opencode-graphiti";\n' + |
| 121 | + "console.log(JSON.stringify(Object.keys(plugin).sort()));\n", |
| 122 | + ); |
95 | 123 |
|
96 | | - if (await commandExists("bun")) { |
97 | | - const bunLoad = await run("bun", [bunRunnerPath], tempDir); |
98 | | - assertEquals(bunLoad.code, 0, bunLoad.stderr || bunLoad.stdout); |
99 | | - assertEquals(bunLoad.stdout.trim(), '["graphiti"]'); |
100 | | - } |
| 124 | + const esmLoad = await run("node", [esmRunnerPath]); |
| 125 | + assertEquals(esmLoad.code, 0, esmLoad.stderr || esmLoad.stdout); |
| 126 | + assertEquals(esmLoad.stdout.trim(), '["graphiti"]'); |
101 | 127 |
|
102 | | - const localOpenCodePath = "/Users/vicary/.opencode/bin/opencode"; |
103 | | - try { |
104 | | - const opencodeInfo = await Deno.stat(localOpenCodePath); |
105 | | - if (opencodeInfo.isFile) { |
106 | | - const isolatedOpenCode = await new Deno.Command(localOpenCodePath, { |
107 | | - args: ["--print-logs", "stats"], |
108 | | - cwd: workspacePath, |
109 | | - env: { |
110 | | - HOME: isolatedHome, |
111 | | - XDG_CONFIG_HOME: join(isolatedHome, ".config"), |
112 | | - }, |
113 | | - stdout: "piped", |
114 | | - stderr: "piped", |
115 | | - }).output(); |
116 | | - const isolatedOpenCodeOutput = decodeText(isolatedOpenCode.stdout) + |
117 | | - decodeText(isolatedOpenCode.stderr); |
118 | | - assertEquals( |
119 | | - isolatedOpenCodeOutput.includes("Missing 'default' export"), |
120 | | - false, |
121 | | - isolatedOpenCodeOutput, |
122 | | - ); |
| 128 | + if (bunRunPermissionGranted && await commandExists("bun")) { |
| 129 | + const bunLoad = await run("bun", [bunRunnerPath], tempDir); |
| 130 | + assertEquals(bunLoad.code, 0, bunLoad.stderr || bunLoad.stdout); |
| 131 | + assertEquals(bunLoad.stdout.trim(), '["graphiti"]'); |
123 | 132 | } |
124 | | - } catch { |
125 | | - // OpenCode is not available in CI; keep the portable package checks above. |
| 133 | + |
| 134 | + if (optionalOpenCodePath) { |
| 135 | + try { |
| 136 | + const opencodeInfo = await Deno.stat(optionalOpenCodePath); |
| 137 | + if (opencodeInfo.isFile) { |
| 138 | + const isolatedOpenCode = await new Deno.Command( |
| 139 | + optionalOpenCodePath, |
| 140 | + { |
| 141 | + args: ["--print-logs", "stats"], |
| 142 | + cwd: workspacePath, |
| 143 | + env: { |
| 144 | + HOME: isolatedHome, |
| 145 | + XDG_CONFIG_HOME: join(isolatedHome, ".config"), |
| 146 | + }, |
| 147 | + stdout: "piped", |
| 148 | + stderr: "piped", |
| 149 | + }, |
| 150 | + ).output(); |
| 151 | + const isolatedOpenCodeOutput = decodeText(isolatedOpenCode.stdout) + |
| 152 | + decodeText(isolatedOpenCode.stderr); |
| 153 | + assertEquals( |
| 154 | + isolatedOpenCodeOutput.includes("Missing 'default' export"), |
| 155 | + false, |
| 156 | + isolatedOpenCodeOutput, |
| 157 | + ); |
| 158 | + } |
| 159 | + } catch { |
| 160 | + // OPENCODE_BIN is optional; keep the portable package checks above. |
| 161 | + } |
| 162 | + } |
| 163 | + } finally { |
| 164 | + await Deno.remove(tempDir, { recursive: true }).catch(() => undefined); |
126 | 165 | } |
127 | | - } finally { |
128 | | - await Deno.remove(tempDir, { recursive: true }).catch(() => undefined); |
129 | | - } |
| 166 | + }, |
130 | 167 | }); |
0 commit comments