341 lines
10 KiB
TypeScript
341 lines
10 KiB
TypeScript
import fs from "node:fs"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import dotenv from "dotenv"
|
|
import * as esbuild from "esbuild"
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = path.dirname(__filename)
|
|
const rootDir = path.resolve(__dirname, "..")
|
|
|
|
// Load .env from repo root
|
|
dotenv.config({ path: path.join(rootDir, ".env") })
|
|
|
|
const production = process.argv.includes("--production")
|
|
const watch = process.argv.includes("--watch")
|
|
|
|
/**
|
|
* Plugin to resolve path aliases from the parent project
|
|
*/
|
|
const aliasResolverPlugin: esbuild.Plugin = {
|
|
name: "alias-resolver",
|
|
setup(build) {
|
|
const aliases = {
|
|
"@": path.resolve(rootDir, "src"),
|
|
"@core": path.resolve(rootDir, "src/core"),
|
|
"@integrations": path.resolve(rootDir, "src/integrations"),
|
|
"@services": path.resolve(rootDir, "src/services"),
|
|
"@shared": path.resolve(rootDir, "src/shared"),
|
|
"@utils": path.resolve(rootDir, "src/utils"),
|
|
"@packages": path.resolve(rootDir, "src/packages"),
|
|
"@hosts": path.resolve(rootDir, "src/hosts"),
|
|
"@generated": path.resolve(rootDir, "src/generated"),
|
|
"@api": path.resolve(rootDir, "src/core/api"),
|
|
}
|
|
|
|
// For each alias entry, create a resolver
|
|
Object.entries(aliases).forEach(([alias, aliasPath]) => {
|
|
const aliasRegex = new RegExp(`^${alias}($|/.*)`)
|
|
build.onResolve({ filter: aliasRegex }, (args) => {
|
|
const importPath = args.path.replace(alias, aliasPath)
|
|
|
|
// First, check if the path exists as is
|
|
if (fs.existsSync(importPath)) {
|
|
const stats = fs.statSync(importPath)
|
|
if (stats.isDirectory()) {
|
|
// If it's a directory, try to find index files
|
|
const extensions = [".ts", ".tsx", ".js", ".jsx"]
|
|
for (const ext of extensions) {
|
|
const indexFile = path.join(importPath, `index${ext}`)
|
|
if (fs.existsSync(indexFile)) {
|
|
return { path: indexFile }
|
|
}
|
|
}
|
|
} else {
|
|
// It's a file that exists, so return it
|
|
return { path: importPath }
|
|
}
|
|
}
|
|
|
|
// If the path doesn't exist, try appending extensions
|
|
const extensions = [".ts", ".tsx", ".js", ".jsx"]
|
|
for (const ext of extensions) {
|
|
const pathWithExtension = `${importPath}${ext}`
|
|
if (fs.existsSync(pathWithExtension)) {
|
|
return { path: pathWithExtension }
|
|
}
|
|
}
|
|
|
|
// Handle .js -> .ts extension mapping (common in ESM TypeScript projects)
|
|
if (importPath.endsWith(".js")) {
|
|
const tsPath = importPath.replace(/\.js$/, ".ts")
|
|
if (fs.existsSync(tsPath)) {
|
|
return { path: tsPath }
|
|
}
|
|
const tsxPath = importPath.replace(/\.js$/, ".tsx")
|
|
if (fs.existsSync(tsxPath)) {
|
|
return { path: tsxPath }
|
|
}
|
|
}
|
|
|
|
// If nothing worked, return the original path and let esbuild handle the error
|
|
return { path: importPath }
|
|
})
|
|
})
|
|
},
|
|
}
|
|
|
|
/**
|
|
* Plugin to redirect vscode imports to our shim
|
|
*/
|
|
const vscodeStubPlugin: esbuild.Plugin = {
|
|
name: "vscode-stub",
|
|
setup(build) {
|
|
// Redirect 'vscode' imports to our shim
|
|
build.onResolve({ filter: /^vscode$/ }, () => {
|
|
return { path: path.join(__dirname, "src", "vscode-shim.ts") }
|
|
})
|
|
},
|
|
}
|
|
|
|
const esbuildProblemMatcherPlugin: esbuild.Plugin = {
|
|
name: "esbuild-problem-matcher",
|
|
setup(build) {
|
|
build.onStart(() => {
|
|
console.log("[cli esbuild] Build started...")
|
|
})
|
|
build.onEnd((result) => {
|
|
result.errors.forEach(({ text, location }) => {
|
|
console.error(`✘ [ERROR] ${text}`)
|
|
if (location) {
|
|
console.error(` ${location.file}:${location.line}:${location.column}:`)
|
|
}
|
|
})
|
|
console.log("[cli esbuild] Build finished")
|
|
})
|
|
},
|
|
}
|
|
|
|
// Plugin to stub out optional devtools module
|
|
const stubOptionalModulesPlugin: esbuild.Plugin = {
|
|
name: "stub-optional-modules",
|
|
setup(build) {
|
|
build.onResolve({ filter: /^react-devtools-core$/ }, () => {
|
|
return { path: path.join(__dirname, "src", "stub-devtools.js"), external: false }
|
|
})
|
|
},
|
|
}
|
|
|
|
const copyWasmFiles: esbuild.Plugin = {
|
|
name: "copy-wasm-files",
|
|
setup(build) {
|
|
build.onEnd(() => {
|
|
const destDir = path.join(__dirname, "dist")
|
|
|
|
// Ensure dist directory exists
|
|
if (!fs.existsSync(destDir)) {
|
|
fs.mkdirSync(destDir, { recursive: true })
|
|
}
|
|
|
|
// tree sitter
|
|
const sourceDir = path.join(rootDir, "node_modules", "web-tree-sitter")
|
|
|
|
// Copy web-tree-sitter module to dist/node_modules so it can be loaded at runtime
|
|
// (web-tree-sitter is externalized from the bundle to avoid Emscripten bundling issues)
|
|
const webTreeSitterTarget = path.join(destDir, "node_modules", "web-tree-sitter")
|
|
fs.mkdirSync(webTreeSitterTarget, { recursive: true })
|
|
if (fs.existsSync(path.join(sourceDir, "tree-sitter.js"))) {
|
|
fs.copyFileSync(path.join(sourceDir, "tree-sitter.js"), path.join(webTreeSitterTarget, "tree-sitter.js"))
|
|
}
|
|
if (fs.existsSync(path.join(sourceDir, "tree-sitter.wasm"))) {
|
|
fs.copyFileSync(path.join(sourceDir, "tree-sitter.wasm"), path.join(webTreeSitterTarget, "tree-sitter.wasm"))
|
|
}
|
|
if (fs.existsSync(path.join(sourceDir, "tree-sitter-web.d.ts"))) {
|
|
fs.copyFileSync(
|
|
path.join(sourceDir, "tree-sitter-web.d.ts"),
|
|
path.join(webTreeSitterTarget, "tree-sitter-web.d.ts"),
|
|
)
|
|
}
|
|
// Write a minimal package.json so require('web-tree-sitter') resolves correctly
|
|
fs.writeFileSync(
|
|
path.join(webTreeSitterTarget, "package.json"),
|
|
JSON.stringify({ name: "web-tree-sitter", main: "tree-sitter.js" }),
|
|
)
|
|
|
|
// Also copy tree-sitter.wasm to dist root for the locateFile callback
|
|
const treeSitterWasm = path.join(sourceDir, "tree-sitter.wasm")
|
|
if (fs.existsSync(treeSitterWasm)) {
|
|
fs.copyFileSync(treeSitterWasm, path.join(destDir, "tree-sitter.wasm"))
|
|
}
|
|
|
|
// Copy sql-wasm.wasm
|
|
const sqlJsSource = path.join(rootDir, "node_modules", "sql.js", "dist", "sql-wasm.wasm")
|
|
if (fs.existsSync(sqlJsSource)) {
|
|
fs.copyFileSync(sqlJsSource, path.join(destDir, "sql-wasm.wasm"))
|
|
}
|
|
|
|
// Copy language-specific WASM files
|
|
const languageWasmDir = path.join(rootDir, "node_modules", "tree-sitter-wasms", "out")
|
|
const languages = [
|
|
"typescript",
|
|
"tsx",
|
|
"python",
|
|
"rust",
|
|
"javascript",
|
|
"go",
|
|
"cpp",
|
|
"c",
|
|
"c_sharp",
|
|
"ruby",
|
|
"java",
|
|
"php",
|
|
"swift",
|
|
"kotlin",
|
|
]
|
|
|
|
if (fs.existsSync(languageWasmDir)) {
|
|
// Copy .hash_anchors
|
|
const dictionarySource = path.join(rootDir, "src", "utils", ".hash_anchors")
|
|
const dictionaryTarget = path.join(destDir, ".hash_anchors")
|
|
if (fs.existsSync(dictionarySource)) {
|
|
fs.copyFileSync(dictionarySource, dictionaryTarget)
|
|
}
|
|
|
|
languages.forEach((lang) => {
|
|
const filename = `tree-sitter-${lang}.wasm`
|
|
const sourcePath = path.join(languageWasmDir, filename)
|
|
if (fs.existsSync(sourcePath)) {
|
|
fs.copyFileSync(sourcePath, path.join(destDir, filename))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
},
|
|
}
|
|
|
|
const buildEnvVars: Record<string, string> = {
|
|
"process.env.IS_STANDALONE": JSON.stringify("true"),
|
|
"process.env.IS_CLI": JSON.stringify("true"),
|
|
}
|
|
|
|
const buildTimeEnvs = [
|
|
"TELEMETRY_SERVICE_API_KEY",
|
|
"ERROR_SERVICE_API_KEY",
|
|
"ENABLE_ERROR_AUTOCAPTURE",
|
|
"POSTHOG_TELEMETRY_ENABLED",
|
|
"OTEL_TELEMETRY_ENABLED",
|
|
"OTEL_LOGS_EXPORTER",
|
|
"OTEL_METRICS_EXPORTER",
|
|
"OTEL_EXPORTER_OTLP_PROTOCOL",
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
"OTEL_EXPORTER_OTLP_HEADERS",
|
|
"OTEL_METRIC_EXPORT_INTERVAL",
|
|
"DIRAC_ENVIRONMENT",
|
|
]
|
|
|
|
buildTimeEnvs.forEach((envVar) => {
|
|
if (process.env[envVar]) {
|
|
console.log(`[cli esbuild] ${envVar} env var is set`)
|
|
buildEnvVars[`process.env.${envVar}`] = JSON.stringify(process.env[envVar])
|
|
}
|
|
})
|
|
|
|
if (production) {
|
|
buildEnvVars["process.env.IS_DEV"] = "false"
|
|
}
|
|
|
|
// Shared build options
|
|
const sharedOptions: Partial<esbuild.BuildOptions> = {
|
|
bundle: true,
|
|
minify: production,
|
|
sourcemap: !production,
|
|
logLevel: "silent",
|
|
define: buildEnvVars,
|
|
tsconfig: path.join(__dirname, "tsconfig.json"),
|
|
plugins: [copyWasmFiles, aliasResolverPlugin, vscodeStubPlugin, stubOptionalModulesPlugin, esbuildProblemMatcherPlugin],
|
|
format: "esm",
|
|
sourcesContent: false,
|
|
platform: "node",
|
|
target: "node20",
|
|
// These modules need to load files from the module directory at runtime
|
|
external: [
|
|
"web-tree-sitter",
|
|
"@grpc/reflection",
|
|
"grpc-health-check",
|
|
"better-sqlite3",
|
|
"ink",
|
|
"ink-spinner",
|
|
"ink-picture",
|
|
"react",
|
|
"aws4fetch",
|
|
"pino",
|
|
"pino-roll",
|
|
"@vscode/ripgrep", // Uses __dirname to locate the binary
|
|
],
|
|
supported: { "top-level-await": true },
|
|
}
|
|
|
|
// CLI executable configuration
|
|
const cliConfig: esbuild.BuildOptions = {
|
|
...sharedOptions,
|
|
entryPoints: [path.join(__dirname, "src", "index.ts")],
|
|
outfile: path.join(__dirname, "dist", "cli.mjs"),
|
|
banner: {
|
|
js: `#!/usr/bin/env node
|
|
// Suppress all Node.js warnings (deprecation, experimental, etc.)
|
|
process.emitWarning = () => {};
|
|
import { createRequire as _createRequire } from 'module';
|
|
import { fileURLToPath as _fileURLToPath } from 'url';
|
|
import { dirname as _dirname } from 'path';
|
|
const require = _createRequire(import.meta.url);
|
|
const __filename = _fileURLToPath(import.meta.url);
|
|
const __dirname = _dirname(__filename);`,
|
|
},
|
|
}
|
|
|
|
// Library configuration for programmatic use
|
|
const libConfig: esbuild.BuildOptions = {
|
|
...sharedOptions,
|
|
entryPoints: [path.join(__dirname, "src", "exports.ts")],
|
|
outfile: path.join(__dirname, "dist", "lib.mjs"),
|
|
banner: {
|
|
js: `// Dirac Library - Programmatic API
|
|
import { createRequire as _createRequire } from 'module';
|
|
import { fileURLToPath as _fileURLToPath } from 'url';
|
|
import { dirname as _dirname } from 'path';
|
|
const require = _createRequire(import.meta.url);
|
|
const __filename = _fileURLToPath(import.meta.url);
|
|
const __dirname = _dirname(__filename);`,
|
|
},
|
|
}
|
|
|
|
async function main() {
|
|
if (watch) {
|
|
// In watch mode, only watch the CLI (primary use case for development)
|
|
const ctx = await esbuild.context(cliConfig)
|
|
await ctx.watch()
|
|
console.log("[cli] Watching for changes...")
|
|
} else {
|
|
// Build both CLI and library
|
|
console.log("[cli esbuild] Building CLI executable...")
|
|
const cliCtx = await esbuild.context(cliConfig)
|
|
await cliCtx.rebuild()
|
|
await cliCtx.dispose()
|
|
|
|
console.log("[cli esbuild] Building library bundle...")
|
|
const libCtx = await esbuild.context(libConfig)
|
|
await libCtx.rebuild()
|
|
await libCtx.dispose()
|
|
|
|
// Make the CLI output executable
|
|
const cliOutfile = path.join(__dirname, "dist", "cli.mjs")
|
|
if (fs.existsSync(cliOutfile)) {
|
|
fs.chmodSync(cliOutfile, "755")
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e)
|
|
process.exit(1)
|
|
})
|