Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
54d4859
Move the TypeScript surfaces onto the TypeScript 7 bridge
Aug 6, 2026
8e49f2d
Close the gaps the TypeScript 7 bridge left behind
Aug 7, 2026
9109d38
Parse lockfile URLs instead of substring-matching, and guard bun/pnpm…
Aug 7, 2026
8b305ae
Route lockfile changes to the guard that checks them
Aug 7, 2026
86c166a
Point every package manager at the approved feed before installing
Aug 7, 2026
59da954
Ship the registry helper in the validation image
Aug 7, 2026
5ace3e7
Record the shipped lockfiles the guard does not yet cover
Aug 7, 2026
2b89750
Pin the origins the unnormalized lockfiles are allowed to use
Aug 7, 2026
a4c07f3
Stop ambient scoped registries from redirecting fixture installs
Aug 7, 2026
e23c221
Stop aspire init producing an uninstallable manifest on TypeScript 7
Aug 7, 2026
9eb7c5e
Ask yarn its version in the directory the registry check reads
Aug 7, 2026
fff3a50
Reject unapproved npm registry overrides
Aug 8, 2026
6d9969d
Pin Azure Functions Core Tools install in tests
Aug 8, 2026
f48bb4e
Revert "Pin Azure Functions Core Tools install in tests"
Aug 8, 2026
2c5b6ca
Make registry source-order guard check source command
Aug 8, 2026
cab6897
Pin TypeScript bridge dependency versions
Aug 8, 2026
31f6107
Treat unknown TypeScript specs as lint-incompatible
Aug 8, 2026
03a924b
Synchronize TypeScript SDK test lockfile
Aug 8, 2026
ebed819
Keep TypeScript lint scaffolding installable
Aug 8, 2026
899defd
Fix TypeScript bridge review gaps
adamint Aug 9, 2026
d6c4757
Route polyglot lockfiles by shape so the unknown-format guard can run
adamint Aug 9, 2026
5748bf8
Route the polyglot fixture tree instead of lockfile shape
adamint Aug 9, 2026
0253642
Own the npm config in Dockerfile.typescript and route every shipped l…
Aug 9, 2026
5984803
Reject Yarn scoped registries that are not the approved feed
Aug 9, 2026
497d4c2
Fix the orphaned Dockerfile block and catch the shape in a test
Aug 9, 2026
72fc837
Prove the npm scope re-pin won and stop the yarn scope check failing …
Aug 9, 2026
8aa1219
Close the bun and corepack registry holes in the polyglot preflight
Aug 10, 2026
b552aa3
Reject fixture dependency specs that bypass the registry
Aug 10, 2026
ff92d95
Pin the TypeScript scaffold to versions the approved feed can serve
Aug 10, 2026
3d5d26c
Leave a project-owned typescript-eslint spec alone and scan configs b…
Copilot Aug 10, 2026
eab854d
Close the dependency-spec bypasses the fixture guard still had
Aug 10, 2026
9bcc3dd
Read bun's ambient config by URL instead of by line shape
Aug 10, 2026
24e3a88
Move the linter with the compiler, and ship the lockfiles to Helix
Copilot Aug 10, 2026
b46e312
Decide TypeScript support by the range's upper bound and move npm ove…
Copilot Aug 10, 2026
0b1607b
Move a self-scoped override entry too
Copilot Aug 10, 2026
b59b291
Compute npm range bounds from the components a literal spells out
adamint Aug 10, 2026
bb84866
Reconcile a project's linter spec when the merge upgrades TypeScript
adamint Aug 10, 2026
6c68397
Reconcile an npm override against every direct spec the merge writes
Aug 10, 2026
e6258de
Merge branch 'main' into adamint/issue18905-ts7-bridge
adamint Aug 10, 2026
f9583b5
Keep the TypeScript 7 bridge focused
Aug 10, 2026
0d02f95
Preserve compatible brownfield npm ranges
Aug 11, 2026
dd4785a
Merge branch main into adamint/issue18905-ts7-bridge
Aug 14, 2026
26e7932
Keep starter lockfiles customer-reachable
Aug 14, 2026
3807f2d
Include the Rust fixture in the TypeScript bridge
Aug 14, 2026
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
19 changes: 14 additions & 5 deletions .agents/skills/vscode-extension/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,23 @@ local iteration `yarn run package` plus the launch configs below is usually enou
The extension has Mocha unit tests under `extension/src/test/` executed by `@vscode/test-cli`.

