Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
146 changes: 111 additions & 35 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2673,38 +2673,42 @@ async function runPackNativePreviewPackages() {
export const packVsixExtensions = task({
name: "vscode-typescript:pack",
hiddenFromTaskList: true,
dependencies: options.forRelease || usePublishedPlatformPackagesForVsix ? undefined : [buildNativePreviewPackages, cleanSignTempDirectory],
dependencies: options.forRelease || usePublishedPlatformPackagesForVsix ? undefined : [packNativePreviewPackages],
Comment thread
jakebailey marked this conversation as resolved.
run: runPackVsixExtensions,
});
Comment thread
andrewbranch marked this conversation as resolved.

/** @type {Map<string, Promise<string>>} */
const publishedPlatformPackageLibDirs = new Map();
const publishedPlatformPackageDirs = new Map();

const getPublishedTypeScriptPackageJson = memoize(() => {
const getPublishedTypeScriptPackageDir = memoize(() => {
const candidates = [
path.join(extensionDir, "node_modules", publishedTypeScriptAliasPackageName, "package.json"),
path.join(__dirname, "node_modules", publishedTypeScriptAliasPackageName, "package.json"),
path.join(extensionDir, "node_modules", publishedTypeScriptAliasPackageName),
path.join(__dirname, "node_modules", publishedTypeScriptAliasPackageName),
];

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
const packageJson = JSON.parse(fs.readFileSync(candidate, "utf8"));
if (packageJson.name !== "typescript") {
throw new Error(`${publishedTypeScriptAliasPackageName} should alias the typescript package, but found ${packageJson.name}.`);
}
if (!packageJson.version || typeof packageJson.version !== "string") {
throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain a version.`);
}
if (!packageJson.optionalDependencies || typeof packageJson.optionalDependencies !== "object") {
throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain platform optionalDependencies.`);
}
return packageJson;
if (fs.existsSync(path.join(candidate, "package.json"))) {
return candidate;
}
}

throw new Error(`Could not find ${publishedTypeScriptAliasPackageName}; run npm install first.`);
});

const getPublishedTypeScriptPackageJson = memoize(() => {
const packageJson = JSON.parse(fs.readFileSync(path.join(getPublishedTypeScriptPackageDir(), "package.json"), "utf8"));
if (packageJson.name !== "typescript") {
throw new Error(`${publishedTypeScriptAliasPackageName} should alias the typescript package, but found ${packageJson.name}.`);
}
if (!packageJson.version || typeof packageJson.version !== "string") {
throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain a version.`);
}
if (!packageJson.optionalDependencies || typeof packageJson.optionalDependencies !== "object") {
throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain platform optionalDependencies.`);
}
return packageJson;
});

