feat: Add prepare:tsconfig hook for updating .wxt/tsconfig.json file (#2450)

This commit is contained in:
Aaron
2026-06-29 09:42:19 -05:00
committed by GitHub
parent 6f1790deef
commit cfc633202b
5 changed files with 198 additions and 77 deletions
+33 -16
View File
@@ -17,28 +17,14 @@ Or if you're in a monorepo, you may not want to extend the config. If you don't
/// <reference path="./.wxt/wxt.d.ts" />
```
## Compiler Options
To specify custom compiler options, add them in `<rootDir>/tsconfig.json`:
```jsonc
// <rootDir>/tsconfig.json
{
"extends": ".wxt/tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
},
}
```
## TSConfig Paths
WXT provides a default set of path aliases.
| Alias | To | Example |
| ----- | ------------- | ----------------------------------------------- |
| `~~` | `<rootDir>/*` | `import "~~/scripts"` |
| `@@` | `<rootDir>/*` | `import "@@/scripts"` |
| `~~` | `<rootDir>/*` | `import "~~/package.json"` |
| `@@` | `<rootDir>/*` | `import "@@/package.json"` |
| `~` | `<srcDir>/*` | `import { toLowerCase } from "~/utils/strings"` |
| `@` | `<srcDir>/*` | `import { toLowerCase } from "@/utils/strings"` |
@@ -63,3 +49,34 @@ export default defineConfig({
import { fakeTab } from 'testing/fake-objects';
import { toLowerCase } from 'strings';
```
## Custom Options
To specify custom compiler or top-level options, you have two options:
1. Override WXT's value completely by setting it in your root `tsconfig.json`:
```jsonc
// <rootDir>/tsconfig.json
{
"extends": ".wxt/tsconfig.json",
"compilerOptions": {
"jsx": "preserve",
},
}
```
2. Merge custom values into values generated by WXT via the `prepare:tsconfig` hook:
```ts
// wxt.config.ts
export default defineConfig({
hooks: {
'prepare:tsconfig': (wxt, { tsconfig }) => {
tsconfig.compilerOptions.lib.push('WebWorker');
},
},
});
```
Overriding is great for simple boolean values, but not for complex options like `paths` or `libs`, where you can't just "add" a value to the object from your `tsconfig.json` file. So WXT provides the hook that let's you add or merge or delete or do whatever you want to the config file before it's written to the `.wxt/` directory.
+6
View File
@@ -6,6 +6,7 @@ const hooks: WxtHooks = {
ready: vi.fn(),
'config:resolved': vi.fn(),
'prepare:types': vi.fn(),
'prepare:tsconfig': vi.fn(),
'prepare:publicPaths': vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
@@ -56,6 +57,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': false,
'build:done': false,
@@ -88,6 +90,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -120,6 +123,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -152,6 +156,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
@@ -191,6 +196,7 @@ describe('Hooks', () => {
ready: true,
'config:resolved': true,
'prepare:types': true,
'prepare:tsconfig': true,
'prepare:publicPaths': true,
'build:before': true,
'build:done': true,
+110 -29
View File
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { readFile } from 'node:fs/promises';
describe('TypeScript Project', () => {
it('should generate defined constants correctly', async () => {
@@ -333,25 +334,63 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"@": [".."],
"@/*": ["../*"],
"~": [".."],
"~/*": ["../*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"@": [
".."
],
"@/*": [
"../*"
],
"~": [
".."
],
"~/*": [
"../*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
it("should allow updating the project's tsconfig via hooks", async () => {
const project = new TestProject();
project.setConfigFileConfig({});
project.addFile('entrypoints/unlisted.html', '<html></html>');
await project.prepare({
hooks: {
'prepare:tsconfig': (_, { tsconfig }) => {
tsconfig.test = 'test';
},
},
});
const actual = JSON.parse(
await readFile(project.resolvePath('.wxt/tsconfig.json'), 'utf-8'),
);
expect(actual).toMatchObject({ test: 'test' });
});
it('should generate correct path aliases for a custom srcDir', async () => {
const project = new TestProject();
project.addFile('src/entrypoints/unlisted.html', '<html></html>');
@@ -377,21 +416,40 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"@": [
"../src"
],
"@/*": [
"../src/*"
],
"~": [
"../src"
],
"~/*": [
"../src/*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
@@ -425,23 +483,46 @@ describe('TypeScript Project', () => {
"strict": true,
"skipLibCheck": true,
"paths": {
"example": ["../example"],
"example/*": ["../example/*"],
"@": ["../src"],
"@/*": ["../src/*"],
"~": ["../src"],
"~/*": ["../src/*"],
"@@": [".."],
"@@/*": ["../*"],
"~~": [".."],
"~~/*": ["../*"]
"example": [
"../example"
],
"example/*": [
"../example/*"
],
"@": [
"../src"
],
"@/*": [
"../src/*"
],
"~": [
"../src"
],
"~/*": [
"../src/*"
],
"@@": [
".."
],
"@@/*": [
"../*"
],
"~~": [
".."
],
"~~/*": [
"../*"
]
}
},
"include": [
"../**/*",
"./wxt.d.ts"
],
"exclude": ["../.output"]
"exclude": [
"../**/node_modules",
"../.output"
]
}"
`);
});
+33 -32
View File
@@ -283,41 +283,42 @@ async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
if (res.startsWith('.') || res.startsWith('/')) return res;
return './' + res;
};
const paths = Object.entries(wxt.config.alias)
.flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
`"${alias}": ["${aliasPath}"]`,
`"${alias}/*": ["${aliasPath}/*"]`,
];
})
.map((line) => ` ${line}`)
.join(',\n');
const text = `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"paths": {
${paths}
}
},
"include": [
"${getTsconfigPath(wxt.config.root)}/**/*",
"./wxt.d.ts"
],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`;
const paths = Object.fromEntries(
Object.entries(wxt.config.alias)
.map(([alias, absolutePath]) => [alias, getTsconfigPath(absolutePath)])
.flatMap(([alias, path]) => [
// Add alias for files
[alias, [path]],
// Add alias for all files in a directory
[alias + '/*', [path + '/*']],
]),
);
const tsconfig = {
compilerOptions: {
target: 'ESNext',
module: 'ESNext',
moduleResolution: 'Bundler',
noEmit: true,
esModuleInterop: true,
forceConsistentCasingInFileNames: true,
resolveJsonModule: true,
strict: true,
skipLibCheck: true,
paths,
},
include: [`${getTsconfigPath(wxt.config.root)}/**/*`, './wxt.d.ts'],
exclude: [
getTsconfigPath(wxt.config.root) + '/**/node_modules',
getTsconfigPath(wxt.config.outBaseDir),
],
};
await wxt.hooks.callHook('prepare:tsconfig', wxt, { tsconfig });
return {
path: 'tsconfig.json',
text,
text: JSON.stringify(tsconfig, null, 2),
};
}
+16
View File
@@ -1259,6 +1259,11 @@ export interface ServerInfo {
origin: string;
}
export type PrepareTsconfigs = {
/** The JSON contents of the `.wxt/tsconfig.json` file. */
tsconfig: any;
};
export type HookResult = Promise<void> | void;
export interface WxtHooks {
@@ -1298,6 +1303,17 @@ export interface WxtHooks {
* });
*/
'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;
/**
* Called before WXT writes your tsconfig to the disk, allowing full
* customization by modifying the object by reference.
*
* @since 0.20.28
* @example
* wxt.hooks.hook('prepare:tsconfig', (wxt, { tsconfig }) => {
* tsconfig.compilerOptions.lib.push('WebWorker');
* });
*/
'prepare:tsconfig': (wxt: Wxt, configs: PrepareTsconfigs) => HookResult;
/**
* Called before generating the list of public paths inside
* `.wxt/types/paths.d.ts`. Use this hook to add additional paths (relative to