diff --git a/.chronus/changes/feature-http-client-js-playground-2026-2-26-18-19-36.md b/.chronus/changes/feature-http-client-js-playground-2026-2-26-18-19-36.md new file mode 100644 index 00000000000..c2c4bf451c1 --- /dev/null +++ b/.chronus/changes/feature-http-client-js-playground-2026-2-26-18-19-36.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/bundler" +--- + +Add support for alloy based emitters diff --git a/eng/tsp-core/scripts/upload-bundler-packages.js b/eng/tsp-core/scripts/upload-bundler-packages.js index 0d443596bca..dd25eddf096 100644 --- a/eng/tsp-core/scripts/upload-bundler-packages.js +++ b/eng/tsp-core/scripts/upload-bundler-packages.js @@ -22,5 +22,6 @@ await bundleAndUploadPackages({ "@typespec/events", "@typespec/sse", "@typespec/xml", + "@typespec/http-client-js", ], }); diff --git a/packages/bundler/src/bundler.ts b/packages/bundler/src/bundler.ts index 3a62a7129b6..367c42ad482 100644 --- a/packages/bundler/src/bundler.ts +++ b/packages/bundler/src/bundler.ts @@ -1,8 +1,7 @@ import { compile, joinPaths, NodeHost, normalizePath, resolvePath } from "@typespec/compiler"; import { BuildOptions, BuildResult, context, Plugin } from "esbuild"; -import { nodeModulesPolyfillPlugin } from "esbuild-plugins-node-modules-polyfill"; -import { mkdir, readFile, realpath, writeFile } from "fs/promises"; -import { basename, join, resolve } from "path"; +import { access, mkdir, readFile, realpath, writeFile } from "fs/promises"; +import { basename, dirname, join, resolve } from "path"; import { promisify } from "util"; import { gzip } from "zlib"; import { relativeTo } from "./utils.js"; @@ -219,6 +218,8 @@ async function createEsBuildContext( }), ); + const externalPeerDeps = await resolveExternalPeerDependencies(libraryPath, definition); + const virtualPlugin: Plugin = { name: "virtual", setup(build) { @@ -229,10 +230,7 @@ async function createEsBuildContext( }; }); build.onResolve({ filter: /.*/ }, (args) => { - if ( - definition.packageJson.peerDependencies && - Object.keys(definition.packageJson.peerDependencies).some((x) => args.path.startsWith(x)) - ) { + if (externalPeerDeps.some((x) => args.path === x || args.path.startsWith(x + "/"))) { return { path: args.path, external: true }; } return null; @@ -246,6 +244,25 @@ async function createEsBuildContext( }); }, }; + + // When containing alloy-js, namespace its globalThis.__ALLOY__ singleton + // guard so that multiple contained bundles can coexist in the same process. + const alloySingletonPlugin: Plugin = { + name: "alloy-singleton-namespace", + setup(build) { + build.onLoad({ filter: /reactivity\.[jt]s$/ }, async (args) => { + if (!args.path.includes("@alloy-js/core")) return undefined; + const source = await readFile(args.path, "utf-8"); + const namespaceKey = `__ALLOY_${definition.packageJson.name.replace(/[^a-zA-Z0-9]/g, "_")}__`; + const patched = source.replaceAll("__ALLOY__", namespaceKey); + return { + contents: patched, + loader: args.path.endsWith(".ts") ? "ts" : "js", + }; + }); + }, + }; + return await context({ write: false, entryPoints: { @@ -260,7 +277,10 @@ async function createEsBuildContext( target: "es2024", minify, keepNames: minify, - plugins: [virtualPlugin, nodeModulesPolyfillPlugin({}), ...plugins], + define: { + "process.env": "{}", + }, + plugins: [virtualPlugin, alloySingletonPlugin, ...plugins], }); } @@ -312,6 +332,55 @@ async function readLibraryPackageJson(path: string): Promise { return JSON.parse(file.toString()); } +/** + * Resolve which peer dependencies should be treated as external. + * Only peer dependencies that are TypeSpec libraries (have `tspMain` in their package.json) + * are externalized. Non-TypeSpec peer dependencies (e.g. alloy-js) are bundled inline. + */ +async function resolveExternalPeerDependencies( + libraryPath: string, + definition: TypeSpecBundleDefinition, +): Promise { + const peerDeps = definition.packageJson.peerDependencies; + if (!peerDeps) { + return []; + } + + const peerDepNames = Object.keys(peerDeps); + const externalDeps: string[] = []; + + for (const depName of peerDepNames) { + const isTypeSpec = await isTypeSpecLibrary(libraryPath, depName); + if (isTypeSpec) { + externalDeps.push(depName); + } + } + + return externalDeps; +} + +async function isTypeSpecLibrary(libraryPath: string, depName: string): Promise { + // Walk up from the library path checking node_modules at each level. + // This avoids require.resolve which fails when packages have exports maps + // that don't expose ./package.json. + let dir = libraryPath; + while (true) { + const pkgJsonPath = join(dir, "node_modules", depName, "package.json"); + try { + await access(pkgJsonPath); + } catch { + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + continue; + } + const depPkgJson = JSON.parse((await readFile(pkgJsonPath)).toString()); + return !!depPkgJson.tspMain; + } + // If we can't resolve the package, externalize to be safe (preserves previous behavior) + return true; +} + /** * Create a virtual JS file being the entrypoint of the bundle. */ diff --git a/packages/playground-website/package.json b/packages/playground-website/package.json index 0fe44bf3678..437568221bc 100644 --- a/packages/playground-website/package.json +++ b/packages/playground-website/package.json @@ -59,6 +59,7 @@ "@typespec/events": "workspace:^", "@typespec/html-program-viewer": "workspace:^", "@typespec/http": "workspace:^", + "@typespec/http-client-js": "workspace:^", "@typespec/json-schema": "workspace:^", "@typespec/openapi": "workspace:^", "@typespec/openapi3": "workspace:^", diff --git a/packages/playground-website/src/config.ts b/packages/playground-website/src/config.ts index ee32fdfa694..d1f23fc8a6b 100644 --- a/packages/playground-website/src/config.ts +++ b/packages/playground-website/src/config.ts @@ -15,6 +15,7 @@ export const TypeSpecPlaygroundConfig = { "@typespec/events", "@typespec/sse", "@typespec/xml", + "@typespec/http-client-js", ], samples, } as const; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d155a68b27..e21ab2eb8a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,9 +9,6 @@ catalogs: '@alloy-js/cli': specifier: ^0.23.0 version: 0.23.0 - '@alloy-js/core': - specifier: ^0.23.0 - version: 0.23.0 '@alloy-js/csharp': specifier: ^0.23.0 version: 0.23.0 @@ -522,6 +519,7 @@ overrides: diff@>=6.0.0 <8.0.3: '>=8.0.3' dompurify: ^3.3.3 yaml@>=2.0.0 <2.8.3: '>=2.8.3' + '@alloy-js/core': 0.23.1 importers: @@ -898,8 +896,8 @@ importers: specifier: 'catalog:' version: 0.23.0 '@alloy-js/core': - specifier: 'catalog:' - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/python': specifier: 'catalog:' version: 0.4.0 @@ -1153,8 +1151,8 @@ importers: specifier: 'catalog:' version: 0.23.0 '@alloy-js/core': - specifier: 'catalog:' - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/rollup-plugin': specifier: 'catalog:' version: 0.1.1(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.60.1) @@ -1189,8 +1187,8 @@ importers: packages/http-client-js: dependencies: '@alloy-js/core': - specifier: 'catalog:' - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/typescript': specifier: 'catalog:' version: 0.23.0 @@ -1978,6 +1976,9 @@ importers: '@typespec/http': specifier: workspace:^ version: link:../http + '@typespec/http-client-js': + specifier: workspace:^ + version: link:../http-client-js '@typespec/json-schema': specifier: workspace:^ version: link:../json-schema @@ -2677,8 +2678,8 @@ importers: packages/tspd: dependencies: '@alloy-js/core': - specifier: 'catalog:' - version: 0.23.0 + specifier: 0.23.1 + version: 0.23.1 '@alloy-js/markdown': specifier: 'catalog:' version: 0.23.0 @@ -3110,8 +3111,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@alloy-js/core@0.23.0': - resolution: {integrity: sha512-pbr0a1vMLQgdgUUponGQ6tzZ63fo20yjAANr+FH6OSs8yih1C16fU3Jc02ok247jah7r1obwbzcwwpqi3IAb8w==} + '@alloy-js/core@0.23.1': + resolution: {integrity: sha512-zbPXbiWjF3BvgO+a6dyDiA9uI19x5cHKDy5iC3U2/DIimIB0/j1/ObT9BNIzcM69h5pYt7ADDGXx5eNyb8j7bw==} '@alloy-js/csharp@0.23.0': resolution: {integrity: sha512-z/uc3PP0toaBkP8GoWM0SSVZ8TiC0KO9g8O/hFnBKcWZGpmA7JT0Oe5moRn2QDBf+OZZzuCH5XGDwcMPWTS5PQ==} @@ -13735,7 +13736,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@alloy-js/core@0.23.0': + '@alloy-js/core@0.23.1': dependencies: '@types/ws': 8.18.1 '@vue/reactivity': 3.5.30 @@ -13751,7 +13752,7 @@ snapshots: '@alloy-js/csharp@0.23.0': dependencies: - '@alloy-js/core': 0.23.0 + '@alloy-js/core': 0.23.1 '@alloy-js/msbuild': 0.23.0 change-case: 5.4.4 marked: 16.4.2 @@ -13762,7 +13763,7 @@ snapshots: '@alloy-js/markdown@0.23.0': dependencies: - '@alloy-js/core': 0.23.0 + '@alloy-js/core': 0.23.1 yaml: 2.8.3 transitivePeerDependencies: - bufferutil @@ -13770,7 +13771,7 @@ snapshots: '@alloy-js/msbuild@0.23.0': dependencies: - '@alloy-js/core': 0.23.0 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 marked: 16.4.2 pathe: 2.0.3 @@ -13780,7 +13781,7 @@ snapshots: '@alloy-js/python@0.4.0': dependencies: - '@alloy-js/core': 0.23.0 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: @@ -13800,7 +13801,7 @@ snapshots: '@alloy-js/typescript@0.23.0': dependencies: - '@alloy-js/core': 0.23.0 + '@alloy-js/core': 0.23.1 change-case: 5.4.4 pathe: 2.0.3 transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e7f215fea2f..7f88f067b61 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -188,3 +188,4 @@ overrides: diff@>=6.0.0 <8.0.3: ">=8.0.3" dompurify: ^3.3.3 yaml@>=2.0.0 <2.8.3: ">=2.8.3" + "@alloy-js/core": 0.23.1