Compare commits

...

7 Commits

Author SHA1 Message Date
GitHub Actions 06a8c5fb0e chore(release): wxt v0.18.10 2024-06-25 20:03:59 +00:00
Aaron eb86413f35 chore: Consolidate unimport code into a built-in module (#771) 2024-06-25 14:59:28 -05:00
Aaron b29d49cbf1 fix: Await prepare:types hook 2024-06-24 10:04:44 -05:00
Aaron c0a0edb30c feat(modules): Allow adding generated public files (#769) 2024-06-24 08:46:45 -05:00
Aaron b15a933c75 feat: Add prepare:types hook to extend .wxt/ directory generation (#767) 2024-06-23 15:12:28 -05:00
Malix 1851be22a7 docs: Update CRXJS Firefox Compatibility (#764)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-06-23 10:32:09 -05:00
Aaron 39f6c29e4f chore: Refactor package manager test fixtures 2024-06-21 09:10:22 -05:00
31 changed files with 419 additions and 243 deletions
+26 -26
View File
@@ -4,30 +4,30 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
## Overview
| Features | WXT | Plasmo | CRXJS |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| Create Extension ZIPs | ✅ | ✅ | |
| Create Firefox Sources ZIP | ✅ | ❌ | |
| First-class TypeScript support | ✅ | ✅ | |
| Entrypoint discovery | File based | File based | |
| Inline entrypoint config | ✅ | ✅ | Manifest based |
| Auto-imports | ✅ | ❌ | |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | |
| Automated publishing | ✅ | ✅ | |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | |
| Features | WXT | Plasmo | CRXJS |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :-------------------------------------: |
| Supports all browsers | ✅ | ✅ | ✅ As of `v2.0.0-beta.23` |
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| Create Extension ZIPs | ✅ | ✅ | |
| Create Firefox Sources ZIP | ✅ | ❌ | |
| First-class TypeScript support | ✅ | ✅ | |
| Entrypoint discovery | File based | File based | |
| Inline entrypoint config | ✅ | ✅ | Manifest based |
| Auto-imports | ✅ | ❌ | |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | |
| Automated publishing | ✅ | ✅ | |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | |
| <strong style="opacity: 50%">Dev Mode</strong> | | |
| `.env` Files | ✅ | ✅ | |
| Opens browser and install extension | ✅ | ❌ | |
| HMR for UIs | ✅ | 🟡 React only | |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
| Storage | ✅ | ✅ | |
| Messaging | ❌ | ✅ | |
| Content Script UI | ✅ | ✅ | |
| `.env` Files | ✅ | ✅ | |
| Opens browser and install extension | ✅ | ❌ | |
| HMR for UIs | ✅ | 🟡 React only | |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
| Storage | ✅ | ✅ | |
| Messaging | ❌ | ✅ | |
| Content Script UI | ✅ | ✅ | |
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## v0.18.10
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)
### 🚀 Enhancements
- Add `prepare:types` hook to extend `.wxt/` directory generation ([#767](https://github.com/wxt-dev/wxt/pull/767))
- **modules:** Allow adding generated public files ([#769](https://github.com/wxt-dev/wxt/pull/769))
### 🩹 Fixes
- Await `prepare:types` hook ([b29d49c](https://github.com/wxt-dev/wxt/commit/b29d49c))
### 🏡 Chore
- Refactor package manager test fixtures ([39f6c29](https://github.com/wxt-dev/wxt/commit/39f6c29))
- Consolidate `unimport` code into a built-in module ([#771](https://github.com/wxt-dev/wxt/pull/771))
## v0.18.9
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)
+1 -1
View File
@@ -47,10 +47,10 @@ describe('Auto Imports', () => {
----------------------------------------
// Generated by wxt
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
/// <reference types="./types/imports.d.ts" />
"
`);
});
+5
View File
@@ -4,6 +4,7 @@ import { WxtHooks } from '~/types';
const hooks: WxtHooks = {
ready: vi.fn(),
'prepare:types': vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(),
@@ -41,6 +42,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'prepare:types': true,
'build:before': false,
'build:done': false,
'build:publicAssets': false,
@@ -60,6 +62,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'prepare:types': true,
'build:before': true,
'build:done': true,
'build:publicAssets': true,
@@ -79,6 +82,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'prepare:types': true,
'build:before': true,
'build:done': true,
'build:publicAssets': true,
@@ -104,6 +108,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({
ready: true,
'prepare:types': true,
'build:before': true,
'build:done': true,
'build:publicAssets': true,
+10 -1
View File
@@ -101,7 +101,13 @@ describe('Module Helpers', () => {
export default defineWxtModule((wxt) => {
addPublicAssets(wxt, "${normalizePath(dir)}")
})
wxt.hooks.hook("build:publicAssets", (_, assets) => {
assets.push({
relativeDest: "example/generated.txt",
contents: "",
});
});
});
`,
);
@@ -114,6 +120,9 @@ describe('Module Helpers', () => {
await expect(
project.fileExists('.output/chrome-mv3/module.txt'),
).resolves.toBe(true);
await expect(
project.fileExists('.output/chrome-mv3/example/generated.txt'),
).resolves.toBe(true);
});
it("should not overwrite the user's public files", async () => {
@@ -233,10 +233,10 @@ describe('TypeScript Project', () => {
----------------------------------------
// Generated by wxt
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
/// <reference types="./types/imports.d.ts" />
"
`);
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.18.9",
"version": "0.18.10",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -0,0 +1,4 @@
import { WxtModule } from '~/types';
import unimport from './unimport';
export const builtinModules: WxtModule<any>[] = [unimport];
@@ -0,0 +1,122 @@
import { addViteConfig, defineWxtModule } from '~/modules';
import type {
EslintGlobalsPropValue,
WxtDirFileEntry,
WxtModule,
WxtResolvedUnimportOptions,
} from '~/types';
import { type Unimport, createUnimport } from 'unimport';
import { Plugin } from 'vite';
import { extname } from 'node:path';
export default defineWxtModule({
name: 'wxt:built-in:unimport',
setup(wxt) {
const options = wxt.config.imports;
if (options === false) return;
let unimport: Unimport;
// Add user module imports to config
wxt.hooks.hook('ready', () => {
const addModuleImports = (module: WxtModule<any>) => {
if (!module.imports) return;
options.imports ??= [];
options.imports.push(...module.imports);
};
wxt.config.builtinModules.forEach(addModuleImports);
wxt.config.userModules.forEach(addModuleImports);
});
// Create unimport instance after READY so any modifications to the config
// inside "ready" are applied.
wxt.hooks.afterEach((event) => {
if (event.name === 'ready') {
unimport = createUnimport(options);
}
});
// Generate types
wxt.hooks.hook('prepare:types', async (_, entries) => {
// Update cache before each rebuild
await unimport.init();
entries.push(await getImportsDeclarationEntry(unimport));
if (!options.eslintrc.enabled) return;
entries.push(await getImportsEslintEntry(unimport, options));
});
// Add vite plugin
addViteConfig(wxt, () => ({
plugins: [vitePlugin(unimport)],
}));
},
});
export function vitePlugin(unimport: Unimport): Plugin {
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
]);
return {
name: 'wxt:unimport',
async transform(code, id) {
// Don't transform dependencies
if (id.includes('node_modules')) return;
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
};
},
};
}
async function getImportsDeclarationEntry(
unimport: Unimport,
): Promise<WxtDirFileEntry> {
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.init();
return {
path: 'types/imports.d.ts',
text: [
'// Generated by wxt',
await unimport.generateTypeDeclarations(),
'',
].join('\n'),
tsReference: true,
};
}
async function getImportsEslintEntry(
unimport: Unimport,
options: WxtResolvedUnimportOptions,
): Promise<WxtDirFileEntry> {
const globals: Record<string, EslintGlobalsPropValue> = {};
const eslintrc = { globals };
(await unimport.getImports())
.map((i) => i.as ?? i.name)
.filter(Boolean)
.sort()
.forEach((name) => {
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
});
return {
path: options.eslintrc.filePath,
text: JSON.stringify(eslintrc, null, 2) + '\n',
};
}
@@ -61,7 +61,6 @@ export async function createViteBuilder(
config.plugins.push(
wxtPlugins.download(wxtConfig),
wxtPlugins.devHtmlPrerender(wxtConfig, server),
wxtPlugins.unimport(wxtConfig),
wxtPlugins.resolveVirtualModules(wxtConfig),
wxtPlugins.devServerGlobals(wxtConfig, server),
wxtPlugins.tsconfigPaths(wxtConfig),
@@ -2,7 +2,6 @@ export * from './devHtmlPrerender';
export * from './devServerGlobals';
export * from './download';
export * from './multipageMove';
export * from './unimport';
export * from './resolveVirtualModules';
export * from './tsconfigPaths';
export * from './noopBackground';
@@ -1,43 +0,0 @@
import { createUnimport } from 'unimport';
import { ResolvedConfig } from '~/types';
import type * as vite from 'vite';
import { extname } from 'path';
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
]);
/**
* Inject any global imports defined by unimport
*/
export function unimport(config: ResolvedConfig): vite.PluginOption {
const options = config.imports;
if (options === false) return [];
const unimport = createUnimport(options);
return {
name: 'wxt:unimport',
async config() {
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
},
async transform(code, id) {
// Don't transform dependencies
if (id.includes('node_modules')) return;
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
};
},
};
}
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { bun } from '../bun';
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
describe.skipIf(() => process.platform === 'win32')(
'Bun Package Management Utils',
() => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-bun-project');
it('should list direct dependencies', async () => {
const actual = await bun.listDependencies({ cwd });
expect(actual).toEqual([
@@ -4,15 +4,14 @@ import { npm } from '../npm';
import { execaCommand } from 'execa';
import { exists } from 'fs-extra';
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
describe('NPM Package Management Utils', () => {
beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd });
}, 60e3);
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd });
}, 60e3);
it('should list direct dependencies', async () => {
const actual = await npm.listDependencies({ cwd });
expect(actual).toEqual([
@@ -32,6 +31,8 @@ describe('NPM Package Management Utils', () => {
});
describe('downloadDependency', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
it('should download the dependency as a tarball', async () => {
const downloadDir = path.resolve(cwd, 'dist');
const id = 'mime-db@1.52.0';
@@ -3,17 +3,16 @@ import path from 'node:path';
import { pnpm } from '../pnpm';
import { execaCommand } from 'execa';
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('PNPM Package Management Utils', () => {
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd });
});
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd });
});
it('should list direct dependencies', async () => {
const actual = await pnpm.listDependencies({ cwd });
expect(actual).toEqual([
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { yarn } from '../yarn';
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
describe('Yarn Package Management Utils', () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');
it('should list direct dependencies', async () => {
const actual = await yarn.listDependencies({ cwd });
expect(actual).toEqual([
@@ -45,14 +45,18 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = [];
for (const { absoluteSrc, relativeDest } of files) {
const absoluteDest = resolve(wxt.config.outDir, relativeDest);
for (const file of files) {
const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
await fs.ensureDir(dirname(absoluteDest));
await fs.copyFile(absoluteSrc, absoluteDest);
if ('absoluteSrc' in file) {
await fs.copyFile(file.absoluteSrc, absoluteDest);
} else {
await fs.writeFile(absoluteDest, file.contents, 'utf8');
}
publicAssets.push({
type: 'asset',
fileName: relativeDest,
fileName: file.relativeDest,
});
}
@@ -1,11 +1,6 @@
import { Unimport, createUnimport } from 'unimport';
import {
EslintGlobalsPropValue,
Entrypoint,
WxtResolvedUnimportOptions,
} from '~/types';
import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '~/types';
import fs from 'fs-extra';
import { relative, resolve } from 'path';
import { dirname, relative, resolve } from 'node:path';
import {
getEntrypointBundlePath,
isHtmlEntrypoint,
@@ -25,61 +20,57 @@ export async function generateTypesDir(
): Promise<void> {
await fs.ensureDir(wxt.config.typesDir);
const references: string[] = [];
const entries: WxtDirEntry[] = [
// Hard-coded entries
{ module: 'wxt/vite-builder-env' },
];
if (wxt.config.imports !== false) {
const unimport = createUnimport(wxt.config.imports);
references.push(await writeImportsDeclarationFile(unimport));
if (wxt.config.imports.eslintrc.enabled) {
await writeImportsEslintFile(unimport, wxt.config.imports);
}
}
// Add references to modules installed from NPM to the TS project so their
// type augmentation can update InlineConfig correctly. Local modules defined
// in <root>/modules are already apart of the project, so we don't need to
// add them.
wxt.config.userModules.forEach((module) => {
if (module.type === 'node_module' && module.configKey != null)
entries.push({ module: module.id });
});
references.push(await writePathsDeclarationFile(entrypoints));
references.push(await writeI18nDeclarationFile());
references.push(await writeGlobalsDeclarationFile());
// browser.runtime.getURL
entries.push(await getPathsDeclarationEntry(entrypoints));
const mainReference = await writeMainDeclarationFile(references);
await writeTsConfigFile(mainReference);
}
// browser.i18n.getMessage
entries.push(await getI18nDeclarationEntry());
async function writeImportsDeclarationFile(unimport: Unimport) {
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
// import.meta.env.*
entries.push(await getGlobalsDeclarationEntry());
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
// tsconfig.json
entries.push(await getTsConfigEntry());
await writeFileIfDifferent(
filePath,
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
'\n',
) + '\n',
// Let modules add more entries
await wxt.hooks.callHook('prepare:types', wxt, entries);
// Add main declaration file, not editable
entries.push(getMainDeclarationEntry(entries));
// Write all the files
const absoluteFileEntries = (
entries.filter((entry) => 'path' in entry) as WxtDirFileEntry[]
).map<WxtDirFileEntry>((entry) => ({
...entry,
path: resolve(wxt.config.wxtDir, entry.path),
}));
await Promise.all(
absoluteFileEntries.map(async (file) => {
await fs.ensureDir(dirname(file.path));
await writeFileIfDifferent(file.path, file.text);
}),
);
return filePath;
}
async function writeImportsEslintFile(
unimport: Unimport,
options: WxtResolvedUnimportOptions,
) {
const globals: Record<string, EslintGlobalsPropValue> = {};
const eslintrc = { globals };
(await unimport.getImports())
.map((i) => i.as ?? i.name)
.filter(Boolean)
.sort()
.forEach((name) => {
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
});
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
}
async function writePathsDeclarationFile(
async function getPathsDeclarationEntry(
entrypoints: Entrypoint[],
): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
): Promise<WxtDirFileEntry> {
const unions = entrypoints
.map((entry) =>
getEntrypointBundlePath(
@@ -108,16 +99,14 @@ declare module "wxt/browser" {
}
`;
await writeFileIfDifferent(
filePath,
template.replace('{{ union }}', unions || ' | never'),
);
return filePath;
return {
path: 'types/paths.d.ts',
text: template.replace('{{ union }}', unions || ' | never'),
tsReference: true,
};
}
async function writeI18nDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
async function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {
const defaultLocale = wxt.config.manifest.default_locale;
const template = `// Generated by wxt
import "wxt/browser";
@@ -165,20 +154,19 @@ declare module "wxt/browser" {
options?: GetMessageOptions,
): string;`;
});
await writeFileIfDifferent(
filePath,
template.replace('{{ overrides }}', overrides.join('\n')),
);
return filePath;
return {
path: 'types/i18n.d.ts',
text: template.replace('{{ overrides }}', overrides.join('\n')),
tsReference: true,
};
}
async function writeGlobalsDeclarationFile(): Promise<string> {
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
async function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
await writeFileIfDifferent(
filePath,
[
return {
path: 'types/globals.d.ts',
text: [
'// Generated by wxt',
'export {}',
'interface ImportMetaEnv {',
@@ -187,39 +175,30 @@ async function writeGlobalsDeclarationFile(): Promise<string> {
'interface ImportMeta {',
' readonly env: ImportMetaEnv',
'}',
].join('\n') + '\n',
);
return filePath;
'',
].join('\n'),
tsReference: true,
};
}
async function writeMainDeclarationFile(references: string[]): Promise<string> {
const dir = wxt.config.wxtDir;
const filePath = resolve(dir, 'wxt.d.ts');
await writeFileIfDifferent(
filePath,
[
'// Generated by wxt',
`/// <reference types="wxt/vite-builder-env" />`,
...references.map(
(ref) =>
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
),
// Add references to modules installed from NPM to the TS project so
// their type augmentation can update InlineConfig correctly. Local
// modules defined in <root>/modules are already apart of the project, so
// we don't need to add them.
...wxt.config.modules
.filter(
(module) => module.type === 'node_module' && module.configKey != null,
)
.map((module) => `/// <reference types="${module.id}" />`),
].join('\n') + '\n',
);
return filePath;
function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
const lines = ['// Generated by wxt'];
references.forEach((ref) => {
if ('module' in ref) {
return lines.push(`/// <reference types="${ref.module}" />`);
} else if (ref.tsReference) {
const absolutePath = resolve(wxt.config.wxtDir, ref.path);
const relativePath = relative(wxt.config.wxtDir, absolutePath);
lines.push(`/// <reference types="./${normalizePath(relativePath)}" />`);
}
});
return {
path: 'wxt.d.ts',
text: lines.join('\n') + '\n',
};
}
async function writeTsConfigFile(mainReference: string) {
async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
const dir = wxt.config.wxtDir;
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
const paths = Object.entries(wxt.config.alias)
@@ -232,9 +211,7 @@ async function writeTsConfigFile(mainReference: string) {
})
.join(',\n');
await writeFileIfDifferent(
resolve(dir, 'tsconfig.json'),
`{
const text = `{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
@@ -251,9 +228,13 @@ ${paths}
},
"include": [
"${getTsconfigPath(wxt.config.root)}/**/*",
"./${getTsconfigPath(mainReference)}"
"./wxt.d.ts"
],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`,
);
}`;
return {
path: 'tsconfig.json',
text,
};
}
@@ -22,6 +22,7 @@ import { isModuleInstalled } from '../package';
import fs from 'fs-extra';
import { normalizePath } from '../paths';
import glob from 'fast-glob';
import { builtinModules } from '~/builtin-modules';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
@@ -127,15 +128,21 @@ export async function resolveConfig(
};
}
const modules = await resolveWxtModules(modulesDir, mergedConfig.modules);
const moduleOptions = modules.reduce<Record<string, any>>((map, module) => {
if (module.configKey) {
map[module.configKey] =
// @ts-expect-error
mergedConfig[module.configKey];
}
return map;
}, {});
const userModules = await resolveWxtUserModules(
modulesDir,
mergedConfig.modules,
);
const moduleOptions = userModules.reduce<Record<string, any>>(
(map, module) => {
if (module.configKey) {
map[module.configKey] =
// @ts-expect-error
mergedConfig[module.configKey];
}
return map;
},
{},
);
return {
browser,
@@ -146,7 +153,7 @@ export async function resolveConfig(
filterEntrypoints,
env,
fsCache: createFsCache(wxtDir),
imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion,
@@ -175,7 +182,8 @@ export async function resolveConfig(
},
hooks: mergedConfig.hooks ?? {},
vite: mergedConfig.vite ?? (() => ({})),
modules,
builtinModules,
userModules,
plugins: [],
...moduleOptions,
};
@@ -285,20 +293,21 @@ function resolveAnalysisConfig(
async function getUnimportOptions(
wxtDir: string,
srcDir: string,
logger: Logger,
config: InlineConfig,
): Promise<WxtResolvedUnimportOptions | false> {
if (config.imports === false) return false;
const enabledConfig = config.imports?.eslintrc?.enabled;
let enabled: boolean;
switch (enabledConfig) {
const rawEslintEnabled = config.imports?.eslintrc?.enabled;
let eslintEnabled: boolean;
switch (rawEslintEnabled) {
case undefined:
case 'auto':
enabled = await isModuleInstalled('eslint');
eslintEnabled = await isModuleInstalled('eslint');
break;
default:
enabled = enabledConfig;
eslintEnabled = rawEslintEnabled;
}
const defaultOptions: WxtResolvedUnimportOptions = {
@@ -315,8 +324,11 @@ async function getUnimportOptions(
],
warn: logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
dirsScanOptions: {
cwd: srcDir,
},
eslintrc: {
enabled,
enabled: eslintEnabled,
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
globalsPropValue: true,
},
@@ -378,7 +390,7 @@ export async function mergeBuilderConfig(
throw Error('Builder not found. Make sure vite is installed.');
}
export async function resolveWxtModules(
export async function resolveWxtUserModules(
modulesDir: string,
modules: string[] = [],
): Promise<WxtModuleWithMetadata<any>[]> {
@@ -239,7 +239,8 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
command,
entrypointsDir: fakeDir(),
modulesDir: fakeDir(),
modules: [],
builtinModules: [],
userModules: [],
env: { browser, command, manifestVersion, mode },
fsCache: mock<FsCache>(),
imports: {
+6 -6
View File
@@ -5,11 +5,13 @@ import {
WxtCommand,
WxtDevServer,
WxtHooks,
WxtModule,
} from '~/types';
import { resolveConfig } from './utils/building';
import { createHooks } from 'hookable';
import { createWxtPackageManager } from './package-managers';
import { createViteBuilder } from './builders/vite';
import { builtinModules } from '~/builtin-modules';
/**
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
@@ -46,18 +48,16 @@ export async function registerWxt(
};
// Initialize modules
for (const module of config.modules) {
const initModule = async (module: WxtModule<any>) => {
if (module.hooks) wxt.hooks.addHooks(module.hooks);
if (wxt.config.imports !== false && module.imports) {
wxt.config.imports.imports ??= [];
wxt.config.imports.imports.push(...module.imports);
}
await module.setup?.(
wxt,
// @ts-expect-error: Untyped configKey field
module.configKey ? config[module.configKey] : undefined,
);
}
};
for (const builtinModule of builtinModules) await initModule(builtinModule);
for (const userModule of config.userModules) await initModule(userModule);
// Initialize hooks
wxt.hooks.addHooks(config.hooks);
+16 -8
View File
@@ -1,6 +1,5 @@
import type * as vite from 'vite';
import {
unimport,
download,
tsconfigPaths,
globals,
@@ -8,6 +7,8 @@ import {
} from '../core/builders/vite/plugins';
import { resolveConfig } from '~/core/utils/building';
import { InlineConfig } from '../types';
import { vitePlugin as unimportPlugin } from '~/builtin-modules/unimport';
import { createUnimport } from 'unimport';
/**
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts`
@@ -25,11 +26,18 @@ import { InlineConfig } from '../types';
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
*/
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [
webextensionPolyfillMock(config),
unimport(config),
globals(config),
download(config),
tsconfigPaths(config),
]);
return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => {
const plugins = [
webextensionPolyfillMock(config),
globals(config),
download(config),
tsconfigPaths(config),
];
if (config.imports !== false) {
const unimport = createUnimport(config.imports);
await unimport.init();
plugins.push(unimportPlugin(unimport));
}
return plugins;
});
}
+65 -8
View File
@@ -1034,6 +1034,23 @@ export interface WxtHooks {
* @returns Promise
*/
ready: (wxt: Wxt) => HookResult;
/**
* Called before WXT writes .wxt/tsconfig.json and .wxt/wxt.d.ts, allowing
* addition of custom references and declarations in wxt.d.ts, or directly
* modifying the options in `tsconfig.json`.
*
* @example
* wxt.hooks.hook("prepare:types", (wxt, entries) => {
* // Add a file, ".wxt/types/example.d.ts", that defines a global
* // variable called "example" in the TS project.
* entries.push({
* path: "types/example.d.ts",
* textContent: "declare const a: string;",
* tsReference: true,
* });
* })
*/
'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;
/**
* Called before the build is started in both dev mode and build mode.
*
@@ -1188,7 +1205,8 @@ export interface ResolvedConfig {
reloadCommand: string | false;
};
hooks: NestedHooks<WxtHooks>;
modules: WxtModuleWithMetadata<any>[];
builtinModules: WxtModule<any>[];
userModules: WxtModuleWithMetadata<any>[];
/**
* An array of string to import plugins from. These paths should be
* resolvable by vite, and they should `export default defineWxtPlugin(...)`.
@@ -1334,13 +1352,9 @@ export interface WxtModuleWithMetadata<TOptions extends WxtModuleOptions>
id: string;
}
export interface ResolvedPublicFile {
/**
* The absolute path to the file that will be copied to the output directory.
* @example
* "/path/to/any/file.css"
*/
absoluteSrc: string;
export type ResolvedPublicFile = CopiedPublicFile | GeneratedPublicFile;
export interface ResolvedBasePublicFile {
/**
* The relative path in the output directory to copy the file to.
* @example
@@ -1349,4 +1363,47 @@ export interface ResolvedPublicFile {
relativeDest: string;
}
export interface CopiedPublicFile extends ResolvedBasePublicFile {
/**
* The absolute path to the file that will be copied to the output directory.
* @example
* "/path/to/any/file.css"
*/
absoluteSrc: string;
}
export interface GeneratedPublicFile extends ResolvedBasePublicFile {
/**
* Text to write to the file.
*/
contents: string;
}
export type WxtPlugin = () => void;
export type WxtDirEntry = WxtDirTypeReferenceEntry | WxtDirFileEntry;
/**
* Represents type reference to a node module to be added to `.wxt/wxt.d.ts` file
*/
export interface WxtDirTypeReferenceEntry {
module: string;
}
/**
* Represents a file to be written to the project's `.wxt/` directory.
*/
export interface WxtDirFileEntry {
/**
* Path relative to the `.wxt/` directory. So "tsconfig.json" would resolve to ".wxt/tsconfig.json".
*/
path: string;
/**
* The text that will be written to the file.
*/
text: string;
/**
* Set to `true` to add a reference to this file in `.wxt/wxt.d.ts`.
*/
tsReference?: boolean;
}