Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions packages/opencode/script/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ async function copyAssets(targetDir: string) {
await $`cp ../dbt-tools/bin/altimate-dbt ${targetDir}/dbt-tools/bin/altimate-dbt`
await $`mkdir -p ${targetDir}/dbt-tools/dist`
await $`cp ../dbt-tools/dist/index.js ${targetDir}/dbt-tools/dist/`
// A package.json with "type": "module" must be present so Node loads
// dist/index.js as ESM instead of CJS. We synthesize a minimal one rather
// than copying the full source package.json (which contains devDependencies
// with Bun catalog: versions that would confuse vulnerability scanners).
await Bun.file(`${targetDir}/dbt-tools/package.json`).write(
JSON.stringify({ type: "module" }, null, 2) + "\n",
)
if (fs.existsSync("../dbt-tools/dist/altimate_python_packages")) {
await $`cp -r ../dbt-tools/dist/altimate_python_packages ${targetDir}/dbt-tools/dist/`
}
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/test/branding/build-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ describe("Bundle Completeness", () => {
expect(publishScript).toContain("dbt-tools/bin/altimate-dbt")
expect(publishScript).toContain("dbt-tools/dist")
expect(publishScript).toContain("bun run build")
// package.json must be bundled so Node sees "type": "module"
expect(publishScript).toContain("dbt-tools/package.json")
})

test("publish.ts copies only needed dbt-tools dist files (not .node binaries)", () => {
Expand Down
125 changes: 125 additions & 0 deletions packages/opencode/test/install/dbt-tools-bundle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, test, expect } from "bun:test"
import fs from "fs"
import path from "path"

const REPO_ROOT = path.resolve(import.meta.dir, "../../../..")
const PUBLISH_SCRIPT = path.join(REPO_ROOT, "packages/opencode/script/publish.ts")
const DBT_TOOLS_DIR = path.join(REPO_ROOT, "packages/dbt-tools")

// ---------------------------------------------------------------------------
// 1. Source dbt-tools uses ESM — if this changes, publish.ts must adapt
// ---------------------------------------------------------------------------

describe("dbt-tools ESM contract", () => {
test('source package.json declares "type": "module"', () => {
const pkg = JSON.parse(fs.readFileSync(path.join(DBT_TOOLS_DIR, "package.json"), "utf-8"))
expect(pkg.type).toBe("module")
})

test("bin/altimate-dbt uses node shebang (not bun)", () => {
const bin = fs.readFileSync(path.join(DBT_TOOLS_DIR, "bin/altimate-dbt"), "utf-8")
expect(bin).toContain("#!/usr/bin/env node")
})

test("bin/altimate-dbt uses ESM import() to load dist/index.js", () => {
const bin = fs.readFileSync(path.join(DBT_TOOLS_DIR, "bin/altimate-dbt"), "utf-8")
expect(bin).toContain('import("../dist/index.js")')
})

test("build outputs ESM format (--format esm in build script)", () => {
const pkg = JSON.parse(fs.readFileSync(path.join(DBT_TOOLS_DIR, "package.json"), "utf-8"))
const buildScript = pkg.scripts?.build || ""
expect(buildScript).toContain("--format esm")
})
})

// ---------------------------------------------------------------------------
// 2. publish.ts must bundle a package.json with "type": "module"
// ---------------------------------------------------------------------------

describe("publish.ts dbt-tools ESM bundling", () => {
const publishSource = fs.readFileSync(PUBLISH_SCRIPT, "utf-8")

test("copyAssets writes a dbt-tools/package.json", () => {
// The publish script must create a package.json in the bundled dbt-tools dir.
// Without it, Node defaults to CJS and `import` statements in dist/index.js fail.
expect(publishSource).toContain("dbt-tools/package.json")
})

test('bundled package.json includes "type": "module"', () => {
// The publish script must write `{ "type": "module" }` (or equivalent)
// so that Node treats .js files in dbt-tools/ as ESM.
expect(publishSource).toContain('"type": "module"')
})

test("copyAssets creates dbt-tools/bin and dbt-tools/dist directories", () => {
expect(publishSource).toContain("dbt-tools/bin")
expect(publishSource).toContain("dbt-tools/dist")
})

test("copyAssets copies the altimate-dbt bin wrapper", () => {
expect(publishSource).toContain("dbt-tools/bin/altimate-dbt")
})

test("copyAssets copies dist/index.js (not the entire dist/ tree)", () => {
// Copying all of dist/ would include ~220MB of .node native binaries
expect(publishSource).toContain("dist/index.js")
})
})

// ---------------------------------------------------------------------------
// 3. Structural: if dbt-tools ever switches away from ESM, tests should catch it
// ---------------------------------------------------------------------------

describe("dbt-tools + Node compatibility", () => {
test("bin wrapper import path matches dist output location", () => {
// The bin wrapper does `import("../dist/index.js")`.
// If the build output location changes, this test forces an update.
const bin = fs.readFileSync(path.join(DBT_TOOLS_DIR, "bin/altimate-dbt"), "utf-8")
const match = bin.match(/import\(["']([^"']+)["']\)/)
expect(match).not.toBeNull()
const importPath = match![1]
expect(importPath).toBe("../dist/index.js")
})

test("Node would fail without package.json type:module (regression guard)", () => {
// This is the exact scenario that caused the bug:
// - bin/altimate-dbt has `#!/usr/bin/env node`
// - It uses `import("../dist/index.js")`
// - dist/index.js starts with `import { createRequire } from "node:module"`
// - Without "type": "module" in package.json, Node treats .js as CJS
// - CJS cannot use top-level `import` → SyntaxError
//
// This test verifies the chain of conditions that require package.json:
const bin = fs.readFileSync(path.join(DBT_TOOLS_DIR, "bin/altimate-dbt"), "utf-8")

// Condition 1: Uses node (not bun) as the runtime
const usesNode = bin.includes("#!/usr/bin/env node")
// Condition 2: Uses ESM import syntax
const usesESMImport = bin.includes("import(")

if (usesNode && usesESMImport) {
// Then package.json MUST have "type": "module"
const pkg = JSON.parse(fs.readFileSync(path.join(DBT_TOOLS_DIR, "package.json"), "utf-8"))
expect(pkg.type).toBe("module")

// AND publish.ts MUST bundle that information
const publishSource = fs.readFileSync(PUBLISH_SCRIPT, "utf-8")
expect(publishSource).toContain('"type": "module"')
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})

test("all dbt-tools bin entries use node shebang (consistency check)", () => {
const pkg = JSON.parse(fs.readFileSync(path.join(DBT_TOOLS_DIR, "package.json"), "utf-8"))
const binEntries: Record<string, string> = pkg.bin || {}

for (const [name, relPath] of Object.entries(binEntries)) {
const binPath = path.join(DBT_TOOLS_DIR, relPath)
expect(fs.existsSync(binPath)).toBe(true)

const content = fs.readFileSync(binPath, "utf-8")
// All bin entries should use node (not bun) since end users may not have bun
expect(content).toContain("#!/usr/bin/env node")
}
})
})
2 changes: 2 additions & 0 deletions packages/opencode/test/install/publish-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ describe("publish package validation", () => {
expect(publishScript).toContain("cp -r ../../.opencode/skills")
expect(publishScript).toContain("dbt-tools/bin/altimate-dbt")
expect(publishScript).toContain("dbt-tools/dist")
// package.json needed for "type": "module" so Node loads ESM correctly
expect(publishScript).toContain("dbt-tools/package.json")
})

test("source scripts exist and use expected patterns", () => {
Expand Down
Loading