Skip to content

Commit 7b94270

Browse files
ntottenclaude
andcommitted
Convert extension to ESM and improve Prettier module loading
This PR converts the extension from CommonJS to ESM modules and improves how Prettier is loaded and resolved. Key changes: **ESM Conversion** - Convert all source files to use ESM imports/exports - Update esbuild configuration for ESM output - Add .js extensions to all relative imports - Update tsconfig for ESM module resolution **Prettier Module Loading** - Lazy-load bundled Prettier using dynamic import() for faster activation - Remove worker thread implementation (PrettierWorkerInstance) - now use PrettierDynamicInstance which loads Prettier dynamically - Improve plugin loading to resolve and import plugins as ES modules - Add utility functions for finding modules (find-up, resolve-module-entry) **Extension Activation** - Make activate() async and await formatter registration - Ensures formatters are ready when extension.isActive becomes true - Fixes race condition where tests could run before formatters registered **Test Infrastructure** - Add ensureExtensionActivated() helper for reliable test setup - Extract common format test utilities to formatTestUtils.ts - Update test imports for ESM compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent e7bc410 commit 7b94270

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+1700
-1369
lines changed

.claude/settings.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
{
22
"permissions": {
33
"allow": [
4-
"WebFetch(domain:prettier.io)",
5-
"WebFetch(domain:code.visualstudio.com)",
6-
"Bash(npm run prettier:*)",
4+
"Bash(npm run check-types:*)",
5+
"Bash(npm run compile:*)",
76
"Bash(npm run lint:*)",
8-
"mcp__ide__getDiagnostics"
7+
"Bash(npm run prettier:*)",
8+
"Bash(npm test:*)",
9+
"mcp__ide__getDiagnostics",
10+
"WebFetch(domain:code.visualstudio.com)",
11+
"WebFetch(domain:prettier.io)"
912
],
1013
"deny": [],
1114
"ask": []

.github/instructions/tests.instructions.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ Tests run inside a VS Code Extension Development Host using Mocha.
2828
## Async Patterns
2929

3030
- Tests are async - use `async/await`
31-
- Use `wait()` helper when needing delays
32-
- Prettier v3 formatting is async, may need retries for timing
3331

3432
## Test File Naming
3533

.vscode-test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defineConfig } from "@vscode/test-cli";
22

33
export default defineConfig({
4-
files: "out/test/suite/**/*.test.js",
4+
files: "out/test/suite/**/*.test.cjs",
55
mocha: {
66
ui: "bdd",
77
timeout: 10000,

.vscode/launch.json

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"--extensionDevelopmentPath=${workspaceFolder}"
1616
],
1717
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
18-
"preLaunchTask": "npm: webpack"
18+
"preLaunchTask": "npm: compile"
1919
},
2020
{
2121
"name": "Run Extension (With Other Extensions)",
@@ -24,33 +24,27 @@
2424
"runtimeExecutable": "${execPath}",
2525
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
2626
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
27-
"preLaunchTask": "npm: webpack"
27+
"preLaunchTask": "npm: compile"
2828
},
2929
{
3030
"name": "Extension Tests",
3131
"type": "extensionHost",
3232
"request": "launch",
33-
"runtimeExecutable": "${execPath}",
34-
"args": [
35-
"${workspaceFolder}/test-fixtures/test.code-workspace",
36-
"--disable-extensions",
37-
"--extensionDevelopmentPath=${workspaceFolder}",
38-
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
39-
],
40-
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
33+
"testConfiguration": "${workspaceFolder}/.vscode-test.mjs",
34+
"outFiles": ["${workspaceFolder}/out/test/**/*.cjs"],
4135
"preLaunchTask": "npm: compile:test"
4236
},
4337
{
4438
"name": "Run Web Extension",
45-
"type": "pwa-extensionHost",
39+
"type": "extensionHost",
4640
"debugWebWorkerHost": true,
4741
"request": "launch",
4842
"args": [
4943
"--extensionDevelopmentPath=${workspaceFolder}",
5044
"--extensionDevelopmentKind=web"
5145
],
5246
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
53-
"preLaunchTask": "npm: webpack"
47+
"preLaunchTask": "npm: compile"
5448
}
5549
]
5650
}