```bash
yarn run compile-tests # tsc -> ./out (or watch-tests to keep rebuilding)
yarn run test # alias for unit-test (vscode-test); runs lint+compile via pretest
yarn run compile-tests # tsc6 -> ./out (or watch-tests to keep rebuilding)
yarn run test # alias for unit-test (vscode-test); runs lint+compile+typecheck via pretest
yarn run lint # eslint src
yarn run typecheck # TypeScript 7 native compiler, --noEmit
```

`pretest` runs `compile-tests`, `compile`, and `lint`, so `yarn run test` is the single command that
builds and runs everything. When iterating on one area, keep `watch-tests` running and re-run
`yarn run test`.
`pretest` runs `compile-tests`, `compile`, `lint`, and `typecheck`, so `yarn run test` is the single
command that builds and runs everything. When iterating on one area, keep `watch-tests` running and
re-run `yarn run test`.

The extension installs two TypeScript compilers side by side: `typescript` is aliased to
`npm:@typescript/typescript6` (the TypeScript 6.0 JavaScript API, plus a `tsc6` binary) because
`editor/parsers/jsTsAppHostParser.ts`, `test/telemetryInventory.test.ts`, ts-loader,
gulp-typescript, and typescript-eslint all need that API, while `@typescript/native` is aliased to
`npm:typescript@7` for the native compiler used by `yarn run typecheck`. That is why the emitting
scripts call `tsc6` rather than `tsc`. See
[extension/CONTRIBUTING.md](../../../extension/CONTRIBUTING.md) for the full rationale.

Add new tests as `extension/src/test/<area>.test.ts` mirroring nearby tests (e.g.
`appHostDiscovery.test.ts`, `strings.test.ts`). There is a `strings.test.ts` that guards
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/polyglot-validation/Dockerfile.typescript
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
ARG NODE_VERSION=24
FROM mcr.microsoft.com/devcontainers/typescript-node:${NODE_VERSION}
ARG NODE_VERSION
ARG TSGO_VERSION=7.0.0-dev.20260523.1
ARG TYPESCRIPT_VERSION=7.0.2
ARG NPM_REGISTRY=https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/

# Ensure Yarn APT repository signing key is available (base image includes Yarn repo)
Expand All @@ -32,15 +32,19 @@ RUN case "$NODE_VERSION" in 22|24) ;; *) echo "Unsupported NODE_VERSION: $NODE_V
# package-manager markers, so make all selected tools available in the image.
# Install each tool directly because Corepack's package-manager downloads bypass
# NPM_REGISTRY and would acquire pnpm and Yarn outside dotnet-public-npm.
#
# TypeScript 7 (the native Go compiler) ships as the plain `typescript` package with a `tsc` binary
# since 7.0 GA, replacing the pre-GA `@typescript/native-preview` package and its `tsgo` binary.
# See https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/.
RUN npm install --global --force --registry "${NPM_REGISTRY}" \
pnpm@10.0.0 \
@yarnpkg/cli-dist@4.14.1 \
bun@1.2.0 \
"@typescript/native-preview@${TSGO_VERSION}" && \
"typescript@${TYPESCRIPT_VERSION}" && \
Comment thread
adamint marked this conversation as resolved.
test "$(pnpm --version)" = "10.0.0" && \
test "$(yarn --version)" = "4.14.1" && \
test "$(bun --version)" = "1.2.0" && \
test "$(tsgo --version)" = "Version ${TSGO_VERSION}"
test "$(tsc --version)" = "Version ${TYPESCRIPT_VERSION}"

# Pre-configure Aspire CLI path
ENV PATH="/root/.aspire/bin:${PATH}"
Expand Down
78 changes: 61 additions & 17 deletions .github/workflows/polyglot-validation/test-typescript-playground.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# Polyglot SDK Validation - TypeScript validation AppHosts
# Iterates all TypeScript validation AppHosts under tests/PolyglotAppHosts/*/TypeScript,
# runs 'aspire restore --apphost' to regenerate the per-integration .aspire/modules/ SDK, and
# type-checks each AppHost with tsgo against the generated API surface.
# type-checks each AppHost twice: once with the TypeScript the AppHost's own package.json declares,
# and once with the TypeScript 7 native compiler.
set -euo pipefail

echo "=== TypeScript Validation AppHost Codegen Validation ==="
Expand All @@ -22,19 +23,26 @@ if ! command -v npm &> /dev/null; then
exit 1
fi

