diff --git a/docs/guide/essentials/config/typescript.md b/docs/guide/essentials/config/typescript.md
index 8d939e2e..2bfa8b1d 100644
--- a/docs/guide/essentials/config/typescript.md
+++ b/docs/guide/essentials/config/typescript.md
@@ -17,28 +17,14 @@ Or if you're in a monorepo, you may not want to extend the config. If you don't
///
```
-## Compiler Options
-
-To specify custom compiler options, add them in `/tsconfig.json`:
-
-```jsonc
-// /tsconfig.json
-{
- "extends": ".wxt/tsconfig.json",
- "compilerOptions": {
- "jsx": "preserve",
- },
-}
-```
-
## TSConfig Paths
WXT provides a default set of path aliases.
| Alias | To | Example |
| ----- | ------------- | ----------------------------------------------- |
-| `~~` | `/*` | `import "~~/scripts"` |
-| `@@` | `/*` | `import "@@/scripts"` |
+| `~~` | `/*` | `import "~~/package.json"` |
+| `@@` | `/*` | `import "@@/package.json"` |
| `~` | `/*` | `import { toLowerCase } from "~/utils/strings"` |
| `@` | `/*` | `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
+ // /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.
diff --git a/packages/wxt/e2e/tests/hooks.test.ts b/packages/wxt/e2e/tests/hooks.test.ts
index 47ecb109..613a0b6f 100644
--- a/packages/wxt/e2e/tests/hooks.test.ts
+++ b/packages/wxt/e2e/tests/hooks.test.ts
@@ -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,
diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts
index 9710484a..673783ca 100644
--- a/packages/wxt/e2e/tests/typescript-project.test.ts
+++ b/packages/wxt/e2e/tests/typescript-project.test.ts
@@ -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', '');
+
+ 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', '');
@@ -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"
+ ]
}"
`);
});
diff --git a/packages/wxt/src/core/generate-wxt-dir.ts b/packages/wxt/src/core/generate-wxt-dir.ts
index 5ee5e043..05083b98 100644
--- a/packages/wxt/src/core/generate-wxt-dir.ts
+++ b/packages/wxt/src/core/generate-wxt-dir.ts
@@ -283,41 +283,42 @@ async function getTsConfigEntry(): Promise {
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),
};
}
diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts
index 1cb2aadf..5ebee7c7 100644
--- a/packages/wxt/src/types.ts
+++ b/packages/wxt/src/types.ts
@@ -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;
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