.vscode/tasks.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,22 @@
4949
}
5050
},
5151
{
52-
"label": "compile",
52+
"label": "npm: compile",
5353
"type": "npm",
5454
"script": "compile",
5555
"problemMatcher": "$tsc",
5656
"presentation": {
5757
"reveal": "silent"
5858
}
59+
},
60+
{
61+
"label": "npm: compile:test",
62+
"type": "npm",
63+
"script": "compile:test",
64+
"problemMatcher": "$tsc",
65+
"presentation": {
66+
"reveal": "silent"
67+
}
5968
}
6069
]
6170
}

CONTRIBUTING.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ Key components:
9090

9191
- `PrettierEditService.ts` - Handles document formatting
9292
- `ModuleResolver.ts` - Resolves Prettier installations (local, global, or bundled)
93-
- `PrettierInstance.ts` - Interface for Prettier, with `PrettierMainThreadInstance` and `PrettierWorkerInstance` implementations
9493

9594
## Submitting Changes
9695

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
</p>
5656

5757
> [!IMPORTANT]
58-
> **Extension Migration:** This extension is being moved from `esbenp.prettier-vscode` to [`prettier.prettier-vscode`](https://marketplace.visualstudio.com/items?itemName=prettier.prettier-vscode). Version 12+ is only published to the new for now as it is a major change. Once it is stable, we will publish v12 to both extensions and deprecate the `esbenp.prettier-vscode` extension. **Version 12.x is currently not stable, use with caution in production environments.**
58+
> **Extension Migration:** This extension is being moved from `esbenp.prettier-vscode` to [`prettier.prettier-vscode`](https://marketplace.visualstudio.com/items?itemName=prettier.prettier-vscode). Version 12+ is only published to the new for now as it is a major change. Once it is stable, we will publish v12 to both extensions and deprecate the `esbenp.prettier-vscode` extension. **Version 12.x is currently not stable, use with caution and report bugs.**
5959
6060
## Installation
6161

esbuild.mjs

Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const browserAliasPlugin = {
4646
name: "browser-alias",
4747
setup(build) {
4848
// Replace ModuleResolver imports with BrowserModuleResolver for browser build
49-
build.onResolve({ filter: /\.\/ModuleResolver$/ }, (args) => {
49+
// Match both ./ModuleResolver and ./ModuleResolver.js patterns
50+
build.onResolve({ filter: /\.\/ModuleResolver(\.js)?$/ }, (args) => {
5051
return {
5152
path: path.join(args.resolveDir, "BrowserModuleResolver.ts"),
5253
};
@@ -56,18 +57,34 @@ const browserAliasPlugin = {
5657

5758
/**
5859
* Node extension configuration
60+
* Uses ESM format for native ES module support, following the pattern from
61+
* https://github.com/microsoft/vscode-github-issue-notebooks
5962
* @type {import('esbuild').BuildOptions}
6063
*/
6164
const nodeConfig = {
6265
entryPoints: ["src/extension.ts"],
6366
bundle: true,
64-
format: "cjs",
67+
format: "esm",
6568
minify: production,
66-
sourcemap: !production,
67-
sourcesContent: false,
68-
platform: "node",
69+
sourcemap: true,
70+
platform: "neutral",
71+
target: ["node22"],
6972
outfile: "dist/extension.js",
70-
external: ["vscode", "prettier"],
73+
// Keep vscode external - provided by the VS Code extension host
74+
// Keep prettier external - loaded dynamically at runtime
75+
// Keep Node.js built-ins external - available in the extension host runtime
76+
external: [
77+
"vscode",
78+
"prettier",
79+
"fs",
80+
"fs/promises",
81+
"path",
82+
"os",
83+
"url",
84+
"util",
85+
"module",
86+
"child_process",
87+
],
7188
define: {
7289
"process.env.EXTENSION_NAME": JSON.stringify(
7390
`${extensionPackage.publisher}.${extensionPackage.name}`,
@@ -88,19 +105,52 @@ const browserShimsPlugin = {
88105
build.onResolve({ filter: /^os$/ }, () => {
89106
return { path: "os", namespace: "browser-shim" };
90107
});
108+
build.onResolve({ filter: /^fs$/ }, () => {
109+
return { path: "fs", namespace: "browser-shim" };
110+
});
111+
build.onResolve({ filter: /^url$/ }, () => {
112+
return { path: "url", namespace: "browser-shim" };
113+
});
91114
build.onLoad({ filter: /.*/, namespace: "browser-shim" }, (args) => {
92115
if (args.path === "os") {
93116
return {
94117
contents: `export function homedir() { return ""; }`,
95118
loader: "js",
96119
};
97120
}
121+
if (args.path === "fs") {
122+
// Provide a minimal fs shim - these functions won't be called in browser
123+
// but need to exist to satisfy imports
124+
return {
125+
contents: `
126+
export const promises = {
127+
access: async () => { throw new Error("Not available in browser"); },
128+
lstat: async () => { throw new Error("Not available in browser"); },
129+
readdir: async () => [],
130+
readFile: async () => { throw new Error("Not available in browser"); },
131+
};
132+
export default { promises };
133+
`,
134+
loader: "js",
135+
};
136+
}
137+
if (args.path === "url") {
138+
// Provide a minimal url shim for browser
139+
return {
140+
contents: `
141+
export function pathToFileURL(path) {
142+
return new URL("file://" + path);
143+
}
144+
`,
145+
loader: "js",
146+
};
147+
}
98148
});
99149
},
100150
};
101151

102152
/**
103-
* Browser/web extension configurationn
153+
* Browser/web extension configuration (CJS required for web extension host)
104154
* @type {import('esbuild').BuildOptions}
105155
*/
106156
const browserConfig = {
@@ -111,7 +161,8 @@ const browserConfig = {
111161
sourcemap: !production,
112162
sourcesContent: false,
113163
platform: "browser",
114-
outfile: "dist/web-extension.js",
164+
target: "es2020",
165+
outfile: "dist/web-extension.cjs",
115166
external: ["vscode"],
116167
define: {
117168
"process.env.EXTENSION_NAME": JSON.stringify(
@@ -135,8 +186,29 @@ const browserConfig = {
135186
],
136187
};
137188

189+
/**
190+
* Desktop test bundle configuration
191+
* Uses CJS format with .cjs extension so tests work with "type": "module" in package.json
192+
* @type {import('esbuild').BuildOptions}
193+
*/
194+
const desktopTestConfig = {
195+
entryPoints: ["src/test/suite/*.test.ts"],
196+
bundle: true, // Bundle each test file separately
197+
format: "cjs",
198+
minify: false,
199+
sourcemap: true,
200+
platform: "node",
201+
target: "node20",
202+
outdir: "out/test/suite",
203+
outExtension: { ".js": ".cjs" },
204+
external: ["vscode", "mocha", "prettier"],
205+
logLevel: "silent",
206+
plugins: [esbuildProblemMatcherPlugin],
207+
};
208+
138209
/**
139210
* Web test bundle configuration
211+
* Uses CJS format with .cjs extension since package.json has "type": "module"
140212
* @type {import('esbuild').BuildOptions}
141213
*/
142214
const webTestConfig = {
@@ -147,7 +219,7 @@ const webTestConfig = {
147219
sourcemap: true,
148220
sourcesContent: false,
149221
platform: "browser",
150-
outfile: "dist/web/test/suite/index.js",
222+
outfile: "dist/web/test/suite/index.cjs",
151223
external: ["vscode"],
152224
define: {
153225
"process.env.BROWSER_ENV": JSON.stringify("true"),
@@ -160,57 +232,30 @@ const webTestConfig = {
160232
plugins: [esbuildProblemMatcherPlugin],
161233
};
162234

163-
function copyWorkerFile() {
164-
// Copy to dist (for production/esbuild bundle)
165-
const distWorkerDir = "dist/worker";
166-
if (!fs.existsSync(distWorkerDir)) {
167-
fs.mkdirSync(distWorkerDir, { recursive: true });
168-
}
169-
fs.copyFileSync(
170-
"src/worker/prettier-instance-worker.js",
171-
"dist/worker/prettier-instance-worker.js",
172-
);
173-
174-
// Copy to out (for tests/tsc output)
175-
const outWorkerDir = "out/worker";
176-
if (!fs.existsSync(outWorkerDir)) {
177-
fs.mkdirSync(outWorkerDir, { recursive: true });
178-
}
179-
fs.copyFileSync(
180-
"src/worker/prettier-instance-worker.js",
181-
"out/worker/prettier-instance-worker.js",
182-
);
183-
}
184-
185235
async function main() {
186236
const nodeCtx = await esbuild.context(nodeConfig);
187237
const browserCtx = await esbuild.context(browserConfig);
238+
const desktopTestCtx = await esbuild.context(desktopTestConfig);
188239
const webTestCtx = await esbuild.context(webTestConfig);
189240

190-
// Copy worker file
191-
copyWorkerFile();
192-
193241
if (watch) {
194-
// Watch the worker file for changes
195-
fs.watchFile("src/worker/prettier-instance-worker.js", () => {
196-
console.log("[watch] copying worker file");
197-
copyWorkerFile();
198-
});
199-
200242
await Promise.all([
201243
nodeCtx.watch(),
202244
browserCtx.watch(),
245+
desktopTestCtx.watch(),
203246
webTestCtx.watch(),
204247
]);
205248
} else {
206249
await Promise.all([
207250
nodeCtx.rebuild(),
208251
browserCtx.rebuild(),
252+
desktopTestCtx.rebuild(),
209253
webTestCtx.rebuild(),
210254
]);
211255
await Promise.all([
212256
nodeCtx.dispose(),
213257
browserCtx.dispose(),
258+
desktopTestCtx.dispose(),
214259
webTestCtx.dispose(),
215260
]);
216261
}

eslint.config.mjs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ export default tseslint.config(
2020
// Base ESLint recommended rules
2121
eslint.configs.recommended,
2222

23-
// TypeScript files
23+
// TypeScript source files (excluding tests)
2424
{
2525
files: ["src/**/*.ts"],
26+
ignores: ["src/test/**/*.ts"],
2627
extends: [...tseslint.configs.recommendedTypeChecked],
2728
languageOptions: {
2829
parserOptions: {
@@ -57,11 +58,30 @@ export default tseslint.config(
5758
},
5859
},
5960

60-
// Test files - relax rules for test code
61+
// Test files - use separate tsconfig and relax rules
6162
{
6263
files: ["src/test/**/*.ts"],
64+
extends: [...tseslint.configs.recommendedTypeChecked],
65+
languageOptions: {
66+
parserOptions: {
67+
project: ["./tsconfig.test.json"],
68+
tsconfigRootDir: import.meta.dirname,
69+
},
70+
},
6371
rules: {
6472
"@typescript-eslint/no-floating-promises": "off",
73+
"@typescript-eslint/no-unsafe-assignment": "off",
74+
"@typescript-eslint/no-unsafe-member-access": "off",
75+
"@typescript-eslint/no-unsafe-return": "off",
76+
"@typescript-eslint/no-unsafe-argument": "off",
77+
"@typescript-eslint/no-unsafe-call": "off",
78+
"@typescript-eslint/require-await": "off",
79+
"@typescript-eslint/restrict-template-expressions": "off",
80+
"@typescript-eslint/no-explicit-any": "off",
81+
"@typescript-eslint/no-unused-vars": [
82+
"error",
83+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
84+
],
6585
"no-console": "off",
6686
},
6787
},

0 commit comments

Comments
 (0)