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