function getPublishedTypeScriptVersion() {
const version = getPublishedTypeScriptPackageJson().version;
if (releaseVscodeTypescript && !stableThreeComponentVersionPattern.test(version)) {
Expand All @@ -2725,23 +2729,23 @@ const getPackageLock = memoize(() => JSON.parse(fs.readFileSync(path.join(__dirn
/**
* @param {string} npmPackageName
*/
async function getPublishedPlatformPackageLibDir(npmPackageName) {
let promise = publishedPlatformPackageLibDirs.get(npmPackageName);
async function getPublishedPlatformPackageDir(npmPackageName) {
let promise = publishedPlatformPackageDirs.get(npmPackageName);
if (!promise) {
promise = getPublishedPlatformPackageLibDirWorker(npmPackageName);
publishedPlatformPackageLibDirs.set(npmPackageName, promise);
promise = getPublishedPlatformPackageDirWorker(npmPackageName);
publishedPlatformPackageDirs.set(npmPackageName, promise);
}
return promise;
}

/**
* @param {string} npmPackageName
*/
async function getPublishedPlatformPackageLibDirWorker(npmPackageName) {
async function getPublishedPlatformPackageDirWorker(npmPackageName) {
const dest = path.join(builtPublishedPlatformPackages, "node_modules", ...npmPackageName.split("/"));
const lib = path.join(dest, "lib");
if (fs.existsSync(lib)) {
return lib;
return dest;
}

await fs.promises.mkdir(dest, { recursive: true });
Expand Down Expand Up @@ -2799,26 +2803,27 @@ async function getPublishedPlatformPackageLibDirWorker(npmPackageName) {
throw new Error(`Published platform package ${npmPackageName}@${version} did not contain a lib directory.`);
}

return lib;
return dest;
}

async function runPackVsixExtensions() {
await rimraf(builtVsix);
await fs.promises.mkdir(builtVsix, { recursive: true });
if (usePublishedPlatformPackagesForVsix) {
checkPublishedPlatformPackagesForVsix();
publishedPlatformPackageLibDirs.clear();
publishedPlatformPackageDirs.clear();
await rimraf(builtPublishedPlatformPackages);
}

const platforms = getPlatforms();
const extensions = platforms.flatMap(({ npmDir, npmPackageName, extensions }) => extensions.map(e => ({ npmDir, npmPackageName, ...e })));
const extensions = platforms.flatMap(({ npmTarball, npmPackageName, extensions }) => extensions.map(e => ({ npmTarball, npmPackageName, ...e })));
if (!extensions.length) {
console.log("No VSIX targets configured; skipping extension packaging.");
return;
}

// We don't use vscode:prepublish, as that would run the build for each package below.
await run("npm", ["run", "-w", "@typescript/typescript", "build"]);
await run("npm", ["run", "bundle:release"], { cwd: extensionDir, env: releasePackageEnv });

let version = "0.0.0";
Expand All @@ -2844,29 +2849,43 @@ async function runPackVsixExtensions() {

console.log("Version:", version);

await Promise.all(extensions.map(async ({ npmDir, npmPackageName, nodeOs, vscodeTarget, sourceDir, extensionDir: thisExtensionDir, vsixPath, vsixManifestPath, vsixSignaturePath }) => {
const npmLibDir = usePublishedPlatformPackagesForVsix
? await getPublishedPlatformPackageLibDir(npmPackageName)
: path.join(npmDir, "lib");
const extensionLibDir = path.join(thisExtensionDir, "lib");
await fs.promises.mkdir(extensionLibDir, { recursive: true });
await Promise.all(extensions.map(async ({ npmTarball, npmPackageName, nodeOs, vscodeTarget, sourceDir, extensionDir: thisExtensionDir, vsixPath, vsixManifestPath, vsixSignaturePath }) => {
const nodeModules = path.join(thisExtensionDir, "node_modules");
const embeddedPlatformPackageDir = path.join(nodeModules, ...npmPackageName.split("/"));
const embeddedTypeScriptDir = path.join(nodeModules, "typescript");

await cpWithoutNodeModulesOrTsconfig(sourceDir, thisExtensionDir);
await cpWithoutNodeModulesOrTsconfig(npmLibDir, extensionLibDir);
await fs.promises.chmod(path.join(extensionLibDir, nativePreviewExeName(nodeOs)), 0o755);
if (usePublishedPlatformPackagesForVsix) {
await cpRecursive(await getPublishedPlatformPackageDir(npmPackageName), embeddedPlatformPackageDir);
await cpRecursive(getPublishedTypeScriptPackageDir(), embeddedTypeScriptDir, p => !p.endsWith("/node_modules"));
}
else {
await fs.promises.mkdir(embeddedPlatformPackageDir, { recursive: true });
await fs.promises.mkdir(embeddedTypeScriptDir, { recursive: true });
await tar.x({ file: npmTarball, cwd: embeddedPlatformPackageDir, strip: 1 });
await tar.x({ file: mainNativePreviewPackage.npmTarball, cwd: embeddedTypeScriptDir, strip: 1 });
Comment thread
andrewbranch marked this conversation as resolved.
}
await fs.promises.chmod(path.join(embeddedPlatformPackageDir, "lib", nativePreviewExeName(nodeOs)), 0o755);
const embeddedTypeScriptPackageJson = JSON.parse(await fs.promises.readFile(path.join(embeddedTypeScriptDir, "package.json"), "utf8"));

const packageJsonPath = path.join(thisExtensionDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
packageJson.version = version;
packageJson.bundledTypeScriptVersion = usePublishedPlatformPackagesForVsix ? getPublishedTypeScriptVersion() : getVersion();
packageJson.dependencies = {
typescript: embeddedTypeScriptPackageJson.name === "typescript"
? embeddedTypeScriptPackageJson.version
: `npm:${embeddedTypeScriptPackageJson.name}@${embeddedTypeScriptPackageJson.version}`,
};
Comment thread
andrewbranch marked this conversation as resolved.
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 4));

await fs.promises.copyFile("NOTICE.txt", path.join(thisExtensionDir, "NOTICE.txt"));

await run("vsce", ["package", version, "--no-update-package-json", "--no-dependencies", "--out", vsixPath, "--target", vscodeTarget], {
await run("vsce", ["package", version, "--no-update-package-json", "--no-yarn", "--out", vsixPath, "--target", vscodeTarget], {
cwd: thisExtensionDir,
env: releasePackageEnv,
});
await testVsixPackage(vsixPath, thisExtensionDir, npmPackageName, nodeOs, vscodeTarget);

if (options.forRelease) {
await run("vsce", ["generate-manifest", "--packagePath", vsixPath, "--out", vsixManifestPath], {
Expand All @@ -2878,6 +2897,63 @@ async function runPackVsixExtensions() {
}));
}

/**
* @param {string} vsixPath
* @param {string} extensionPath
* @param {string} platformPackageName
* @param {string} nodeOs
* @param {string} vscodeTarget
*/
async function testVsixPackage(vsixPath, extensionPath, platformPackageName, nodeOs, vscodeTarget) {
const zip = new AdmZip(vsixPath);
for (const packageName of ["typescript", platformPackageName]) {
const packagePath = path.join(extensionPath, "node_modules", ...packageName.split("/"));
const files = await fs.promises.readdir(packagePath, { recursive: true, withFileTypes: true });
for (const file of files) {
if (!file.isFile()) continue;
const filePath = path.join(file.parentPath, file.name);
const archivePath = `extension/node_modules/${packageName}/${path.relative(packagePath, filePath).replace(/\\/g, "/")}`;
const entry = zip.getEntry(archivePath);
assert(entry, `VSIX is missing ${archivePath}`);
assert.deepEqual(entry.getData(), await fs.promises.readFile(filePath), `VSIX changed ${archivePath}`);
}
}

const hostTarget = `${process.platform}-${process.arch === "arm" ? "armhf" : process.arch}`;
if (vscodeTarget !== hostTarget) return;

const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "typescript-vsix-test-"));
try {
zip.extractAllTo(directory);
const platformPath = path.join(directory, "extension", "node_modules", ...platformPackageName.split("/"));
await fs.promises.chmod(path.join(platformPath, "lib", nativePreviewExeName(nodeOs)), 0o755);
await fs.promises.writeFile(path.join(directory, "tsconfig.json"), '{"files": []}\n');
await run(process.execPath, [
"--input-type=module",
"--eval",
`
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
const require = createRequire(process.cwd() + "/extension/node_modules/typescript/package.json");
const manifest = require("./package.json");
for (const mode of ["async", "sync"]) {
const { API } = await import(pathToFileURL(require.resolve(manifest.name + "/unstable/" + mode)).href);
const api = new API();
try {
assert.ok(await api.parseConfigFile(process.cwd() + "/tsconfig.json"));
} finally {
await api.close();
}
}
`,
], { cwd: directory });
}
finally {
await fs.promises.rm(directory, { recursive: true, force: true });
}
}

export const signVsixExtensions = task({
name: "vscode-typescript:sign",
hiddenFromTaskList: true,
Expand Down
3 changes: 2 additions & 1 deletion packages/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"./schemas/jsconfig.schema.json": "./schemas/jsconfig.schema.json",
".": "./lib/version.cjs",
"./unstable/vscode": {
"types": "./dist/vscode/extensionApi.d.ts"
"@typescript/source": "./src/vscode/extensionApi.ts",
"default": "./dist/vscode/extensionApi.js"
},
"./unstable/sync": {
"@typescript/source": "./src/api/sync/api.ts",
Expand Down
50 changes: 48 additions & 2 deletions packages/typescript/src/vscode/extensionApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { TypeScriptModuleLoader } from "./moduleLoader.ts";
import type { LspMiddlewareRequests } from "./protocol.generated.ts";
export { createTypeScriptModuleLoader, type TypeScriptModuleLoader } from "./moduleLoader.ts";
export type * from "./protocol.generated.ts";

export interface Disposable {
Expand Down Expand Up @@ -45,9 +47,53 @@ export type LspMiddlewareTransformer<M extends LspMiddlewareMethod> = (
context: LspMiddlewareContext<M>,
) => LspMiddlewareResult<M> | PromiseLike<LspMiddlewareResult<M>>;

export interface ExtensionAPI {
onLanguageServerInitialized: Event<void>;
export interface APIModules {
"typescript/unstable/async": typeof import("../api/async/api.ts");
"typescript/unstable/sync": typeof import("../api/sync/api.ts");
"typescript/unstable/fs": typeof import("../api/fs.ts");
"typescript/unstable/path": typeof import("../api/typedPaths.ts");
"typescript/unstable/proto": typeof import("../api/proto.ts");
"typescript/unstable/ast": typeof import("../ast/index.ts");
"typescript/unstable/ast/is": typeof import("../ast/is.ts");
"typescript/unstable/ast/factory": typeof import("../ast/factory.generated.ts");
"typescript/unstable/ast/utils": typeof import("../ast/utils.ts");
"typescript/unstable/ast/scanner": typeof import("../ast/scanner.ts");
"typescript/unstable/ast/visitor": typeof import("../ast/visitor.ts");
"typescript/unstable/ast/clone": typeof import("../ast/clone.ts");
[exportPath: string]: unknown;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to have a fallback here so you could use a newer import path than your types expose, but I'm not sure if there's much reason not to just install the latest types as soon as you want to use the latest features. Would it be better to remove this?

}

/**
* The selected TypeScript installation and its module loader.
* Restart TS Server offers to restart extensions if an acquired installation's
* version changed on disk, without first restarting the server.
*/
export interface TypeScriptSDK extends TypeScriptModuleLoader {
readonly version: string;
/** The matching API package manifest; undefined for a bare executable without a companion package. */
readonly packageJsonPath: string | undefined;
/**
* Whether an initialized language server currently uses this installation.
* False while stopped or restarting; true again after a same-installation restart.
* This is a synchronous snapshot, not a guarantee for subsequent async operations
* or a check for changes on disk.
*/
isCurrent(): boolean;
/**
* Opens an API pipe, waiting for scheduled server restarts to complete.
* Handles remain usable after restarts of the same installation. If a different
* installation is selected, use the SDK from the latest initialization event.
*/
initializeAPIConnection(pipe?: string): Promise<string>;
}

export interface ExtensionAPI {
/**
* Fires after each language server initialization. Existing API connections
* should be discarded and recreated when this fires. Newly registered listeners
* are invoked immediately if the server is already initialized.
*/
onLanguageServerInitialized: Event<TypeScriptSDK>;
registerContentMappers(contributorId: string, contributions: readonly ContentMapperContribution[]): Disposable;

/**
Expand Down
41 changes: 41 additions & 0 deletions packages/typescript/src/vscode/moduleLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import fs from "node:fs/promises";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import type { APIModules } from "./extensionApi.ts";

export interface TypeScriptModuleLoader {
/**
* Imports a module from the selected API package using the full package specifier.
*/
importModule<K extends string>(exportPath: K): Promise<APIModules[K]>;
}

/**
* Loads the selected installation's API modules in this process.
* Pass the SDK's packageJsonPath through your server's initialization options.
* Restart this process after an in-place package upgrade to clear its module cache.
*/
export function createTypeScriptModuleLoader(packageJsonPath: string): TypeScriptModuleLoader {
function importModule<K extends string>(exportPath: K): Promise<APIModules[K]>;
async function importModule(exportPath: string): Promise<unknown> {
if (
!exportPath.startsWith("typescript/")
|| exportPath.length === "typescript/".length
|| exportPath.includes("\\")
|| exportPath.split("/").includes("..")
) {
throw new Error(`Invalid TypeScript API module export path '${exportPath}'.`);
}

const manifestPath = await fs.realpath(packageJsonPath);
const manifest: unknown = JSON.parse(await fs.readFile(manifestPath, "utf8"));
if (!manifest || typeof manifest !== "object" || !("name" in manifest) || typeof manifest.name !== "string") {
throw new Error(`TypeScript API package manifest at '${packageJsonPath}' does not contain a package name.`);
}
const require = createRequire(manifestPath);
const modulePath = require.resolve(`${manifest.name}/${exportPath.slice("typescript/".length)}`);
return import(pathToFileURL(modulePath).href);
Comment thread
jakebailey marked this conversation as resolved.
}

return { importModule };
}
3 changes: 2 additions & 1 deletion packages/vscode-typescript-nightly/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
}
},
"files": [
"lib",
"node_modules/typescript",
"node_modules/@typescript",
"LICENSE",
"NOTICE.txt",
"README.md",
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-typescript/l10n/bundle.l10n.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"No source definitions found.": "No source definitions found.",
"No tsconfig": "No tsconfig",
"OK": "OK",
"One or more extensions are using a TypeScript installation that has been modified on disk. Restart the extension host to ensure they continue functioning.": "One or more extensions are using a TypeScript installation that has been modified on disk. Restart the extension host to ensure they continue functioning.",
"Open Config File": "Open Config File",
"PID": "PID",
"Report an issue with TypeScript 7": "Report an issue with TypeScript 7",
Expand All @@ -60,6 +61,7 @@
"Switch back to the built-in TypeScript extension": "Switch back to the built-in TypeScript extension",
"The TypeScript language server crashed {0} times in the last 3 minutes. The server will not be restarted. See the output for more information.": "The TypeScript language server crashed {0} times in the last 3 minutes. The server will not be restarted. See the output for more information.",
"The built-in TypeScript extension is disabled. Sync launch.json with launch.template.json to reenable.": "The built-in TypeScript extension is disabled. Sync launch.json with launch.template.json to reenable.",
"The selected TypeScript installation has changed. Use the SDK from the latest language server initialization.": "The selected TypeScript installation has changed. Use the SDK from the latest language server initialization.",
"This workspace has a TypeScript 7 tsdk configured ({0}). Would you like to use it?": "This workspace has a TypeScript 7 tsdk configured ({0}). Would you like to use it?",
"TypeScript 7": "TypeScript 7",
"TypeScript 7 Project Status": "TypeScript 7 Project Status",
Expand Down
Loading
Loading