Skip to content

Commit b0f892e

Browse files
deyaaeldeenCopilot
andauthored
[warp] Migrate core packages to warp with type-compatible browser polyfills (#37442)
## [warp] Migrate core packages to warp with type-compatible browser polyfills ### Root tsconfig changes - `tsconfig.src.esm.json`: Replace `WebWorker` with `DOM` + `DOM.Iterable` in `lib` - `tsconfig.src.cjs.json`: Add `DOM` + `DOM.Iterable` to `lib` (was `ES2023` only) ### `@azure/core-amqp` - Add `warp.config.yml`, remove `tshy` config and devDependency - Add `checkNetworkConnection.common.ts` browser polyfill ### `@azure/core-client` - Add `warp.config.yml`, remove `tshy` config and devDependency - Add `import type` annotations to `state-cjs.cts` so tsc can type-check the polyfill content when it is substituted under the `.ts` filename by warp's polyfill host - Update `vitest.config.ts` alias to point to compiled `dist/commonjs/state.js` instead of source `state-cjs.cts` (Vite/Rollup treats `.cts` as JavaScript and cannot parse TypeScript syntax like `import type`) ### `@azure/core-rest-pipeline` - Add `warp.config.yml`, remove `tshy` config and devDependency ### `@azure/core-tracing` - Add `warp.config.yml`, remove `tshy` config and devDependency - Add `import type` annotations to `state-cjs.cts` so tsc can type-check the polyfill content when it is substituted under the `.ts` filename by warp's polyfill host - Update `vitest.config.ts` alias to point to compiled `dist/commonjs/state.js` instead of source `state-cjs.cts` (Vite/Rollup treats `.cts` as JavaScript and cannot parse TypeScript syntax like `import type`) ### `@azure/core-xml` - Add `warp.config.yml`, remove `tshy` config and devDependency - Add local `tsconfig.src.browser.json` with `trusted-types` in `types` (needed by `xml-browser.mts`) - Remove triple-slash reference directives from `xml-browser.mts` ### `@typespec/ts-http-runtime` - Add `warp.config.yml`, remove `tshy` config and devDependency - Add/update browser polyfills: `decompressResponsePolicy-browser.mts`, `proxyPolicy.common.ts`, `concat.common.ts` - Update API diff reports for browser and react-native targets - `concat.common.ts`: Export `ConcatSource` type and add `NodeJS.ReadableStream` to its union. The browser polyfill (`concat-browser.mts`) re-exports from `concat.common.ts`. When warp type-checks the browser target, `multipartPolicy.ts` passes `BodyPart.body` — whose type includes `NodeJS.ReadableStream` — into `concat()`. So `ConcatSource` must accept that type to satisfy the type-checker. tshy never caught this mismatch because it did not type-check browser polyfills against their callers. The added union member is unreachable at runtime in browser environments — the function already throws for unsupported source types. ### warp changes - Discover `.cts` polyfill files (search order: `.mts` → `.cts` → `.ts`) - Add `polyfillSuffix: "-cjs"` support for CJS-specific polyfills (tshy's module-local-state pattern) - Add `platform: "node"` to esbuild CJS transform so it emits `0 && (module.exports = {...})` sentinel for Node's `cjs-module-lexer` to detect named exports in ESM→CJS interop - Type-check `.cts` polyfill content by substituting it under the original `.ts` filename via the polyfill host — tsc sees TypeScript, not a `.cts` extension ### Why vitest configs changed The `state.ts` files in core-client and core-tracing use tshy's module-local-state pattern: the ESM entry (`state.ts`) re-exports from `../commonjs/state.js` so that ESM and CJS share a single state object (preventing dual-package hazard). During tests, vitest resolves `../commonjs/state.js` via an alias. Previously the alias pointed to the source `state-cjs.cts` file, but Vite/Rollup treats `.cts` as JavaScript and chokes on TypeScript syntax (`import type`). The fix is to alias to the compiled `dist/commonjs/state.js` instead. ### Why `.cts` files need type annotations Warp's polyfill host substitutes the `.cts` file's *content* under the original `.ts` *filename*, so tsc type-checks it as TypeScript. The original `.cts` files used `any`-typed variables (e.g. `let state: any`). Adding proper `import type` declarations and explicit type annotations (e.g. `as Instrumenter | undefined`, `WeakMap<OperationRequest, OperationRequestInfo>`) ensures the polyfill content is fully type-checked at build time, catching mismatches between the CJS and ESM implementations. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4a2ece1 commit b0f892e

29 files changed

Lines changed: 435 additions & 399 deletions

common/tools/warp/src/compiler.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,13 @@ export async function discoverPolyfills(
285285
continue;
286286
}
287287

288+
// Check .cts polyfill (e.g. state-cjs.cts for CJS module-local-state)
289+
const ctsName = `${stem}${suffix}.cts`;
290+
if (entries.has(ctsName)) {
291+
polyfillMap.set(path.resolve(fileName), path.join(dir, ctsName));
292+
continue;
293+
}
294+
288295
// Fall back to .ts polyfill
289296
const tsName = `${stem}${suffix}.ts`;
290297
if (entries.has(tsName)) {
@@ -609,12 +616,18 @@ async function transpileWithEsbuild(
609616
const target = tsTargetToEsbuild(options.target);
610617
const sourcemap = options.sourceMap !== false;
611618

619+
// platform:"node" makes esbuild annotate CJS output with
620+
// `0 && (module.exports = {...})` so that Node's cjs-module-lexer
621+
// can statically detect named exports for ESM→CJS interop.
622+
const platform: esbuild.Platform | undefined = format === "cjs" ? "node" : undefined;
623+
612624
return Promise.all(
613625
sources.map(async ({ fileName, content }) => {
614626
const result = await esbuild.transform(content, {
615627
loader: "ts",
616628
format,
617629
target,
630+
platform,
618631
sourcemap: sourcemap ? "external" : false,
619632
sourcefile: fileName,
620633
});
@@ -727,13 +740,13 @@ export async function transpileFiles(
727740
/**
728741
* Filter rootNames to exclude polyfill source files.
729742
* Uses basename-aware matching: a file is a polyfill if its basename
730-
* (without extension) ends with the suffix and the extension is .mts or .ts.
743+
* (without extension) ends with the suffix and the extension is .mts, .cts, or .ts.
731744
*/
732745
function filterPolyfillRootNames(rootNames: readonly string[], suffix: string): string[] {
733746
return rootNames.filter((f) => {
734747
const base = path.basename(f);
735748
const ext = path.extname(base);
736-
if (ext !== ".mts" && ext !== ".ts") return true;
749+
if (ext !== ".mts" && ext !== ".cts" && ext !== ".ts") return true;
737750
const stem = base.slice(0, -ext.length);
738751
return !stem.endsWith(suffix);
739752
});
@@ -925,7 +938,7 @@ export async function compileAllTargets(
925938
);
926939

927940
// Fast path: when type-checking and declarations are both skipped,
928-
// use transpileFiles (ts.transpileModule per-file) to bypass program
941+
// use transpileFiles (esbuild per-file transpilation) to bypass program
929942
// creation entirely — ~3-10× faster for format-only re-emit.
930943
let primaryResult: CompileResult;
931944
if (!needsTypeCheck && canSkipDeclarations) {

common/tools/warp/test/public/polyfill.spec.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,47 @@ describe("discoverPolyfills", () => {
9191
expect(map.size).toBe(1);
9292
expect(map.get(path.join(tmpDir, "src/foo.ts"))).toBe(path.join(tmpDir, "src/foo-browser.mts"));
9393
});
94+
95+
it("discovers .cts polyfill files", async () => {
96+
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true });
97+
await fs.writeFile(path.join(tmpDir, "src/state.ts"), "export const state = { x: undefined };");
98+
await fs.writeFile(
99+
path.join(tmpDir, "src/state-cjs.cts"),
100+
"export const state = { x: undefined };",
101+
);
102+
103+
const fileNames = [path.join(tmpDir, "src/state.ts")];
104+
const map = await discoverPolyfills(fileNames, "-cjs");
105+
expect(map.size).toBe(1);
106+
expect(map.get(path.join(tmpDir, "src/state.ts"))).toBe(
107+
path.join(tmpDir, "src/state-cjs.cts"),
108+
);
109+
});
110+
111+
it("prefers .mts over .cts over .ts polyfill", async () => {
112+
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true });
113+
await fs.writeFile(path.join(tmpDir, "src/foo.ts"), "export const x = 1;");
114+
await fs.writeFile(path.join(tmpDir, "src/foo-cjs.mts"), "export const x = 2; // mts");
115+
await fs.writeFile(path.join(tmpDir, "src/foo-cjs.cts"), "export const x = 3; // cts");
116+
await fs.writeFile(path.join(tmpDir, "src/foo-cjs.ts"), "export const x = 4; // ts");
117+
118+
const fileNames = [path.join(tmpDir, "src/foo.ts")];
119+
const map = await discoverPolyfills(fileNames, "-cjs");
120+
expect(map.size).toBe(1);
121+
expect(map.get(path.join(tmpDir, "src/foo.ts"))).toBe(path.join(tmpDir, "src/foo-cjs.mts"));
122+
});
123+
124+
it("prefers .cts over .ts polyfill when no .mts exists", async () => {
125+
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true });
126+
await fs.writeFile(path.join(tmpDir, "src/foo.ts"), "export const x = 1;");
127+
await fs.writeFile(path.join(tmpDir, "src/foo-cjs.cts"), "export const x = 3; // cts");
128+
await fs.writeFile(path.join(tmpDir, "src/foo-cjs.ts"), "export const x = 4; // ts");
129+
130+
const fileNames = [path.join(tmpDir, "src/foo.ts")];
131+
const map = await discoverPolyfills(fileNames, "-cjs");
132+
expect(map.size).toBe(1);
133+
expect(map.get(path.join(tmpDir, "src/foo.ts"))).toBe(path.join(tmpDir, "src/foo-cjs.cts"));
134+
});
94135
});
95136

96137
describe("polyfill substitution build", () => {
@@ -326,4 +367,200 @@ describe("polyfill substitution build", () => {
326367
// They must NOT be identical (dedup should not have fired)
327368
expect(esmIndex).not.toBe(browserIndex);
328369
});
370+
371+
it("substitutes .cts polyfill content in CJS target with full type-checking", async () => {
372+
// Source files — state.ts defines a typed state object, state-cjs.cts provides
373+
// a type-safe CJS version. This mirrors the tshy module-local-state pattern
374+
// used in core-tracing and core-client.
375+
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true });
376+
await fs.writeFile(
377+
path.join(tmpDir, "src/index.ts"),
378+
['import { state } from "./state.js";', "export { state };"].join("\n"),
379+
);
380+
// Types in a separate file (like interfaces.ts in core-tracing)
381+
await fs.writeFile(
382+
path.join(tmpDir, "src/types.ts"),
383+
"export interface Widget { name: string; }",
384+
);
385+
await fs.writeFile(
386+
path.join(tmpDir, "src/state.ts"),
387+
[
388+
'import type { Widget } from "./types.js";',
389+
"export const state = {",
390+
" current: undefined as Widget | undefined,",
391+
"};",
392+
].join("\n"),
393+
);
394+
// CJS polyfill — has proper TypeScript types for type-checking
395+
await fs.writeFile(
396+
path.join(tmpDir, "src/state-cjs.cts"),
397+
[
398+
'import type { Widget } from "./types.js";',
399+
"export const state = {",
400+
" current: undefined as Widget | undefined,",
401+
"};",
402+
].join("\n"),
403+
);
404+
405+
// tsconfigs
406+
const esmTsconfig = {
407+
compilerOptions: {
408+
outDir: "./dist/esm",
409+
rootDir: "./src",
410+
module: "NodeNext",
411+
moduleResolution: "NodeNext",
412+
target: "ES2023",
413+
declaration: true,
414+
strict: true,
415+
},
416+
include: ["src/**/*.ts"],
417+
};
418+
const cjsTsconfig = {
419+
compilerOptions: {
420+
outDir: "./dist/commonjs",
421+
rootDir: "./src",
422+
module: "CommonJS",
423+
moduleResolution: "Node10",
424+
target: "ES2023",
425+
declaration: true,
426+
strict: true,
427+
},
428+
include: ["src/**/*.ts"],
429+
};
430+
431+
await fs.writeFile(path.join(tmpDir, "tsconfig.esm.json"), JSON.stringify(esmTsconfig));
432+
await fs.writeFile(path.join(tmpDir, "tsconfig.cjs.json"), JSON.stringify(cjsTsconfig));
433+
434+
// Warp config — ESM compiled normally, CJS uses -cjs polyfill
435+
await fs.writeFile(
436+
path.join(tmpDir, "warp.config.yml"),
437+
stringify({
438+
exports: { ".": "./src/index.ts" },
439+
targets: [
440+
{ name: "esm", condition: "import", tsconfig: "./tsconfig.esm.json" },
441+
{
442+
name: "commonjs",
443+
condition: "require",
444+
tsconfig: "./tsconfig.cjs.json",
445+
polyfillSuffix: "-cjs",
446+
},
447+
],
448+
}),
449+
);
450+
451+
await fs.writeFile(
452+
path.join(tmpDir, "package.json"),
453+
`${JSON.stringify({ name: "test-cts-polyfill", version: "1.0.0", type: "module" }, null, 2)}\n`,
454+
);
455+
await fs.writeFile(path.join(tmpDir, "pnpm-workspace.yaml"), "packages: []");
456+
457+
const result = await build({ cwd: tmpDir });
458+
expect(result.success).toBe(true);
459+
460+
// ESM state.js should have the compiled version
461+
const esmState = await fs.readFile(path.join(tmpDir, "dist/esm/state.js"), "utf-8");
462+
expect(esmState).toContain("undefined");
463+
464+
// CJS state.js should have the polyfill content (from state-cjs.cts)
465+
const cjsState = await fs.readFile(path.join(tmpDir, "dist/commonjs/state.js"), "utf-8");
466+
expect(cjsState).toMatch(/undefined|void 0/);
467+
// CJS should use tsc's CommonJS exports pattern (Node ESM-interop compatible)
468+
expect(cjsState).toContain("exports");
469+
470+
// CJS target should NOT produce state-cjs.cjs (polyfill is filtered)
471+
expect(await exists(path.join(tmpDir, "dist/commonjs/state-cjs.cjs"))).toBe(false);
472+
473+
// CJS .d.ts should have full type info (type-checked from polyfill content)
474+
const cjsDts = await fs.readFile(path.join(tmpDir, "dist/commonjs/state.d.ts"), "utf-8");
475+
expect(cjsDts).toContain("Widget");
476+
expect(cjsDts).toContain("undefined");
477+
});
478+
479+
it("catches type errors in .cts polyfill files", async () => {
480+
await fs.mkdir(path.join(tmpDir, "src"), { recursive: true });
481+
// Types in a separate file
482+
await fs.writeFile(
483+
path.join(tmpDir, "src/types.ts"),
484+
"export interface Widget { name: string; }",
485+
);
486+
await fs.writeFile(
487+
path.join(tmpDir, "src/state.ts"),
488+
[
489+
'import type { Widget } from "./types.js";',
490+
"export const state = {",
491+
" current: undefined as Widget | undefined,",
492+
"};",
493+
].join("\n"),
494+
);
495+
// A consumer that assigns to state.current — requires Widget | undefined type
496+
await fs.writeFile(
497+
path.join(tmpDir, "src/index.ts"),
498+
[
499+
'import type { Widget } from "./types.js";',
500+
'import { state } from "./state.js";',
501+
"export function setWidget(w: Widget): void { state.current = w; }",
502+
"export { state };",
503+
].join("\n"),
504+
);
505+
// CJS polyfill with WRONG type — narrower than what setWidget expects
506+
await fs.writeFile(
507+
path.join(tmpDir, "src/state-cjs.cts"),
508+
["export const state = {", " current: undefined,", "};"].join("\n"),
509+
);
510+
511+
const esmTsconfig = {
512+
compilerOptions: {
513+
outDir: "./dist/esm",
514+
rootDir: "./src",
515+
module: "NodeNext",
516+
moduleResolution: "NodeNext",
517+
target: "ES2023",
518+
declaration: true,
519+
strict: true,
520+
},
521+
include: ["src/**/*.ts"],
522+
};
523+
const cjsTsconfig = {
524+
compilerOptions: {
525+
outDir: "./dist/commonjs",
526+
rootDir: "./src",
527+
module: "CommonJS",
528+
moduleResolution: "Node10",
529+
target: "ES2023",
530+
declaration: true,
531+
strict: true,
532+
},
533+
include: ["src/**/*.ts"],
534+
};
535+
536+
await fs.writeFile(path.join(tmpDir, "tsconfig.esm.json"), JSON.stringify(esmTsconfig));
537+
await fs.writeFile(path.join(tmpDir, "tsconfig.cjs.json"), JSON.stringify(cjsTsconfig));
538+
539+
await fs.writeFile(
540+
path.join(tmpDir, "warp.config.yml"),
541+
stringify({
542+
exports: { ".": "./src/index.ts" },
543+
targets: [
544+
{ name: "esm", condition: "import", tsconfig: "./tsconfig.esm.json" },
545+
{
546+
name: "commonjs",
547+
condition: "require",
548+
tsconfig: "./tsconfig.cjs.json",
549+
polyfillSuffix: "-cjs",
550+
},
551+
],
552+
}),
553+
);
554+
555+
await fs.writeFile(
556+
path.join(tmpDir, "package.json"),
557+
`${JSON.stringify({ name: "test-type-error", version: "1.0.0", type: "module" }, null, 2)}\n`,
558+
);
559+
await fs.writeFile(path.join(tmpDir, "pnpm-workspace.yaml"), "packages: []");
560+
561+
const result = await build({ cwd: tmpDir });
562+
// Build should fail because the CJS polyfill has narrow type for state.current
563+
// (just `undefined`) but index.ts assigns a Widget to it
564+
expect(result.success).toBe(false);
565+
});
329566
});

