Skip to content

Commit 83c9ee7

Browse files
claude: Replace esbuild with deno bundle for extension building
Switch from esbuild to deno bundle for better import map support and simpler tooling. deno bundle natively handles JSR/npm/https imports and does type-checking + bundling in one step. Changes: - Replace esbuild with deno bundle in bundle() function - Remove typeCheck() function (deno bundle does both) - Restore import map entries for Deno std libraries - Restore updateImportMap() version syncing in prepare-dist.ts - Remove externals from quartoExtension interface (not needed) - Add validateExtensionYml() to warn about path mismatches - Simplify main action: --check uses deno check, normal build uses deno bundle Benefits: - Import maps work natively (no external URL workarounds) - Extension authors can use bare imports ("path" vs "jsr:@std/path@1.0.8") - One tool instead of two (simpler, faster) - Native support for jsr:, npm:, https: imports - Version consistency with Quarto maintained via import map - ~60 lines of code removed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a7bcffb commit 83c9ee7

File tree

4 files changed

+111
-66
lines changed

4 files changed

+111
-66
lines changed

package/src/common/prepare-dist.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,32 @@ function updateImportMap(config: Configuration) {
237237
);
238238
const importMapContent = JSON.parse(Deno.readTextFileSync(importMapPath));
239239

240-
// Update @quarto/types path from dev (../../../packages/...) to dist (./quarto-types.d.ts)
241-
importMapContent.imports["@quarto/types"] = "./quarto-types.d.ts";
240+
// Read the source import map to get current Deno std versions
241+
const sourceImportMapPath = join(config.directoryInfo.src, "import_map.json");
242+
const sourceImportMap = JSON.parse(Deno.readTextFileSync(sourceImportMapPath));
243+
const sourceImports = sourceImportMap.imports as Record<string, string>;
244+
245+
// Update the import map for distribution:
246+
// 1. Change @quarto/types path from dev (../../../packages/...) to dist (./quarto-types.d.ts)
247+
// 2. Update all other imports (Deno std versions) from source import map
248+
const updatedImports: Record<string, string> = {
249+
"@quarto/types": "./quarto-types.d.ts",
250+
};
251+
252+
// Copy all other imports from source, updating versions
253+
for (const key in importMapContent.imports) {
254+
if (key !== "@quarto/types") {
255+
const sourceValue = sourceImports[key];
256+
if (!sourceValue) {
257+
throw new Error(
258+
`Import map key "${key}" not found in source import_map.json`,
259+
);
260+
}
261+
updatedImports[key] = sourceValue;
262+
}
263+
}
264+
265+
importMapContent.imports = updatedImports;
242266

