Compare commits

...

16 Commits

Author SHA1 Message Date
GitHub Actions 5801f96f79 chore(release): v0.5.5 2023-09-19 23:00:53 +00:00
Aaron Klinker 92039b855e fix: Automatically replace vite's process.env.NODE_ENV output in lib mode with the mode 2023-09-19 17:56:10 -05:00
GitHub Actions f145e00cc9 chore(release): v0.5.4 2023-09-19 22:39:57 +00:00
Aaron Klinker 87e8df90f5 fix: Don't warn about deep entrypoint subdirectories not being recognized 2023-09-19 17:32:25 -05:00
Aaron Klinker 419fab8193 fix: Recognize background/index.ts as an entrypoint 2023-09-19 17:32:25 -05:00
GitHub Actions 71743b3bf3 chore(release): v0.5.3 2023-09-19 21:40:41 +00:00
Aaron 8e3169eb1e chore: Refactor how config is resolved (#118) 2023-09-19 16:36:00 -05:00
Aaron Klinker 4ec904ea3c fix: Allow function for vite config 2023-09-19 12:53:35 -05:00
GitHub Actions b284305605 chore(release): v0.5.2 2023-09-19 15:55:16 +00:00
Aaron 234bd39a11 fix: Import client utils when getting entrypoint config (#117) 2023-09-19 10:50:41 -05:00
Aaron b5acea106d Bump templates to ^0.5.0 (#116) 2023-09-15 13:57:10 -05:00
GitHub Actions 33ed04ff6b chore(release): v0.5.1 2023-09-15 18:56:09 +00:00
Aaron f51bbc0a68 feat: Include/exclude entrypoints based on target browser (#115) 2023-09-15 13:51:53 -05:00
Aaron 6ee5b22e6c feat: Allow disabling auto-imports (#114) 2023-09-15 12:25:22 -05:00
Aaron Klinker b4de93d2e6 fix: Allow any string for target browser 2023-09-15 12:24:52 -05:00
Aaron Klinker 2570debc5c Update changelog 2023-09-14 15:19:32 -05:00
41 changed files with 824 additions and 333 deletions
+73 -2
View File
@@ -1,5 +1,77 @@
# Changelog
## v0.5.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.4...v0.5.5)
### 🩹 Fixes
- Automatically replace vite's `process.env.NODE_ENV` output in lib mode with the mode ([92039b8](https://github.com/wxt-dev/wxt/commit/92039b8))
### ❤️ Contributors
- Aaron Klinker <aaronklinker1@gmail.com>
## v0.5.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.3...v0.5.4)
### 🩹 Fixes
- Recognize `background/index.ts` as an entrypoint ([419fab8](https://github.com/wxt-dev/wxt/commit/419fab8))
- Don't warn about deep entrypoint subdirectories not being recognized ([87e8df9](https://github.com/wxt-dev/wxt/commit/87e8df9))
### ❤️ Contributors
- Aaron Klinker <aaronklinker1@gmail.com>
## v0.5.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.2...v0.5.3)
### 🩹 Fixes
- Allow function for vite config ([4ec904e](https://github.com/wxt-dev/wxt/commit/4ec904e))
### 🏡 Chore
- Refactor how config is resolved ([#118](https://github.com/wxt-dev/wxt/pull/118))
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
- Aaron Klinker <aaronklinker1@gmail.com>
## v0.5.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.1...v0.5.2)
### 🩹 Fixes
- Import client utils when getting entrypoint config ([#117](https://github.com/wxt-dev/wxt/pull/117))
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
## v0.5.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.5.0...v0.5.1)
### 🚀 Enhancements
- Allow disabling auto-imports ([#114](https://github.com/wxt-dev/wxt/pull/114))
- Include/exclude entrypoints based on target browser ([#115](https://github.com/wxt-dev/wxt/pull/115))
### 🩹 Fixes
- Allow any string for target browser ([b4de93d](https://github.com/wxt-dev/wxt/commit/b4de93d))
### ❤️ Contributors
- Aaron <aaronklinker1@gmail.com>
- Aaron Klinker <aaronklinker1@gmail.com>
## v0.5.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.4.1...v0.5.0)
@@ -31,8 +103,7 @@
### ❤️ Contributors
- Aaron Klinker <aaronklinker1@gmail.com>
- Aaron <aaronklinker1@gmail.com>
- Aaron Klinker ([@aklinker1](https://github.com/aklinker1))
## v0.4.1
-3
View File
@@ -2,9 +2,6 @@ import { defineConfig } from 'wxt';
export default defineConfig({
srcDir: 'src',
storeIds: {
chrome: '123',
},
manifest: {
default_locale: 'en',
},
+11 -12
View File
@@ -23,11 +23,16 @@ const LEAF_PATHS = ['imports', 'vite', 'server'];
/**
* Override any types that resolve to `import(...)` instead of their type names when calling
* `type.getText()`
* `type.getText()`.
*
* This also stops any further type inspection for objects, meaning the object will be documented,
* not it's properties.
*/
const CUSTOM_TYPES = {
manifest:
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
imports: "false | Partial<import('unimport').UnimportOptions>",
vite: "Omit<import('vite').UserConfig, 'root' | 'configFile' | 'mode'>",
};
export function generateConfigDocs() {
@@ -61,18 +66,12 @@ export function generateConfigDocs() {
throw Error('Unsupported type node: ' + node.getKindName());
}
if (type.isObject() && !type.isArray()) {
return (
type
.getProperties()
// .sort((l, r) => l.getName().localeCompare(r.getName()))
.flatMap((property) => {
const childPath = [...path, property.getName()];
if (LEAF_PATHS.includes(childPath.join('.'))) return [];
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
return type.getProperties().flatMap((property) => {
const childPath = [...path, property.getName()];
return getDocsFor(childPath, property.getDeclarations()[0]);
})
);
return getDocsFor(childPath, property.getDeclarations()[0]);
});
}
if ('getJsDocs' in node) {
+5
View File
@@ -9,6 +9,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
<EntrypointPatterns
:patterns="[
['background.ts', 'background.js'],
['background/index.ts', 'background.js'],
]"
/>
@@ -32,6 +33,10 @@ export default defineBackground({
persistent: undefined | true | false,
type: undefined | 'module',
// Set include/exclude if the background should be removed from some builds
include: undefined | string[],
exclude: undefined | string[],
// Executed when background is loaded
main() {
// ...
+3 -2
View File
@@ -13,8 +13,6 @@
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -22,6 +20,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+4
View File
@@ -30,6 +30,10 @@ export default defineContentScript({
matchOriginAsFallback: undefined | true | false,
world: undefined | 'ISOLATED' | 'MAIN',
// Set include/exclude if the background should be removed from some builds
include: undefined | string[],
exclude: undefined | string[],
main() {
// Executed when content script is loaded
},
+3
View File
@@ -22,6 +22,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3 -2
View File
@@ -13,8 +13,6 @@
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -22,6 +20,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3 -2
View File
@@ -13,8 +13,6 @@
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -22,6 +20,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3
View File
@@ -23,6 +23,9 @@
<meta name="manifest.open_in_tab" content="true|false" />
<meta name="manifest.chrome_style" content="true|false" />
<meta name="manifest.browser_style" content="true|false" />
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3
View File
@@ -29,6 +29,9 @@
}"
/>
<meta name="manifest.type" content="page_action|browser_action" />
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3 -2
View File
@@ -19,8 +19,6 @@ Firefox does not support sandboxed pages.
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -28,6 +26,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3 -2
View File
@@ -19,8 +19,6 @@ Firefox does not support sidepanel pages.
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -28,6 +26,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+3 -2
View File
@@ -29,8 +29,6 @@ console.log(url); // "chrome-extension://<id>/<name>.html"
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
@@ -38,6 +36,9 @@ Plain old HTML file.
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
</head>
<body>
<!-- ... -->
+69 -42
View File
@@ -2,60 +2,87 @@ import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe('Auto Imports', () => {
it('should output types for entrypoint paths', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {})',
);
project.addFile(
'entrypoints/overlay.content.ts',
'export default defineContentScript(() => {})',
);
project.addFile('entrypoints/popup.html', '<html></html>');
describe('imports: { ... }', () => {
it('should generate a declaration file, imports.d.ts, for auto-imports', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.build();
expect(await project.serializeFile('.wxt/types/paths.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/paths.d.ts
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports.d.ts
----------------------------------------
// Generated by wxt
export {}
declare global {
const browser: typeof import('wxt/browser')['browser']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
}
"
`);
});
it('should include auto-imports in the project', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
expect(await project.serializeFile('.wxt/wxt.d.ts'))
.toMatchInlineSnapshot(`
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
import \\"wxt/browser\\";
declare module \\"wxt/browser\\" {
type PublicPath =
| \\"/background.js\\"
| \\"/content-scripts/overlay.js\\"
| \\"/popup.html\\"
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
}
}
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/imports.d.ts\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
"
`);
});
});
it('should make some client utils auto-importable', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
describe('imports: false', () => {
it('should not generate a imports.d.ts file', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
});
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
await project.build();
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports.d.ts
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
});
it('should not include imports.d.ts in the type references', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
});
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.build();
expect(
await project.serializeFile('.wxt/wxt.d.ts'),
).toMatchInlineSnapshot(
`
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
export {}
declare global {
const browser: typeof import('wxt/browser')['browser']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
}
/// <reference types=\\"vite/client\\" />
/// <reference types=\\"./types/paths.d.ts\\" />
/// <reference types=\\"./types/i18n.d.ts\\" />
/// <reference types=\\"./types/globals.d.ts\\" />
"
`);
`,
);
});
});
});
+40
View File
@@ -93,4 +93,44 @@ describe('Output Directory Structure', () => {
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
`);
});
it('should not include an entrypoint if the target browser is not in the list of included targets', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', '<html></html>');
project.addFile(
'entrypoints/background.ts',
`
export default defineBackground({
include: ["chrome"],
main() {},
})
`,
);
await project.build({ browser: 'firefox' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', '<html></html>');
project.addFile(
'entrypoints/background.ts',
`
export default defineBackground({
exclude: ["chrome"],
main() {},
})
`,
);
await project.build({ browser: 'chrome' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
});
+1 -23
View File
@@ -27,28 +27,6 @@ describe('TypeScript Project', () => {
`);
});
it('should define auto-import globals', async () => {
const project = new TestProject();
await project.build();
const output = await project.serializeFile('.wxt/types/imports.d.ts');
expect(output).toMatchInlineSnapshot(`
".wxt/types/imports.d.ts
----------------------------------------
// Generated by wxt
export {}
declare global {
const browser: typeof import('wxt/browser')['browser']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
}
"
`);
});
it('should augment the types for browser.runtime.getURL', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html');
@@ -295,7 +273,7 @@ describe('TypeScript Project', () => {
`);
});
it('should correct path aliases for a custom srcDir', async () => {
it('should generate correct path aliases for a custom srcDir', async () => {
const project = new TestProject();
project.setConfigFileConfig({
srcDir: 'src',
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.5.0",
"version": "0.5.5",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18.16.0",
+1 -2
View File
@@ -14,10 +14,9 @@ export const build = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'production';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
+1 -2
View File
@@ -14,10 +14,9 @@ export const dev = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'development';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
+1 -2
View File
@@ -17,10 +17,9 @@ export const zip = defineCommand<
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'production';
const cliConfig: wxt.InlineConfig = {
root,
mode,
mode: flags.mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
@@ -9,13 +9,13 @@ import {
import { resolve } from 'path';
import { findEntrypoints } from '../findEntrypoints';
import fs from 'fs-extra';
import { importTsFile } from '../../utils/importTsFile';
import { importEntrypointFile } from '../../utils/importEntrypointFile';
import glob from 'fast-glob';
import { fakeInternalConfig } from '../../../testing/fake-objects';
import { unnormalizePath } from '../../utils/paths';
vi.mock('../../utils/importTsFile');
const importTsFileMock = vi.mocked(importTsFile);
vi.mock('../../utils/importEntrypointFile');
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
vi.mock('fast-glob');
const globMock = vi.mocked(glob);
@@ -191,13 +191,16 @@ describe('findEntrypoints', () => {
matches: ['<all_urls>'],
};
globMock.mockResolvedValueOnce([path]);
importTsFileMock.mockResolvedValue(options);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
},
);
@@ -211,6 +214,15 @@ describe('findEntrypoints', () => {
outputDir: config.outDir,
},
],
[
'background/index.ts',
{
type: 'background',
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
outputDir: config.outDir,
},
],
])(
'should find and load background entrypoint config from %s',
async (path, expected) => {
@@ -218,13 +230,16 @@ describe('findEntrypoints', () => {
type: 'module',
};
globMock.mockResolvedValueOnce([path]);
importTsFileMock.mockResolvedValue(options);
importEntrypointFileMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
expect(importEntrypointFileMock).toBeCalledWith(
expected.inputPath,
config,
);
},
);
@@ -255,6 +270,7 @@ describe('findEntrypoints', () => {
name: 'sandbox',
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -264,6 +280,7 @@ describe('findEntrypoints', () => {
name: 'sandbox',
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -273,6 +290,7 @@ describe('findEntrypoints', () => {
name: 'named',
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -282,6 +300,7 @@ describe('findEntrypoints', () => {
name: 'named',
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -293,6 +312,7 @@ describe('findEntrypoints', () => {
name: 'bookmarks',
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -302,6 +322,7 @@ describe('findEntrypoints', () => {
name: 'bookmarks',
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -313,6 +334,7 @@ describe('findEntrypoints', () => {
name: 'history',
inputPath: resolve(config.entrypointsDir, 'history.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -322,6 +344,7 @@ describe('findEntrypoints', () => {
name: 'history',
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -333,6 +356,7 @@ describe('findEntrypoints', () => {
name: 'newtab',
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -342,6 +366,7 @@ describe('findEntrypoints', () => {
name: 'newtab',
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -353,6 +378,7 @@ describe('findEntrypoints', () => {
name: 'sidepanel',
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -362,6 +388,7 @@ describe('findEntrypoints', () => {
name: 'sidepanel',
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -371,6 +398,7 @@ describe('findEntrypoints', () => {
name: 'named',
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -380,6 +408,7 @@ describe('findEntrypoints', () => {
name: 'named',
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -391,6 +420,7 @@ describe('findEntrypoints', () => {
name: 'devtools',
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -400,6 +430,7 @@ describe('findEntrypoints', () => {
name: 'devtools',
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -411,6 +442,7 @@ describe('findEntrypoints', () => {
name: 'onboarding',
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -420,6 +452,7 @@ describe('findEntrypoints', () => {
name: 'onboarding',
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
outputDir: config.outDir,
options: {},
},
],
@@ -431,6 +464,7 @@ describe('findEntrypoints', () => {
name: 'injected',
inputPath: resolve(config.entrypointsDir, 'injected.ts'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -440,6 +474,7 @@ describe('findEntrypoints', () => {
name: 'injected',
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
outputDir: config.outDir,
options: {},
},
],
@@ -451,6 +486,7 @@ describe('findEntrypoints', () => {
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
outputDir: config.outDir,
options: {},
},
],
[
@@ -460,6 +496,7 @@ describe('findEntrypoints', () => {
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
outputDir: config.outDir,
options: {},
},
],
@@ -471,6 +508,7 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
},
],
[
@@ -480,6 +518,7 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
},
],
[
@@ -489,6 +528,7 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
},
],
[
@@ -498,6 +538,7 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
},
],
])('should find entrypoint for %s', async (path, expected) => {
@@ -521,4 +562,148 @@ describe('findEntrypoints', () => {
expectedPaths.join(', '),
);
});
describe('include option', () => {
it("should filter out the background when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it("should filter out content scripts when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({
include: ['not' + config.browser],
});
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it("should filter out the popup when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['popup.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.include" content="['${
'not' + config.browser
}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it("should filter out the options page when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['options.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.include" content="['${
'not' + config.browser
}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it("should filter out an unlisted page when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['unlisted.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.include" content="['${
'not' + config.browser
}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
});
describe('exclude option', () => {
it('should filter out the background when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({
exclude: [config.browser],
});
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it('should filter out content scripts when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({
exclude: [config.browser],
});
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it('should filter out the popup when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['popup.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.exclude" content="['${config.browser}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it('should filter out the options page when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['options.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.exclude" content="['${config.browser}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
it('should filter out an unlisted page when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['unlisted.html']);
readFileMock.mockResolvedValueOnce(
`<html>
<head>
<meta name="manifest.exclude" content="['${config.browser}']" />
</head>
</html>`,
);
const entrypoints = await findEntrypoints(config);
expect(entrypoints).toEqual([]);
});
});
});
+4
View File
@@ -78,6 +78,10 @@ async function buildSingleEntrypoint(
},
},
},
define: {
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
'process.env.NODE_ENV': JSON.stringify(config.mode),
},
};
const entryConfig = vite.mergeConfig(
libMode,
+86 -16
View File
@@ -2,9 +2,11 @@ import { relative, resolve } from 'path';
import {
BackgroundEntrypoint,
BackgroundScriptDefintition,
BaseEntrypointOptions,
ContentScriptDefinition,
ContentScriptEntrypoint,
Entrypoint,
GenericEntrypoint,
InternalConfig,
OptionsEntrypoint,
PopupEntrypoint,
@@ -13,7 +15,7 @@ import fs from 'fs-extra';
import { minimatch } from 'minimatch';
import { parseHTML } from 'linkedom';
import JSON5 from 'json5';
import { importTsFile } from '../utils/importTsFile';
import { importEntrypointFile } from '../utils/importEntrypointFile';
import glob from 'fast-glob';
import { getEntrypointName } from '../utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
@@ -75,12 +77,19 @@ export async function findEntrypoints(
path,
);
break;
case 'unlisted-page':
entrypoint = await getUnlistedPageEntrypoint(config, path);
break;
case 'content-script-style':
entrypoint = {
type,
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options: {
include: undefined,
exclude: undefined,
},
};
break;
default:
@@ -89,6 +98,10 @@ export async function findEntrypoints(
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: config.outDir,
options: {
include: undefined,
exclude: undefined,
},
};
}
@@ -112,7 +125,47 @@ export async function findEntrypoints(
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
);
}
return entrypoints;
config.logger.debug('All entrypoints:', entrypoints);
const targetEntrypoints = entrypoints.filter((entry) => {
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
config.logger.warn(
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
);
return false;
}
if (exclude?.length && !include?.length) {
return !exclude.includes(config.browser);
}
if (include?.length && !exclude?.length) {
return include.includes(config.browser);
}
return true;
});
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
return targetEntrypoints;
}
function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
const options: BaseEntrypointOptions = {};
const includeContent = document
.querySelector("meta[name='manifest.include']")
?.getAttribute('content');
if (includeContent) {
options.include = JSON5.parse(includeContent);
}
const excludeContent = document
.querySelector("meta[name='manifest.exclude']")
?.getAttribute('content');
if (excludeContent) {
options.exclude = JSON5.parse(excludeContent);
}
return options;
}
/**
@@ -123,11 +176,11 @@ async function getPopupEntrypoint(
config: InternalConfig,
path: string,
): Promise<PopupEntrypoint> {
const options: PopupEntrypoint['options'] = {};
const content = await fs.readFile(path, 'utf-8');
const { document } = parseHTML(content);
const options: PopupEntrypoint['options'] = getHtmlBaseOptions(document);
const title = document.querySelector('title');
if (title != null) options.defaultTitle = title.textContent ?? undefined;
@@ -170,11 +223,11 @@ async function getOptionsEntrypoint(
config: InternalConfig,
path: string,
): Promise<OptionsEntrypoint> {
const options: OptionsEntrypoint['options'] = {};
const content = await fs.readFile(path, 'utf-8');
const { document } = parseHTML(content);
const options: OptionsEntrypoint['options'] = getHtmlBaseOptions(document);
const openInTabContent = document
.querySelector("meta[name='manifest.open_in_tab']")
?.getAttribute('content');
@@ -205,6 +258,26 @@ async function getOptionsEntrypoint(
};
}
/**
* @param path Absolute path to the HTML file.
* @param content String contents of the file at the path.
*/
async function getUnlistedPageEntrypoint(
config: InternalConfig,
path: string,
): Promise<GenericEntrypoint> {
const content = await fs.readFile(path, 'utf-8');
const { document } = parseHTML(content);
return {
type: 'unlisted-page',
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: config.outDir,
options: getHtmlBaseOptions(document),
};
}
/**
* @param path Absolute path to the background's TS file.
*/
@@ -214,10 +287,8 @@ async function getBackgroundEntrypoint(
): Promise<BackgroundEntrypoint> {
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
path,
config,
);
const defaultExport =
await importEntrypointFile<BackgroundScriptDefintition>(path, config);
if (defaultExport == null) {
throw Error('Background script does not have a default export');
}
@@ -241,10 +312,8 @@ async function getContentScriptEntrypoint(
name: string,
path: string,
): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } = await importTsFile<ContentScriptDefinition>(
path,
config,
);
const { main: _, ...options } =
await importEntrypointFile<ContentScriptDefinition>(path, config);
if (options == null) {
throw Error(`Content script ${name} does not have a default export`);
}
@@ -281,6 +350,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'devtools/index.html': 'devtools',
'background.ts': 'background',
'background/index.ts': 'background',
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
'content.ts?(x)': 'content-script',
@@ -305,8 +375,8 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
'*/*': 'ignored',
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files or tests
'*/**': 'ignored',
};
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
+9 -3
View File
@@ -1,4 +1,4 @@
import { createUnimport } from 'unimport';
import { UnimportOptions, createUnimport } from 'unimport';
import { Entrypoint, InternalConfig } from '../types';
import fs from 'fs-extra';
import { relative, resolve } from 'path';
@@ -21,7 +21,12 @@ export async function generateTypesDir(
await fs.ensureDir(config.typesDir);
const references: string[] = [];
references.push(await writeImportsDeclarationFile(config));
const imports = getUnimportOptions(config);
if (imports !== false) {
references.push(await writeImportsDeclarationFile(config, imports));
}
references.push(await writePathsDeclarationFile(entrypoints, config));
references.push(await writeI18nDeclarationFile(config));
references.push(await writeGlobalsDeclarationFile(config));
@@ -32,9 +37,10 @@ export async function generateTypesDir(
async function writeImportsDeclarationFile(
config: InternalConfig,
unimportOptions: Partial<UnimportOptions>,
): Promise<string> {
const filePath = resolve(config.typesDir, 'imports.d.ts');
const unimport = createUnimport(getUnimportOptions(config));
const unimport = createUnimport(unimportOptions);
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
+58 -48
View File
@@ -32,7 +32,7 @@ export interface InlineConfig {
/**
* > Only available when using the JS API. Not available in `wxt.config.ts` files
*
* Path to `"wxt.config.ts"` file or false to disable config file discovery.
* Path to `wxt.config.ts` file or `false` to disable config file discovery.
*
* @default "wxt.config.ts"
*/
@@ -43,23 +43,25 @@ export interface InlineConfig {
* @default false
*/
debug?: boolean;
/**
* ID of the extension for each store. Used for publishing.
*/
storeIds?: {
chrome?: string;
firefox?: string;
edge?: string;
};
/**
* Explicitly set a mode to run in. This will override the default mode for each command, and can
* be overridden by the command line `--mode` option.
*/
mode?: string;
/**
* Customize auto-import options.
* Customize auto-import options. Set to `false` to disable auto-imports.
*
* For example, to add a directory to auto-import from, you can use:
*
* ```ts
* export default defineConfig({
* imports: {
* dirs: ["some-directory"]
* }
* })
* ```
*/
imports?: Partial<UnimportOptions>;
imports?: Partial<UnimportOptions> | false;
/**
* Explicitly set a browser to build for. This will override the default browser for each command,
* and can be overridden by the command line `--browser` option.
@@ -81,18 +83,18 @@ export interface InlineConfig {
*/
logger?: Logger;
/**
* Custom Vite options.
* Custom Vite options, see <https://vitejs.dev/config/shared-options.html>.
*
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config.
*/
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
vite?:
| WxtViteConfig
| ((env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>);
/**
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
* object or promise.
*/
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
/**
* Custom server options.
*/
server?: WxtDevServer;
/**
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
*/
@@ -214,7 +216,7 @@ export interface WxtDevServer extends vite.ViteDevServer {
) => void;
}
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
export type TargetBrowser = string;
export type TargetManifestVersion = 2 | 3;
export type UserConfig = Omit<InlineConfig, 'configFile'>;
@@ -230,6 +232,11 @@ export interface Logger {
level: LogLevel;
}
export interface BaseEntrypointOptions {
include?: TargetBrowser[];
exclude?: TargetBrowser[];
}
export interface BaseEntrypoint {
/**
* The entrypoint's name. This is the filename or dirname without the type suffix.
@@ -257,6 +264,7 @@ export interface BaseEntrypoint {
* subdirectory of it.
*/
outputDir: string;
options: BaseEntrypointOptions;
}
export interface GenericEntrypoint extends BaseEntrypoint {
@@ -278,12 +286,12 @@ export interface BackgroundEntrypoint extends BaseEntrypoint {
options: {
persistent?: boolean;
type?: 'module';
};
} & BaseEntrypointOptions;
}
export interface ContentScriptEntrypoint extends BaseEntrypoint {
type: 'content-script';
options: Omit<ContentScriptDefinition, 'main'>;
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
}
export interface PopupEntrypoint extends BaseEntrypoint {
@@ -295,7 +303,7 @@ export interface PopupEntrypoint extends BaseEntrypoint {
mv2Key?: 'browser_action' | 'page_action';
defaultIcon?: Record<string, string>;
defaultTitle?: string;
};
} & BaseEntrypointOptions;
}
export interface OptionsEntrypoint extends BaseEntrypoint {
@@ -304,7 +312,7 @@ export interface OptionsEntrypoint extends BaseEntrypoint {
openInTab?: boolean;
browserStyle?: boolean;
chromeStyle?: boolean;
};
} & BaseEntrypointOptions;
}
export type Entrypoint =
@@ -316,7 +324,7 @@ export type Entrypoint =
export type OnContentScriptStopped = (cb: () => void) => void;
export interface ContentScriptDefinition {
export interface ContentScriptDefinition extends ExcludableEntrypoint {
matches: Manifest.ContentScript['matches'];
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
@@ -364,11 +372,28 @@ export interface ContentScriptDefinition {
main(): void | Promise<void>;
}
export interface BackgroundScriptDefintition {
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
type?: 'module';
main(): void;
}
export interface ExcludableEntrypoint {
/**
* List of target browsers to include this entrypoint in. Defaults to being included in all
* builds. Cannot be used with `exclude`. You must choose one of the two options.
*
* @default undefined
*/
include?: TargetBrowser[];
/**
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
* must choose one of the two options.
*
* @default undefined
*/
exclude?: TargetBrowser[];
}
/**
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
* here, they are configured inline.
@@ -423,31 +448,11 @@ export interface ExtensionRunnerConfig {
openDevtools?: boolean;
/**
* List of browser names and the binary that should be used to open the browser.
*
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
*/
binaries?: {
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
*/
chrome?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
*/
edge?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
*/
opera?: string;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
*/
firefox?:
| 'firefox'
| 'beta'
| 'nightly'
| 'deved'
| 'firefoxdeveloperedition'
| string;
};
binaries?: Record<string, string>;
/**
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile
*/
@@ -473,3 +478,8 @@ export interface ExtensionRunnerConfig {
*/
startUrls?: string[];
}
export type WxtViteConfig = Omit<
vite.UserConfig,
'root' | 'configFile' | 'mode'
>;
+1 -6
View File
@@ -21,17 +21,12 @@ export interface InternalConfig {
outBaseDir: string;
outDir: string;
debug: boolean;
storeIds: {
chrome?: string;
firefox?: string;
edge?: string;
};
mode: string;
command: 'build' | 'serve';
browser: TargetBrowser;
manifestVersion: TargetManifestVersion;
logger: Logger;
imports: Partial<UnimportOptions>;
imports: false | Partial<UnimportOptions>;
vite: vite.InlineConfig;
manifest: UserManifest;
fsCache: FsCache;
@@ -42,6 +42,7 @@ describe('Entrypoint Utils', () => {
inputPath: '...',
name,
outputDir,
options: {},
};
const actual = getEntrypointOutputFile(entrypoint, ext);
@@ -23,6 +23,7 @@ const unlistedScript: Entrypoint = {
name: 'injected',
inputPath: '/injected.ts',
outputDir: '/.output/injected',
options: {},
};
const popup: Entrypoint = {
type: 'popup',
@@ -36,6 +37,7 @@ const unlistedPage: Entrypoint = {
name: 'onboarding',
inputPath: '/onboarding.html',
outputDir: '/.output/onboarding',
options: {},
};
const options: Entrypoint = {
type: 'options',
@@ -49,24 +51,28 @@ const sandbox1: Entrypoint = {
name: 'sandbox',
inputPath: '/sandbox1.html',
outputDir: '/.output/sandbox1',
options: {},
};
const sandbox2: Entrypoint = {
type: 'sandbox',
name: 'sandbox2',
inputPath: '/sandbox2.html',
outputDir: '/.output/sandbox2',
options: {},
};
const unlistedStyle: Entrypoint = {
type: 'unlisted-style',
name: 'injected',
inputPath: '/injected.scss',
outputDir: '/.output',
options: {},
};
const contentScriptStyle: Entrypoint = {
type: 'content-script-style',
name: 'injected',
inputPath: '/overlay.content.scss',
outputDir: '/.output/content-scripts',
options: {},
};
describe('groupEntrypoints', () => {
+3 -1
View File
@@ -4,7 +4,9 @@ import { mergeConfig } from 'vite';
export function getUnimportOptions(
config: InternalConfig,
): Partial<UnimportOptions> {
): Partial<UnimportOptions | false> {
if (config.imports === false) return false;
const defaultOptions: Partial<UnimportOptions> = {
debugLog: config.logger.debug,
imports: [{ name: 'defineConfig', from: 'wxt' }],
+198 -134
View File
@@ -1,176 +1,115 @@
import { loadConfig } from 'c12';
import {
ConfigEnv,
ExtensionRunnerConfig,
InlineConfig,
InternalConfig,
UserConfig,
UserManifest,
ConfigEnv,
UserManifestFn,
UserManifest,
WxtViteConfig,
ExtensionRunnerConfig,
} from '../types';
import path, { resolve } from 'node:path';
import path from 'node:path';
import * as vite from 'vite';
import { LogLevels, consola } from 'consola';
import * as plugins from '../vite-plugins';
import { createFsCache } from './createFsCache';
import consola, { LogLevels } from 'consola';
import * as plugins from '../vite-plugins';
import { getGlobals } from './globals';
import { loadConfig } from 'c12';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
* relative paths, and apply any defaults.
*
* Inline config always has priority over user config. Cli flags are passed as inline config if set.
* If unset, undefined is passed in, letting this function decide default values.
*/
export async function getInternalConfig(
config: InlineConfig,
inlineConfig: InlineConfig,
command: 'build' | 'serve',
): Promise<InternalConfig> {
// Apply defaults to a base config
const root = config.root ? path.resolve(config.root) : process.cwd();
const mode =
config.mode ?? (command === 'build' ? 'production' : 'development');
const browser = config.browser ?? 'chrome';
const manifestVersion =
config.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
const outBaseDir = path.resolve(root, '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
const logger = config.logger ?? consola;
const debug = !!config.debug;
if (debug) logger.level = LogLevels.debug;
// Load user config
const baseConfig: InternalConfigNoUserDirs = {
root,
outDir,
outBaseDir,
storeIds: config.storeIds ?? {},
browser,
manifestVersion,
mode,
command,
debug,
logger,
vite: config.vite ?? {},
imports: config.imports ?? {},
runnerConfig: await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: config.runner,
}),
};
// Load user config from file
let userConfig: UserConfig = {
mode,
};
if (config.configFile !== false) {
let userConfig: UserConfig = {};
if (inlineConfig.configFile !== false) {
const loaded = await loadConfig<UserConfig>({
name: 'wxt',
cwd: root,
cwd: inlineConfig.root ?? process.cwd(),
rcFile: false,
});
userConfig = loaded.config ?? {};
}
// Merge inline and user configs
const merged = vite.mergeConfig(
baseConfig,
userConfig,
) as InternalConfigNoUserDirs;
// Merge it into the inline config
// Apply user config and create final config
const srcDir = userConfig.srcDir ? resolve(root, userConfig.srcDir) : root;
const entrypointsDir = resolve(
srcDir,
userConfig.entrypointsDir ?? 'entrypoints',
const mergedConfig = mergeInlineConfig(inlineConfig, userConfig);
// Apply defaults to make internal config.
const debug = mergedConfig.debug ?? false;
const logger = mergedConfig.logger ?? consola;
if (debug) logger.level = LogLevels.debug;
const browser = mergedConfig.browser ?? 'chrome';
const manifestVersion =
mergedConfig.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
const mode =
mergedConfig.mode ?? (command === 'build' ? 'production' : 'development');
const env: ConfigEnv = { browser, command, manifestVersion, mode };
const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
const publicDir = resolve(srcDir, userConfig.publicDir ?? 'public');
const wxtDir = resolve(root, '.wxt');
const typesDir = resolve(wxtDir, 'types');
const wxtDir = path.resolve(root, '.wxt');
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
const entrypointsDir = path.resolve(
srcDir,
mergedConfig.entrypointsDir ?? 'entrypoints',
);
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
// Merge manifest sources
const env: ConfigEnv = { mode, browser, manifestVersion, command };
const userManifest = await resolveManifestConfig(env, userConfig.manifest);
const inlineManifest = await resolveManifestConfig(env, config.manifest);
const manifest = vite.mergeConfig(userManifest, inlineManifest);
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: mergedConfig.runner,
});
const finalConfig: InternalConfig = {
...merged,
srcDir,
browser,
command,
debug,
entrypointsDir,
publicDir,
wxtDir: wxtDir,
typesDir,
fsCache: createFsCache(wxtDir),
manifest,
zip: {
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
...userConfig.zip,
...config.zip,
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
// Hidden files
'**/.*',
// Tests
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// User config
...(userConfig.zip?.ignoredSources ?? []),
...(config.zip?.ignoredSources ?? []),
],
},
imports: mergedConfig.imports ?? {},
logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion,
mode,
outBaseDir,
outDir,
publicDir,
root,
runnerConfig,
srcDir,
typesDir,
vite: {}, // Real value added after this object is initialized.
wxtDir,
zip: resolveInternalZipConfig(root, mergedConfig),
};
// Customize the default vite config
finalConfig.vite.root = root;
finalConfig.vite.configFile = false;
finalConfig.vite.logLevel = 'warn';
finalConfig.vite.build ??= {};
finalConfig.vite.build.outDir = outDir;
finalConfig.vite.build.emptyOutDir = false;
finalConfig.vite.plugins ??= [];
finalConfig.vite.plugins.push(plugins.download(finalConfig));
finalConfig.vite.plugins.push(plugins.devHtmlPrerender(finalConfig));
finalConfig.vite.plugins.push(plugins.unimport(finalConfig));
finalConfig.vite.plugins.push(
plugins.virtualEntrypoin('background', finalConfig),
finalConfig.vite = await resolveInternalViteConfig(
env,
mergedConfig,
finalConfig,
);
finalConfig.vite.plugins.push(
plugins.virtualEntrypoin('content-script', finalConfig),
);
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
finalConfig.vite.plugins.push(plugins.noopBackground());
finalConfig.vite.define ??= {};
getGlobals(finalConfig).forEach((global) => {
finalConfig.vite.define![global.name] = JSON.stringify(global.value);
});
return finalConfig;
}
/**
* Helper type for defining a base config, since user-configurable directories must be set after
* reading in the user config.
*/
type InternalConfigNoUserDirs = Omit<
InternalConfig,
| 'srcDir'
| 'publicDir'
| 'entrypointsDir'
| 'wxtDir'
| 'typesDir'
| 'fsCache'
| 'manifest'
| 'zip'
>;
async function resolveManifestConfig(
env: ConfigEnv,
manifest: UserManifest | Promise<UserManifest> | UserManifestFn | undefined,
@@ -179,3 +118,128 @@ async function resolveManifestConfig(
? manifest(env)
: manifest ?? {});
}
/**
* Merge the inline config and user config. Inline config is given priority. Defaults are not applied here.
*/
function mergeInlineConfig(
inlineConfig: InlineConfig,
userConfig: UserConfig,
): InlineConfig {
let imports: InlineConfig['imports'];
if (inlineConfig.imports === false || userConfig.imports === false) {
imports = false;
} else if (userConfig.imports == null && inlineConfig.imports == null) {
imports = undefined;
} else {
imports = vite.mergeConfig(
userConfig.imports ?? {},
inlineConfig.imports ?? {},
);
}
const manifest: UserManifestFn = async (env) => {
const user = await resolveManifestConfig(env, userConfig.manifest);
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
return vite.mergeConfig(user, inline);
};
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
const user = await resolveViteConfig(env, userConfig.vite);
const inline = await resolveViteConfig(env, inlineConfig.vite);
return vite.mergeConfig(user, inline);
};
const runner: InlineConfig['runner'] = vite.mergeConfig(
userConfig.runner ?? {},
inlineConfig.runner ?? {},
);
const zip: InlineConfig['zip'] = vite.mergeConfig(
userConfig.zip ?? {},
inlineConfig.zip ?? {},
);
return {
root: inlineConfig.root ?? userConfig.root,
browser: inlineConfig.browser ?? userConfig.browser,
manifestVersion: inlineConfig.manifestVersion ?? userConfig.manifestVersion,
configFile: inlineConfig.configFile,
debug: inlineConfig.debug ?? userConfig.debug,
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
imports,
logger: inlineConfig.logger ?? userConfig.logger,
manifest,
mode: inlineConfig.mode ?? userConfig.mode,
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
runner,
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
vite: viteConfig,
zip,
};
}
async function resolveViteConfig(
env: ConfigEnv,
vite: InlineConfig['vite'],
): Promise<WxtViteConfig> {
return await (typeof vite === 'function' ? vite(env) : vite ?? {});
}
function resolveInternalZipConfig(
root: string,
mergedConfig: InlineConfig,
): InternalConfig['zip'] {
return {
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
...mergedConfig.zip,
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
// Hidden files
'**/.*',
// Tests
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// From user
...(mergedConfig.zip?.ignoredSources ?? []),
],
};
}
async function resolveInternalViteConfig(
env: ConfigEnv,
mergedConfig: InlineConfig,
finalConfig: InternalConfig,
) {
const internalVite: vite.InlineConfig = await resolveViteConfig(
env,
mergedConfig.vite,
);
internalVite.root = finalConfig.root;
internalVite.configFile = false;
internalVite.logLevel = 'warn';
internalVite.build ??= {};
internalVite.build.outDir = finalConfig.outDir;
internalVite.build.emptyOutDir = false;
internalVite.plugins ??= [];
internalVite.plugins.push(plugins.download(finalConfig));
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
internalVite.plugins.push(plugins.unimport(finalConfig));
internalVite.plugins.push(
plugins.virtualEntrypoin('background', finalConfig),
);
internalVite.plugins.push(
plugins.virtualEntrypoin('content-script', finalConfig),
);
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
internalVite.plugins.push(plugins.noopBackground());
internalVite.define ??= {};
for (const global of getGlobals(finalConfig)) {
internalVite.define[global.name] = JSON.stringify(global.value);
}
return internalVite;
}
@@ -5,7 +5,7 @@ import fs from 'fs-extra';
import { resolve } from 'path';
import transform from 'jiti/dist/babel';
import { getUnimportOptions } from './auto-imports';
import { removeImportStatements } from './strings';
import { removeProjectImportStatements } from './strings';
import { normalizePath } from './paths';
/**
@@ -23,7 +23,7 @@ import { normalizePath } from './paths';
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
*/
export async function importTsFile<T>(
export async function importEntrypointFile<T>(
path: string,
config: InternalConfig,
): Promise<T> {
@@ -39,7 +39,7 @@ export async function importTsFile<T>(
await unimport.init();
const text = await fs.readFile(path, 'utf-8');
const textNoImports = removeImportStatements(text);
const textNoImports = removeProjectImportStatements(text);
const { code } = await unimport.injectImports(textNoImports);
config.logger.debug(
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
+11
View File
@@ -15,3 +15,14 @@ export function removeImportStatements(text: string): string {
'',
);
}
/**
* Removes imports, ensuring that some of WXT's client imports are present, so that entrypoints can be parsed if auto-imports are disabled.
*/
export function removeProjectImportStatements(text: string): string {
const noImports = removeImportStatements(text);
return `import { defineContentScript, defineBackground } from 'wxt/client';
${noImports}`;
}
+4 -2
View File
@@ -1,7 +1,7 @@
import { createUnimport } from 'unimport';
import { InternalConfig } from '../types';
import { getUnimportOptions } from '../utils/auto-imports';
import { Plugin } from 'vite';
import * as vite from 'vite';
import { extname } from 'path';
const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
@@ -16,8 +16,10 @@ const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
/**
* Inject any global imports defined by unimport
*/
export function unimport(config: InternalConfig): Plugin {
export function unimport(config: InternalConfig): vite.PluginOption {
const options = getUnimportOptions(config);
if (options === false) return [];
const unimport = createUnimport(options);
return {
+7 -6
View File
@@ -1,7 +1,6 @@
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
import { getInternalConfig } from './core/utils/getInternalConfig';
import pc from 'picocolors';
import * as vite from 'vite';
import { detectDevChanges } from './core/utils/detectDevChanges';
import { Mutex } from 'async-mutex';
import { consola } from 'consola';
@@ -38,12 +37,14 @@ export async function createServer(
): Promise<WxtDevServer> {
const serverInfo = await getServerInfo();
const getLatestInternalConfig = () => {
const viteConfig: vite.InlineConfig = vite.mergeConfig(
serverInfo.viteServerConfig,
config?.vite ?? {},
const getLatestInternalConfig = async () => {
return getInternalConfig(
{
...config,
vite: serverInfo.viteServerConfig,
},
'serve',
);
return getInternalConfig({ ...config, vite: viteConfig }, 'serve');
};
let internalConfig = await getLatestInternalConfig();
+2 -1
View File
@@ -77,6 +77,7 @@ export const fakeUnlistedScriptEntrypoint =
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
}));
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
@@ -133,6 +134,7 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
}),
);
@@ -215,7 +217,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
},
debug: faker.datatype.boolean(),
srcDir: fakeDir(),
storeIds: {},
typesDir: fakeDir(),
vite: {},
wxtDir: fakeDir(),
+1 -1
View File
@@ -23,6 +23,6 @@
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.3",
"typescript": "^5.1.6",
"wxt": "^0.4.0"
"wxt": "^0.5.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"devDependencies": {
"typescript": "^5.1.6",
"vite-plugin-solid": "^2.7.0",
"wxt": "^0.4.0"
"wxt": "^0.5.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"svelte-check": "^3.4.6",
"tslib": "^2.6.0",
"typescript": "^5.1.6",
"wxt": "^0.4.0"
"wxt": "^0.5.0"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.1.6",
"wxt": "^0.4.0"
"wxt": "^0.5.0"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"@vitejs/plugin-vue": "^4.2.3",
"typescript": "^5.1.6",
"vue-tsc": "^1.8.4",
"wxt": "^0.4.0"
"wxt": "^0.5.0"
}
}