Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"LICENSE"
],
"scripts": {
"build": "tsc -p tsconfig.build.json",
"build": "tsc -p tsconfig.build.json && node script/fix-esm-imports.mjs",
"build:schema": "npx tsx script/build-schema.ts",
"typecheck": "tsc --noEmit",
"test": "vitest run",
Expand Down Expand Up @@ -64,4 +64,4 @@
"xdg-basedir": "^5.1.0",
"zod": "^4.0.0"
}
}
}
76 changes: 76 additions & 0 deletions script/fix-esm-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { existsSync } from "node:fs"
import { readdir, readFile, stat, writeFile } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
const distDir = path.join(repoRoot, "dist")

const importPatterns = [
/(from\s*["'])(\.{1,2}\/[^"']+)(["'])/g,
/(import\s*\(\s*["'])(\.{1,2}\/[^"']+)(["'])/g,
/(\bimport\s*["'])(\.{1,2}\/[^"']+)(["'])/g,
]

function hasExtension(specifier) {
return path.posix.extname(specifier) !== ""
}

function resolveSpecifier(filePath, specifier) {
if (hasExtension(specifier)) return specifier

const targetPath = path.resolve(path.dirname(filePath), specifier)
if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
return specifier
Comment on lines +23 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Silent fallback on unresolved specifier — when neither ${specifier}.js nor ${specifier}/index.js exists on disk, the function returns the original extension-less specifier unchanged. The output file will silently contain an unresolvable import, which only fails at runtime in the consuming project rather than failing the build step where it would be easy to catch.

Suggested change
if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
return specifier
if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
console.warn(`[fix-esm-imports] could not resolve specifier: ${specifier} (from ${filePath})`)
return specifier
Prompt To Fix With AI
This is a comment left during a code review.
Path: script/fix-esm-imports.mjs
Line: 23-25

Comment:
Silent fallback on unresolved specifier — when neither `${specifier}.js` nor `${specifier}/index.js` exists on disk, the function returns the original extension-less specifier unchanged. The output file will silently contain an unresolvable import, which only fails at runtime in the consuming project rather than failing the build step where it would be easy to catch.

```suggestion
  if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
  if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
  console.warn(`[fix-esm-imports] could not resolve specifier: ${specifier} (from ${filePath})`)
  return specifier
```

How can I resolve this? If you propose a fix, please make it concise.

}
Comment on lines +19 to +26
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent passthrough leaves broken specifiers unfixed without any signal.

When neither ${specifier}.js nor ${specifier}/index.js resolves on disk, resolveSpecifier returns the original extension-less specifier unchanged. This means a build where TypeScript emits an import for a path that wasn't correctly compiled (e.g., missing output file, typo in source) will silently produce a broken ESM specifier in dist/ — with no build-time indication.

Adding a console.warn for unresolved specifiers makes the failure observable:

🛡️ Proposed fix
 function resolveSpecifier(filePath, specifier) {
   if (hasExtension(specifier)) return specifier
 
   const targetPath = path.resolve(path.dirname(filePath), specifier)
   if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
   if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
+  console.warn(`[fix-esm-imports] Could not resolve specifier "${specifier}" in ${filePath} — leaving unchanged`)
   return specifier
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function resolveSpecifier(filePath, specifier) {
if (hasExtension(specifier)) return specifier
const targetPath = path.resolve(path.dirname(filePath), specifier)
if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
return specifier
}
function resolveSpecifier(filePath, specifier) {
if (hasExtension(specifier)) return specifier
const targetPath = path.resolve(path.dirname(filePath), specifier)
if (existsSync(`${targetPath}.js`)) return `${specifier}.js`
if (existsSync(path.join(targetPath, "index.js"))) return `${specifier}/index.js`
console.warn(`[fix-esm-imports] Could not resolve specifier "${specifier}" in ${filePath} — leaving unchanged`)
return specifier
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@script/fix-esm-imports.mjs` around lines 19 - 26, resolveSpecifier currently
returns the original specifier silently when neither `${targetPath}.js` nor
`${targetPath}/index.js` exists, which can leave broken ESM imports; modify
resolveSpecifier to detect this unresolved case and emit a clear warning (e.g.,
console.warn) that includes the filePath, specifier, and attempted resolution
targets so the build author is alerted; keep existing behavior for specifiers
with extensions (hasExtension) and successful resolutions, and reference
resolveSpecifier, hasExtension, path, and existsSync when locating the change.


async function listJsFiles(dir) {
const entries = await readdir(dir)
const files = []

for (const entry of entries) {
const entryPath = path.join(dir, entry)
const entryStat = await stat(entryPath)
if (entryStat.isDirectory()) {
files.push(...await listJsFiles(entryPath))
} else if (entry.endsWith(".js")) {
files.push(entryPath)
}
}

return files
}

async function fixFile(filePath) {
const source = await readFile(filePath, "utf8")
let output = source

for (const pattern of importPatterns) {
output = output.replace(pattern, (match, prefix, specifier, suffix) => {
return `${prefix}${resolveSpecifier(filePath, specifier)}${suffix}`
})
}
Comment on lines +49 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Stateful /g regex applied per-file in a loopimportPatterns is a module-level array of regex literals all carrying the /g flag. String.prototype.replace() resets lastIndex to 0 before iterating, so this is safe in practice — but it is an easy trap if someone later changes the loop to use .exec() or .test(). Moving the regex construction inside fixFile (or inside main per-run) would eliminate the statefulness entirely and make the intent clearer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: script/fix-esm-imports.mjs
Line: 49-53

Comment:
**Stateful `/g` regex applied per-file in a loop**`importPatterns` is a module-level array of regex literals all carrying the `/g` flag. `String.prototype.replace()` resets `lastIndex` to 0 before iterating, so this is safe in practice — but it is an easy trap if someone later changes the loop to use `.exec()` or `.test()`. Moving the regex construction inside `fixFile` (or inside `main` per-run) would eliminate the statefulness entirely and make the intent clearer.

How can I resolve this? If you propose a fix, please make it concise.


if (output !== source) {
await writeFile(filePath, output)
return true
}

return false
}

async function main() {
if (!existsSync(distDir)) return

const files = await listJsFiles(distDir)
let changed = 0

for (const file of files) {
if (await fixFile(file)) changed += 1
}

console.log(`Fixed ESM import specifiers in ${changed} dist file(s)`)
}

await main()
2 changes: 1 addition & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { exec } from "node:child_process";
import { tool } from "@opencode-ai/plugin";
import { tool } from "@opencode-ai/plugin/tool";
import {
ANTIGRAVITY_DEFAULT_PROJECT_ID,
ANTIGRAVITY_ENDPOINT_FALLBACKS,
Expand Down
2 changes: 1 addition & 1 deletion src/plugin/quota-fallback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ let getHeaderStyleFromUrl: GetHeaderStyleFromUrl | undefined;
let resolveHeaderRoutingDecision: ResolveHeaderRoutingDecision | undefined;

beforeAll(async () => {
vi.mock("@opencode-ai/plugin", () => ({
vi.mock("@opencode-ai/plugin/tool", () => ({
tool: vi.fn(),
}));

Expand Down