243267
// Write back the updated import map
244268
Deno.writeTextFileSync(

src/command/dev-call/build-ts-extension/cmd.ts

Lines changed: 77 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { execProcess } from "../../../core/process.ts";
1414
import { basename, dirname, extname, join } from "../../../deno_ral/path.ts";
1515
import { existsSync } from "../../../deno_ral/fs.ts";
1616
import { expandGlobSync } from "../../../core/deno/expand-glob.ts";
17+
import { readYaml } from "../../../core/yaml.ts";
18+
import { warning } from "../../../deno_ral/log.ts";
1719

1820
interface DenoConfig {
1921
compilerOptions?: Record<string, unknown>;
@@ -23,9 +25,7 @@ interface DenoConfig {
2325
entryPoint?: string;
2426
outputFile?: string;
2527
minify?: boolean;
26-
sourcemap?: boolean;
27-
target?: string;
28-
externals?: string[];
28+
sourcemap?: boolean | string;
2929
};
3030
}
3131

@@ -152,36 +152,6 @@ async function autoDetectEntryPoint(
152152
Deno.exit(1);
153153
}
154154

155-
async function typeCheck(
156-
entryPoint: string,
157-
configPath: string,
158-
): Promise<void> {
159-
info("Type-checking...");
160-
161-
const denoBinary = Deno.env.get("QUARTO_DENO") ||
162-
architectureToolsPath("deno");
163-
164-
const result = await execProcess({
165-
cmd: denoBinary,
166-
args: ["check", `--config=${configPath}`, entryPoint],
167-
cwd: Deno.cwd(),
168-
});
169-
170-
if (!result.success) {
171-
error("Error: Type check failed");
172-
error("");
173-
error(
174-
"See errors above. Fix type errors in your code or adjust compilerOptions in deno.json.",
175-
);
176-
error("");
177-
error("To see just type errors without building:");
178-
error(" quarto dev-call build-ts-extension --check");
179-
Deno.exit(1);
180-
}
181-
182-
info("✓ Type check passed");
183-
}
184-
185155
function inferOutputPath(entryPoint: string): string {
186156
// Get the base name without extension
187157
const fileName = basename(entryPoint, extname(entryPoint));
@@ -245,11 +215,12 @@ function inferOutputPath(entryPoint: string): string {
245215
async function bundle(
246216
entryPoint: string,
247217
config: DenoConfig,
218+
configPath: string,
248219
): Promise<void> {
249220
info("Bundling...");
250221

251-
const esbuildBinary = Deno.env.get("QUARTO_ESBUILD") ||
252-
architectureToolsPath("esbuild");
222+
const denoBinary = Deno.env.get("QUARTO_DENO") ||
223+
architectureToolsPath("deno");
253224

254225
// Determine output path
255226
const outputPath = config.quartoExtension?.outputFile ||
@@ -261,51 +232,83 @@ async function bundle(
261232
Deno.mkdirSync(outputDir, { recursive: true });
262233
}
263234

264-
// Build esbuild arguments
235+
// Build deno bundle arguments
265236
const args = [
237+
"bundle",
238+
`--config=${configPath}`,
239+
`--output=${outputPath}`,
266240
entryPoint,
267-
"--bundle",
268-
"--format=esm",
269-
`--outfile=${outputPath}`,
270241
];
271242

272-
// Add target
273-
const target = config.quartoExtension?.target || "es2022";
274-
args.push(`--target=${target}`);
275-
276243
// Add optional flags
277244
if (config.quartoExtension?.minify) {
278245
args.push("--minify");
279246
}
280247

281248
if (config.quartoExtension?.sourcemap) {
282-
args.push("--sourcemap");
283-
}
284-
285-
// Add external dependencies (not bundled)
286-
if (config.quartoExtension?.externals) {
287-
for (const external of config.quartoExtension.externals) {
288-
args.push(`--external:${external}`);
249+
const sourcemapValue = config.quartoExtension.sourcemap;
250+
if (typeof sourcemapValue === "string") {
251+
args.push(`--sourcemap=${sourcemapValue}`);
252+
} else {
253+
args.push("--sourcemap");
289254
}
290255
}
291256

292257
const result = await execProcess({
293-
cmd: esbuildBinary,
258+
cmd: denoBinary,
294259
args,
295260
cwd: Deno.cwd(),
296261
});
297262

298263
if (!result.success) {
299-
error("Error: esbuild bundling failed");
264+
error("Error: deno bundle failed");
300265
if (result.stderr) {
301266
error(result.stderr);
302267
}
303268
Deno.exit(1);
304269
}
305270

271+
// Validate that _extension.yml path matches output filename
272+
validateExtensionYml(outputPath);
273+
306274
info(`✓ Built ${entryPoint}${outputPath}`);
307275
}
308276

277+
function validateExtensionYml(outputPath: string): void {
278+
// Find _extension.yml in the same directory as output
279+
const extensionDir = dirname(outputPath);
280+
const extensionYmlPath = join(extensionDir, "_extension.yml");
281+
282+
if (!existsSync(extensionYmlPath)) {
283+
return; // No _extension.yml, can't validate
284+
}
285+
286+
try {
287+
const yml = readYaml(extensionYmlPath);
288+
const engines = yml?.contributes?.engines;
289+
290+
if (Array.isArray(engines)) {
291+
const outputFilename = basename(outputPath);
292+
293+
for (const engine of engines) {
294+
const enginePath = typeof engine === "string" ? engine : engine?.path;
295+
if (enginePath && enginePath !== outputFilename) {
296+
warning("");
297+
warning(
298+
`Warning: _extension.yml specifies engine path "${enginePath}" but built file is "${outputFilename}"`,
299+
);
300+
warning(
301+
` Update _extension.yml to: path: ${outputFilename}`,
302+
);
303+
warning("");
304+
}
305+
}
306+
}
307+
} catch {
308+
// Ignore YAML parsing errors
309+
}
310+
}
311+
309312
async function initializeConfig(): Promise<void> {
310313
const configPath = "deno.json";
311314

@@ -385,14 +388,29 @@ export const buildTsExtensionCommand = new Command()
385388
);
386389
info(`Entry point: ${entryPoint}`);
387390

388-
// 3. Type-check with Deno
389-
await typeCheck(entryPoint, configPath);
390-
391-
// 4. Bundle with esbuild (unless --check)
392-
if (!options.check) {
393-
await bundle(entryPoint, config);
391+
// 3. Type-check or bundle
392+
if (options.check) {
393+
// Just type-check
394+
info("Type-checking...");
395+
const denoBinary = Deno.env.get("QUARTO_DENO") ||
396+
architectureToolsPath("deno");
397+
const result = await execProcess({
398+
cmd: denoBinary,
399+
args: ["check", `--config=${configPath}`, entryPoint],
400+
cwd: Deno.cwd(),
401+
});
402+
if (!result.success) {
403+
error("Error: Type check failed");
404+
error("");
405+
error(
406+
"See errors above. Fix type errors in your code or adjust compilerOptions in deno.json.",
407+
);
408+
Deno.exit(1);
409+
}
410+
info("✓ Type check passed");
394411
} else {
395-
info("Skipping bundle (--check flag specified)");
412+
// Type-check and bundle (deno bundle does both)
413+
await bundle(entryPoint, config, configPath);
396414
}
397415
} catch (e) {
398416
if (e instanceof Error) {

src/resources/extension-build/deno.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,5 @@
33
"strict": true,
44
"lib": ["deno.ns", "DOM", "ES2021"]
55
},
6-
"importMap": "./import-map.json",
7-
"quartoExtension": {
8-
"externals": ["jsr:*", "npm:*", "https:*", "http:*"]
9-
}
6+
"importMap": "./import-map.json"
107
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
22
"imports": {
3-
"@quarto/types": "../../../packages/quarto-types/dist/index.d.ts"
3+
"@quarto/types": "../../../packages/quarto-types/dist/index.d.ts",
4+
"path": "jsr:@std/path@1.0.8",
5+
"path/posix": "jsr:@std/path@1.0.8/posix",
6+
"log": "jsr:/@std/log@0.224.0",
7+
"log/": "jsr:/@std/log@0.224.0/",
8+
"fs/": "jsr:/@std/fs@1.0.16/",
9+
"encoding/": "jsr:/@std/encoding@1.0.9/"
410
}
511
}

0 commit comments

Comments
 (0)