sdk/core/core-amqp/package.json

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -96,29 +96,12 @@
9696
"playwright": "catalog:testing",
9797
"prettier": "catalog:",
9898
"rimraf": "catalog:",
99-
"tshy": "catalog:",
10099
"typescript": "catalog:",
101100
"vitest": "catalog:testing",
102101
"ws": "^8.17.0"
103102
},
104103
"//metadata": {
105104
"migrationDate": "2023-03-08T18:36:03.000Z"
106105
},
107-
"tshy": {
108-
"exports": {
109-
"./package.json": "./package.json",
110-
".": "./src/index.ts"
111-
},
112-
"dialects": [
113-
"esm",
114-
"commonjs"
115-
],
116-
"esmDialects": [
117-
"browser",
118-
"react-native"
119-
],
120-
"selfLink": false,
121-
"project": "../../../tsconfig.src.build.json"
122-
},
123106
"module": "./dist/esm/index.js"
124107
}

sdk/core/core-amqp/src/util/checkNetworkConnection.common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
* Checks whether a network connection is detected.
66
* @internal
77
*/
8-
export function checkNetworkConnection(): Promise<boolean> {
8+
export function checkNetworkConnection(_host: string): Promise<boolean> {
99
return Promise.resolve(self.navigator.onLine);
1010
}

sdk/core/core-amqp/warp.config.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# warp.config.yml — @azure/core-amqp build configuration
2+
3+
exports:
4+
"./package.json": "./package.json"
5+
".": "./src/index.ts"
6+
7+
targets:
8+
- name: browser
9+
tsconfig: "../../../tsconfig.src.browser.json"
10+
polyfillSuffix: "-browser"
11+
12+
- name: react-native
13+
tsconfig: "../../../tsconfig.src.react-native.json"
14+
polyfillSuffix: "-react-native"
15+
16+
- name: esm
17+
condition: import
18+
tsconfig: "../../../tsconfig.src.esm.json"
19+
20+
- name: commonjs
21+
condition: require
22+
tsconfig: "../../../tsconfig.src.cjs.json"

sdk/core/core-client/package.json

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -88,28 +88,11 @@
8888
"playwright": "catalog:testing",
8989
"prettier": "catalog:",
9090
"rimraf": "catalog:",
91-
"tshy": "catalog:",
9291
"typescript": "catalog:",
9392
"vitest": "catalog:testing"
9493
},
9594
"//metadata": {
9695
"migrationDate": "2023-03-08T18:36:03.000Z"
9796
},
98-
"tshy": {
99-
"exports": {
100-
"./package.json": "./package.json",
101-
".": "./src/index.ts"
102-
},
103-
"dialects": [
104-
"esm",
105-
"commonjs"
106-
],
107-
"esmDialects": [
108-
"browser",
109-
"react-native"
110-
],
111-
"selfLink": false,
112-
"project": "../../../tsconfig.src.build.json"
113-
},
11497
"module": "./dist/esm/index.js"
11598
}

0 commit comments

Comments
 (0)