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
111 changes: 111 additions & 0 deletions packages/build/src/esbuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,117 @@ export function copyWasms(srcDir: string, distDir: string): void {
console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
}

/**
* Find the esbuild-wasm package directory.
* Handles regular node_modules, pnpm hoisting, and monorepo structures.
*/
function findEsbuildWasmDir(srcDir: string): string | null {
const possiblePaths = [
// Direct in node_modules (npm, yarn)
path.join(srcDir, "node_modules", "esbuild-wasm"),
// Parent directory's node_modules (monorepo with hoisting)
path.join(srcDir, "..", "node_modules", "esbuild-wasm"),
// Root node_modules in monorepo
path.join(srcDir, "..", "..", "node_modules", "esbuild-wasm"),
]

// Check each possible path
for (const possiblePath of possiblePaths) {
if (fs.existsSync(possiblePath)) {
return possiblePath
}
}

// Check pnpm .pnpm directory structure
const pnpmPaths = [
path.join(srcDir, "node_modules", ".pnpm"),
path.join(srcDir, "..", "node_modules", ".pnpm"),
path.join(srcDir, "..", "..", "node_modules", ".pnpm"),
]

for (const pnpmPath of pnpmPaths) {
if (fs.existsSync(pnpmPath)) {
try {
const dirs = fs.readdirSync(pnpmPath)
// Look for esbuild-wasm@<version>
const esbuildDir = dirs.find((dir) => dir.startsWith("esbuild-wasm@"))

if (esbuildDir) {
const packagePath = path.join(pnpmPath, esbuildDir, "node_modules", "esbuild-wasm")
if (fs.existsSync(packagePath)) {
return packagePath
}
}
} catch {
// Ignore errors reading directories
}
}
}

return null
}

/**
* Copy esbuild-wasm files to the dist/bin directory.
*
* This function copies the esbuild-wasm CLI and WASM binary, which provides
* a cross-platform esbuild implementation that works on all platforms.
* This is simpler and smaller (~12MB) than bundling native binaries for all platforms.
*
* Files copied:
* - bin/esbuild (Node.js CLI script)
* - esbuild.wasm (WASM binary)
* - wasm_exec_node.js (Go WASM runtime for Node.js)
*
* @param srcDir - Source directory containing node_modules
* @param distDir - Destination dist directory
*/
export function copyEsbuildWasm(srcDir: string, distDir: string): void {
const esbuildWasmDir = findEsbuildWasmDir(srcDir)

if (!esbuildWasmDir) {
console.warn("[copyEsbuildWasm] esbuild-wasm package not found, skipping")
return
}

// Create bin directory in dist
const binDir = path.join(distDir, "bin")
fs.mkdirSync(binDir, { recursive: true })

// Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
// to be one directory level up from the bin directory (i.e., in distDir directly)
// wasm_exec_node.js requires wasm_exec.js, so we need to copy that too
const filesToCopy = [
{ src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
// wasm_exec_node.js, wasm_exec.js, and esbuild.wasm go in dist/, not dist/bin/
{ src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
{ src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
{ src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
]

let copiedCount = 0

for (const { src, dest } of filesToCopy) {
if (fs.existsSync(src)) {
fs.copyFileSync(src, dest)
copiedCount++

// Make CLI executable
if (src.endsWith("esbuild")) {
try {
fs.chmodSync(dest, 0o755)
} catch {
// Ignore chmod errors on Windows
}
}
} else {
console.warn(`[copyEsbuildWasm] File not found: ${src}`)
}
}

console.log(`[copyEsbuildWasm] Copied ${copiedCount} esbuild-wasm files to ${distDir}`)
}

export function copyLocales(srcDir: string, distDir: string): void {
const destDir = path.join(distDir, "i18n", "locales")
fs.mkdirSync(destDir, { recursive: true })
Expand Down
9 changes: 8 additions & 1 deletion packages/build/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
export { getGitSha } from "./git.js"
export { copyPaths, copyWasms, copyLocales, setupLocaleWatcher, generatePackageJson } from "./esbuild.js"
export {
copyPaths,
copyWasms,
copyEsbuildWasm,
copyLocales,
setupLocaleWatcher,
generatePackageJson,
} from "./esbuild.js"
189 changes: 189 additions & 0 deletions packages/core/src/custom-tools/__tests__/esbuild-runner.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import fs from "fs"
import os from "os"
import path from "path"
import { fileURLToPath } from "url"

import { getEsbuildScriptPath, runEsbuild } from "../esbuild-runner.js"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

describe("esbuild-runner", () => {
describe("getEsbuildScriptPath", () => {
it("should find esbuild-wasm script in node_modules in development", () => {
const scriptPath = getEsbuildScriptPath()

// Should find the script
expect(typeof scriptPath).toBe("string")
expect(scriptPath.length).toBeGreaterThan(0)

// The script should exist
expect(fs.existsSync(scriptPath)).toBe(true)

// Should be the esbuild script (not a binary)
expect(scriptPath).toMatch(/esbuild$/)
})

it("should prefer production path when extensionPath is provided and script exists", () => {
// Create a temporary directory with a fake script
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
const binDir = path.join(tempDir, "dist", "bin")
fs.mkdirSync(binDir, { recursive: true })

const fakeScriptPath = path.join(binDir, "esbuild")
fs.writeFileSync(fakeScriptPath, "#!/usr/bin/env node\nconsole.log('fake esbuild')")

try {
const result = getEsbuildScriptPath(tempDir)
expect(result).toBe(fakeScriptPath)
} finally {
// Cleanup
fs.rmSync(tempDir, { recursive: true, force: true })
}
})

it("should fall back to node_modules when production script does not exist", () => {
// Pass a non-existent extension path
const result = getEsbuildScriptPath("/nonexistent/extension/path")

// Should fall back to development path
expect(typeof result).toBe("string")
expect(result.length).toBeGreaterThan(0)
expect(fs.existsSync(result)).toBe(true)
})
})

describe("runEsbuild", () => {
let tempDir: string

beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
})

afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true })
})

it("should compile a TypeScript file to ESM", async () => {
// Create a simple TypeScript file
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(
inputFile,
`
export const greeting = "Hello, World!"
export function add(a: number, b: number): number {
return a + b
}
`,
)

await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
platform: "node",
target: "node18",
bundle: true,
})

// Verify output file exists
expect(fs.existsSync(outputFile)).toBe(true)

// Verify output content is valid JavaScript
const outputContent = fs.readFileSync(outputFile, "utf-8")
expect(outputContent).toContain("Hello, World!")
expect(outputContent).toContain("add")
}, 30000)

it("should generate inline source maps when specified", async () => {
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(inputFile, `export const value = 42`)

await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
sourcemap: "inline",
})

const outputContent = fs.readFileSync(outputFile, "utf-8")
expect(outputContent).toContain("sourceMappingURL=data:")
}, 30000)