if ! command -v npx &> /dev/null; then
echo "❌ npx not found in PATH (Node.js required to run @typescript/native-preview when tsgo is not installed)"
exit 1
fi

if command -v tsgo &> /dev/null; then
TSGO_COMMAND=(tsgo)
elif command -v npx &> /dev/null; then
TSGO_COMMAND=(npx --yes @typescript/native-preview)
else
echo "❌ tsgo not found in PATH and npx is unavailable to run @typescript/native-preview"
# TypeScript 7 is the native (Go) port of the compiler. Since 7.0 GA it ships as the plain
# `typescript` package with a `tsc` binary; the pre-GA `@typescript/native-preview` package and its
# `tsgo` binary are being retired, and nightly builds have moved to `typescript@next`. See
# https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/.
#
# Dockerfile.typescript installs this compiler globally, so it is always on PATH in CI. There is
# deliberately no `npx` fallback: a missing compiler is a broken validation image and should not be
# hidden by downloading another toolchain during the test.
#
# A `tsc` on PATH can be any TypeScript version (6.x and older are JavaScript builds), so it is only
# accepted when it reports 7.x. `tsc --version` prints e.g. "Version 7.0.2".
TYPESCRIPT_VERSION="${TYPESCRIPT_VERSION:-7}"

if ! command -v tsc &> /dev/null || [[ "$(tsc --version 2>/dev/null)" != "Version 7."* ]]; then
echo "❌ A TypeScript 7 'tsc' is required on PATH to type-check the generated API surface."
echo " Found: $(command -v tsc &> /dev/null && tsc --version || echo 'no tsc on PATH')"
echo " Install it with: npm install --global \"typescript@${TYPESCRIPT_VERSION}\""
exit 1
fi
TYPESCRIPT_COMMAND=(tsc)

