Skip to content

Commit 74b56c7

Browse files
ntottenclaude
andauthored
Convert to ESM (#3860)
* 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 <noreply@anthropic.com> * Add test for ESM config with plugin import statements Tests loading plugins via ESM import in prettier.config.mts files, which is a new feature in Prettier 3.5.0+. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Formatting * Update readme migraiton warning to remove github admonition * Simplify resolveModuleEntry to use Node's built-in resolution Replace manual package.json parsing with createRequire().resolve() which handles all the complexity of exports, main fields, and fallbacks. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Rename module resolver files with platform suffix - Rename ModuleResolver.ts → ModuleResolverNode.ts (desktop) - Rename BrowserModuleResolver.ts → ModuleResolverWeb.ts (browser) - Keep class export name as ModuleResolver for simplicity - Update esbuild browser alias plugin for new file names - Update documentation to reflect new file naming convention 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix resolveModuleEntry to resolve from module's directory Create the require function from within the target module's directory so Node's resolution works correctly for local Prettier installations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify plugin loading: pass paths to Prettier instead of pre-loading - Fix scoped package resolution in resolveModuleEntry (e.g., @prettier/plugin-xml) - Simplify plugin loading to just resolve paths and let Prettier import them - Update browser languages script to dynamically get plugin list from prettier exports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Read package name from package.json instead of parsing path 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify plugin resolution to match Prettier CLI behavior - Handle URLs, absolute paths, relative paths, and package names - Use createRequire to resolve packages from file's directory - Remove unused findUp import 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix plugin loading for Prettier v2 and improve module resolution - Use require.resolve with createRequire from target directory - Only convert plugin paths to file:// URLs for Prettier v3+ - For Prettier v2, pass absolute paths that require() can handle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e7bc410 commit 74b56c7

65 files changed

Lines changed: 1779 additions & 1577 deletions

Some content is hidden

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

.claude/agents/vscode-test-writer.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,38 +45,35 @@ When writing tests, you will:
4545

4646
For this VS Code extension project:
4747

48-
- Tests live alongside source in a test directory structure
48+
- Tests live in `src/test/suite/` directory
4949
- Test fixtures are in `test-fixtures/` with their own package.json files
50-
- Tests run via `yarn test` or the VS Code Debug launcher "Launch Tests"
51-
- The extension uses webpack bundling, but tests run against unbundled source
52-
- Key components to test: `PrettierEditService`, `ModuleResolver`, formatting providers
50+
- Tests run via `npm test` or the VS Code Debug launcher "Launch Tests"
51+
- The extension uses esbuild bundling with ESM modules
52+
- Key components to test: `PrettierEditService`, `ModuleResolver`, `PrettierDynamicInstance`
53+
- Use `ensureExtensionActivated()` from `testUtils.js` to ensure extension is ready before tests
5354

5455
## Test File Structure
5556

5657
```typescript
5758
import * as assert from "assert";
5859
import * as vscode from "vscode";
59-
import * as sinon from "sinon";
60+
import { ensureExtensionActivated } from "./testUtils.js"; // Note: .js extension for ESM
6061

61-
suite("ComponentName Test Suite", () => {
62-
let sandbox: sinon.SinonSandbox;
63-
64-
setup(() => {
65-
sandbox = sinon.createSandbox();
66-
});
67-
68-
teardown(() => {
69-
sandbox.restore();
62+
describe("ComponentName Test Suite", () => {
63+
before(async () => {
64+
await ensureExtensionActivated();
7065
});
7166

72-
test("should describe expected behavior", async () => {
67+
it("should describe expected behavior", async () => {
7368
// Arrange
7469
// Act
7570
// Assert
7671
});
7772
});
7873
```
7974

75+
**Note**: This project uses ESM modules. Always use `.js` extension for local imports.
76+
8077
## Quality Standards
8178

8279
- Tests must be deterministic and not flaky

.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/copilot-instructions.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ This is the official Prettier VS Code extension (`prettier.prettier-vscode`). It
1616
Entry points:
1717

1818
- Desktop: `src/extension.ts` → bundled to `dist/extension.js`
19-
- Browser: Same entry, bundled to `dist/web-extension.js` (uses `BrowserModuleResolver` instead of `ModuleResolver`)
19+
- Browser: Same entry, bundled to `dist/web-extension.cjs` (esbuild swaps `ModuleResolverNode.ts``ModuleResolverWeb.ts`)
2020

2121
Core components:
2222

23-
- `src/extension.ts` - Extension activation, creates ModuleResolver, PrettierEditService, and StatusBar
23+
- `src/extension.ts` - Extension activation (async), creates ModuleResolver, PrettierEditService, and StatusBar
2424
- `src/PrettierEditService.ts` - Registers VS Code document formatting providers, handles format requests
25-
- `src/ModuleResolver.ts` - Resolves local/global Prettier installations, falls back to bundled Prettier
26-
- `src/PrettierInstance.ts` - Interface for Prettier loading, with `PrettierMainThreadInstance` and `PrettierWorkerInstance` implementations
25+
- `src/ModuleResolverNode.ts` - Resolves local/global Prettier installations, falls back to bundled Prettier (desktop)
26+
- `src/ModuleResolverWeb.ts` - Uses bundled standalone Prettier for browser (web)
27+
- `src/PrettierDynamicInstance.ts` - Implements `PrettierInstance` interface, loads Prettier dynamically using ESM `import()`
28+
- `src/types.ts` - TypeScript types including `PrettierInstance` interface
2729

2830
esbuild produces two bundles:
2931

@@ -69,11 +71,11 @@ When reviewing pull requests, focus on:
6971
### Performance
7072

7173
- Avoid blocking the extension host main thread
72-
- `PrettierWorkerInstance` runs Prettier in a worker thread to avoid blocking
74+
- Prettier is loaded lazily using dynamic `import()` to minimize startup time
7375
- Module and config resolution results are cached appropriately
7476

7577
### Browser Compatibility
7678

7779
- Code in the main bundle should work in both Node.js and browser contexts
78-
- Browser-specific code uses `BrowserModuleResolver`
80+
- Browser build swaps `ModuleResolverNode.ts``ModuleResolverWeb.ts` via esbuild aliasing
7981
- No Node.js-only APIs in shared code paths

.github/instructions/tests.instructions.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ Tests run inside a VS Code Extension Development Host using Mocha.
88

99
## Test Structure
1010

11-
- Use Mocha's `suite()` and `test()` functions
11+
- Use Mocha's `describe()` and `it()` functions (BDD style)
1212
- Import assertions from `assert` module
1313
- Tests interact with real VS Code APIs
14+
- Use `.js` extension for local imports (ESM compatibility)
1415

1516
## Test Fixtures
1617

@@ -28,8 +29,6 @@ Tests run inside a VS Code Extension Development Host using Mocha.
2829
## Async Patterns
2930

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

3433
## Test File Naming
3534

.github/instructions/typescript.instructions.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This is a VS Code extension. Follow these patterns:
1111
- Import VS Code API from `vscode` module: `import { commands, workspace, window } from "vscode"`
1212
- Use named imports, not namespace imports
1313
- Group imports: vscode first, then node modules, then local modules
14+
- **ESM**: Always use `.js` extension for local imports (e.g., `import { foo } from "./utils.js"`)
1415

1516
## VS Code Extension Patterns
1617

@@ -22,13 +23,13 @@ This is a VS Code extension. Follow these patterns:
2223
## Class Patterns
2324

2425
- Services follow dependency injection pattern (pass dependencies via constructor)
25-
- Key services: `LoggingService`, `ModuleResolver`, `PrettierEditService`, `StatusBar`
26+
- Key services: `LoggingService`, `ModuleResolver` (from `ModuleResolverNode.ts`/`ModuleResolverWeb.ts`), `PrettierEditService`, `StatusBar`
2627
- Use `Disposable` interface for cleanup
2728

2829
## Prettier Integration
2930

30-
- Support both Prettier v2 and v3+ via `PrettierInstance` interface
31-
- `PrettierMainThreadInstance` loads directly, `PrettierWorkerInstance` uses worker thread
31+
- Support both Prettier v2 and v3+ via `PrettierInstance` interface (defined in `src/types.ts`)
32+
- `PrettierDynamicInstance` loads Prettier using dynamic ESM `import()` for lazy loading
3233
- Module resolution: local install → global install → bundled Prettier
3334
- Handle `.prettierrc`, `.prettierignore`, and `package.json` prettier config
3435

.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
}