it("should throw an error for invalid TypeScript", async () => {
const inputFile = path.join(tempDir, "invalid.ts")
const outputFile = path.join(tempDir, "output.mjs")

// Write syntactically invalid TypeScript
fs.writeFileSync(inputFile, `export const value = {{{ invalid syntax`)

await expect(
runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
}),
).rejects.toThrow()
}, 30000)

it("should throw an error for non-existent file", async () => {
const nonExistentFile = path.join(tempDir, "does-not-exist.ts")
const outputFile = path.join(tempDir, "output.mjs")

await expect(
runEsbuild({
entryPoint: nonExistentFile,
outfile: outputFile,
format: "esm",
}),
).rejects.toThrow()
}, 30000)

it("should bundle dependencies when bundle option is true", async () => {
// Create two files where one imports the other
const libFile = path.join(tempDir, "lib.ts")
const mainFile = path.join(tempDir, "main.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(libFile, `export const PI = 3.14159`)
fs.writeFileSync(
mainFile,
`
import { PI } from "./lib.js"
export const circumference = (r: number) => 2 * PI * r
`,
)

await runEsbuild({
entryPoint: mainFile,
outfile: outputFile,
format: "esm",
bundle: true,
})

const outputContent = fs.readFileSync(outputFile, "utf-8")
// The PI constant should be bundled inline
expect(outputContent).toContain("3.14159")
}, 30000)

it("should respect platform option", async () => {
const inputFile = path.join(tempDir, "input.ts")
const outputFile = path.join(tempDir, "output.mjs")

fs.writeFileSync(inputFile, `export const value = process.env.NODE_ENV`)

await runEsbuild({
entryPoint: inputFile,
outfile: outputFile,
format: "esm",
platform: "node",
})

// File should be created successfully
expect(fs.existsSync(outputFile)).toBe(true)
}, 30000)
})
})
Loading