detect_parallelism() {
if [ -n "${MAX_PARALLEL_TYPESCRIPT_VALIDATIONS:-}" ]; then
Expand All @@ -53,7 +61,7 @@ detect_parallelism() {
echo "Aspire CLI version:"
aspire --version
echo "TypeScript checker:"
"${TSGO_COMMAND[@]}" --version
"${TYPESCRIPT_COMMAND[@]}" --version

export COREPACK_ENABLE_DOWNLOAD_PROMPT=0

Expand Down Expand Up @@ -158,7 +166,11 @@ install_command_text() {
}

typecheck_command_text() {
echo "tsgo --noEmit --project tsconfig.json"
echo "tsc --noEmit --project tsconfig.json"
}

declared_typecheck_command_text() {
echo "node_modules/.bin/tsc --noEmit --project tsconfig.json"
}

run_install() {
Expand All @@ -171,7 +183,31 @@ run_install() {
}

run_typecheck() {
"${TSGO_COMMAND[@]}" --noEmit --project tsconfig.json
"${TYPESCRIPT_COMMAND[@]}" --noEmit --project tsconfig.json
}

# Compiles the generated API surface with the TypeScript the AppHost's own package.json declares,
# rather than only with the native TypeScript 7 compiler the workflow supplies.
#
# Without this leg the installed toolchain is never used: CI would type-check with a compiler no
# scaffolded AppHost actually has, so generated code that the declared compiler rejects would reach
# users with the polyglot job green. The two legs answer different questions — this one is "does the
# compiler users run accept the surface we generate", the TypeScript 7 leg is "will it still be
# accepted once the native compiler is the default".
#
# Every package manager installs a `tsc` shim into node_modules/.bin, including Bun and pnpm with
# --ignore-workspace, so the binary is resolved by path instead of through `npm exec`/`bunx`, which
# would fall back to fetching the package when it is missing locally.
run_declared_typecheck() {
local declared_tsc="node_modules/.bin/tsc"

if [ ! -x "$declared_tsc" ]; then
echo " ❌ $declared_tsc is missing after install; the AppHost must declare typescript in its devDependencies"
return 1
fi

echo " → declared TypeScript: $("$declared_tsc" --version)"
"$declared_tsc" --noEmit --project tsconfig.json
}

ensure_package_manager_available() {
Expand Down Expand Up @@ -269,11 +305,19 @@ validate_apphost() {
return 1
fi

declared_typecheck_command=$(declared_typecheck_command_text)
echo " → $declared_typecheck_command..."
if ! run_declared_typecheck 2>&1; then
echo " ❌ Declared-toolchain TypeScript compilation failed for $integration_name"
printf 'FAIL|%s|declared tsc\n' "$integration_name" > "$result_file"
return 1
fi

typecheck_command=$(typecheck_command_text "$SELECTED_PACKAGE_MANAGER")
echo " → $typecheck_command..."
if ! run_typecheck "$SELECTED_PACKAGE_MANAGER" 2>&1; then
echo " ❌ tsgo compilation failed for $integration_name"
printf 'FAIL|%s|tsgo\n' "$integration_name" > "$result_file"
echo " ❌ TypeScript compilation failed for $integration_name"
printf 'FAIL|%s|tsc\n' "$integration_name" > "$result_file"
return 1
fi

Expand Down
4 changes: 2 additions & 2 deletions eng/github-ci/test-trigger-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,9 @@ affected_project_rules:
- projects: [Aspire.Hosting, Aspire.Hosting.AppHost, Aspire.Hosting.PostgreSQL, Aspire.Hosting.Redis]
targets: [test:Aspire.EndToEnd.Tests]
reason: OUTERLOOP-ONLY today; builds testproject apphost against built nugets
- projects: [Aspire.Cli, Aspire.TypeSystem, Aspire.Managed, Aspire.AppHost.Sdk]
- projects: [Aspire.Cli, Aspire.TypeSystem, Aspire.Managed, Aspire.AppHost.Sdk, Aspire.Hosting.CodeGeneration.*]
targets: [test:Aspire.Cli.EndToEnd.Tests]
reason: consumes the CLI native archive (RequiresCliArchive=true) built from these sources
reason: consumes the CLI native archive (RequiresCliArchive=true) built from these sources; the codegen generators also emit the scaffold and .aspire/modules SDK that the 16 TypeScript* tests here npm-install and build, so a manifest/API change lands in this suite before it lands on a user

# Derived targets: selected test project -> extra jobs/tests. Applied to the UNION of Layer 1 and
# Layer 2 selected tests, to a fixpoint (a derived test is itself expanded; cycle-safe). This is how
Expand Down
21 changes: 21 additions & 0 deletions extension/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ corepack yarn test

This compiles tests and sources, lints, then runs the suite (`corepack yarn lint` lints only). Add or update tests for behavior changes, and ensure tests and lint pass before opening a PR.

### TypeScript 6 and 7 side by side

The extension installs two TypeScript compilers, because TypeScript 7 is a native (Go) compiler that
ships no JavaScript compiler API and TypeScript 7.1 is the first release expected to expose a
replacement:

| `package.json` entry | Resolves to | Used by |
|----------------------|-------------|---------|
| `typescript` → `npm:@typescript/typescript6` | The TypeScript 6.0 API (and the `tsc6` binary) | `import * as ts from 'typescript'` in `editor/parsers/jsTsAppHostParser.ts` and `test/telemetryInventory.test.ts`, plus ts-loader, gulp-typescript, and typescript-eslint (whose `typescript` peer range is capped below 6.1.0) |
| `@typescript/native` → `npm:typescript@7` | The TypeScript 7 native compiler | `corepack yarn typecheck` |

Aliasing `typescript` leaves no `tsc` binary in that package, so the emitting scripts
(`compile-tests`, `watch-tests`, `compile-e2e`) call `tsc6`. `corepack yarn typecheck` runs the
TypeScript 7 compiler with `--noEmit` and is part of `pretest`, so `corepack yarn test` type-checks
the extension with both compilers. See
[Running Side-by-Side with TypeScript 6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/)
for the upstream guidance this follows.

Once typescript-eslint and the other tooling above support the TypeScript 7 API, the `typescript`
alias can be dropped and `@typescript/native` folded back into a plain `typescript` dependency.

To run a single unit-test file or a filtered subset, compile the tests first and pass Mocha selectors through `unit-test`:

```bash
Expand Down
12 changes: 7 additions & 5 deletions extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1099,10 +1099,11 @@
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"compile-tests": "tsc -p . --outDir out",
"compile-e2e": "node -e \"require('fs').rmSync('out/test-e2e',{recursive:true,force:true})\" && tsc -p tsconfig.e2e.json",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "yarn run compile-tests && yarn run compile && yarn run lint",
"compile-tests": "tsc6 -p . --outDir out",
"compile-e2e": "node -e \"require('fs').rmSync('out/test-e2e',{recursive:true,force:true})\" && tsc6 -p tsconfig.e2e.json",
"watch-tests": "tsc6 -p . -w --outDir out",
"typecheck": "node ./scripts/typecheck-native.js",
"pretest": "yarn run compile-tests && yarn run compile && yarn run lint && yarn run typecheck",
"lint": "eslint src",
"unit-test": "vscode-test",
"test": "yarn run unit-test",
Expand All @@ -1128,6 +1129,7 @@
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
"@typescript/native": "npm:typescript@7.0.2",
"@vscode/l10n-dev": "0.0.35",
"@vscode/test-cli": "0.0.12",
"@vscode/test-electron": "3.1.0",
Expand All @@ -1145,7 +1147,7 @@
"sinon": "21.0.3",
"ts-loader": "9.5.7",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"typescript": "npm:@typescript/typescript6@6.0.2",
"typescript-eslint": "8.58.0",
"vscode-extension-tester": "8.23.0",
"wait-for-expect": "4.0.0",
Expand Down
93 changes: 93 additions & 0 deletions extension/scripts/typecheck-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';

// Type-checks the extension with the TypeScript 7 native (Go) compiler.
//
// TypeScript 7 ships no JavaScript compiler API, so `typescript` stays aliased to the
// `@typescript/typescript6` compatibility package for everything that imports the module, and the
// native compiler is installed under the `@typescript/native` alias. See "Running Side-by-Side with
// TypeScript 6.0" in https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/.
//
// The compiler is resolved through Node's module resolution and the package's own `bin` field
// rather than a path such as `node_modules/@typescript/native/bin/tsc`. Two things make the literal
// path wrong to depend on: which directory a package manager installs an aliased package into is
// not contractual (npm, Yarn, pnpm and Bun all differ, and pnpm's default layout is not flat), and
// the `bin` entry is the package's own declaration of where its executable lives. Resolving
// `node_modules/.bin/tsc` instead is also wrong, because `@typescript/old` — pulled in transitively
// by the TypeScript 6 compatibility package — declares a `tsc` bin too, and which one wins the
// shim is decided by install order.

const { spawnSync } = require('child_process');
const fs = require('fs');
const path = require('path');

const nativePackageName = '@typescript/native';
const extensionRoot = path.resolve(__dirname, '..');

// Every project the extension compiles needs to pass the native compiler, not only the one webpack
// and the unit tests build. tsconfig.e2e.json covers src/test-e2e, which is compiled separately by
// `compile-e2e` and is otherwise excluded from tsconfig.json.
const projects = ['tsconfig.json', 'tsconfig.e2e.json'];

function resolveNativeCompiler() {
let packageJsonPath;
try {
packageJsonPath = require.resolve(`${nativePackageName}/package.json`, { paths: [extensionRoot] });
}
catch {
throw new Error(`Could not resolve "${nativePackageName}". Run "corepack yarn install" in ${extensionRoot} first.`);
}

const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));

// The `bin` field is either a string (single executable named after the package) or a map of
// executable name to path. typescript@7 publishes { "tsc": "./bin/tsc" }.
const bin = packageJson.bin;
const relativeBinPath = typeof bin === 'string' ? bin : bin?.tsc;
if (!relativeBinPath) {
throw new Error(`"${nativePackageName}" (${packageJson.name}@${packageJson.version}) declares no "tsc" bin: ${JSON.stringify(bin)}`);
}

const binPath = path.resolve(path.dirname(packageJsonPath), relativeBinPath);
if (!fs.existsSync(binPath)) {
throw new Error(`"${nativePackageName}" declares its tsc bin at ${relativeBinPath}, but ${binPath} does not exist.`);
}

return { binPath, version: packageJson.version };
}

function main() {
const { binPath, version } = resolveNativeCompiler();
console.log(`Type-checking with the TypeScript ${version} native compiler (${path.relative(extensionRoot, binPath)}).`);

for (const project of projects) {
const projectPath = path.join(extensionRoot, project);
if (!fs.existsSync(projectPath)) {
throw new Error(`Expected to type-check ${project}, but it does not exist in ${extensionRoot}.`);
}

console.log(` → ${project}`);

// The bin is a Node script without a portable shebang on Windows, so it is run through the
// current Node executable rather than executed directly.
const result = spawnSync(process.execPath, [binPath, '--noEmit', '-p', projectPath], {
cwd: extensionRoot,
stdio: 'inherit',
});

if (result.error) {
throw result.error;
}

if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
}

try {
main();
}
catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
}
Loading
Loading