CLAUDE.md

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Web tests are located in `src/test/web/suite/` and test the extension's browser
6363
### Entry Points
6464

6565
- **Desktop**: `src/extension.ts` → bundled to `dist/extension.js`
66-
- **Browser**: Same entry, bundled to `dist/web-extension.js` (uses `BrowserModuleResolver` instead of `ModuleResolver`)
66+
- **Browser**: Same entry, bundled to `dist/web-extension.cjs` (esbuild swaps `ModuleResolverNode.ts``ModuleResolverWeb.ts`)
6767

6868
### Core Components
6969

@@ -79,28 +79,25 @@ Web tests are located in `src/test/web/suite/` and test the extension's browser
7979
- Watches for config file changes (`.prettierrc`, `package.json`, etc.)
8080
- Builds language selectors based on Prettier's supported languages + plugins
8181

82-
**ModuleResolver** (`src/ModuleResolver.ts`):
82+
**ModuleResolver** (`src/ModuleResolverNode.ts` for desktop, `src/ModuleResolverWeb.ts` for browser):
8383

84-
- Resolves local/global Prettier installations
85-
- Falls back to bundled Prettier if none found
86-
- Caches resolved modules and configurations
87-
- Handles Workspace Trust restrictions
84+
- **Desktop (ModuleResolverNode.ts)**: Resolves local/global Prettier installations, falls back to bundled Prettier, caches resolved modules, handles Workspace Trust
85+
- **Browser (ModuleResolverWeb.ts)**: Uses bundled Prettier standalone with all built-in plugins
8886

89-
**Prettier Instance** (`PrettierInstance.ts`, `PrettierMainThreadInstance.ts`, `PrettierWorkerInstance.ts`):
87+
**Prettier Instance** (`src/PrettierDynamicInstance.ts`):
9088

91-
- `PrettierInstance` is an interface with two implementations
92-
- `PrettierMainThreadInstance` loads Prettier directly via `require()`
93-
- `PrettierWorkerInstance` loads Prettier in a worker thread to avoid blocking
89+
- Implements the `PrettierInstance` interface (defined in `src/types.ts`)
90+
- Loads Prettier dynamically using ESM `import()` for lazy loading
9491
- Works with both Prettier v2 and v3+
9592

9693
### Bundling
9794

9895
esbuild produces two bundles:
9996

10097
- Node bundle (`dist/extension.js`) for desktop VS Code
101-
- Web bundle (`dist/web-extension.js`) for vscode.dev/browser
98+
- Web bundle (`dist/web-extension.cjs`) for vscode.dev/browser
10299

103-
The browser build uses path aliasing to swap `ModuleResolver``BrowserModuleResolver`.
100+
The browser build uses path aliasing to swap `ModuleResolverNode.ts``ModuleResolverWeb.ts`.
104101

105102
Build configuration is in `esbuild.mjs`.
106103

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

0 commit comments

Comments
 (0)