Compare commits

..

7 Commits

Author SHA1 Message Date
GitHub Actions 3c723d2c92 chore(release): v0.7.0 2023-09-30 17:45:03 +00:00
Aaron Klinker 1eb35c7207 Reduce codecov requirement to 80% 2023-09-30 12:38:14 -05:00
Aaron 725ecf7c64 fix: Reorder typescript paths to give priority to @ and ~ over @@ and ~~ (#142) 2023-09-30 12:33:33 -05:00
Aaron 3847092df4 feat: Content script cssInjectionMode (#141) 2023-09-30 12:32:00 -05:00
Aaron 19b11c236f fix!: Output content script CSS to content-scripts/<name>.css (#140)
BREAKING CHANGE: Content script CSS used to be output to `assets/<name>.css`, but is now `content-scripts/<name>.css` to match the docs.
2023-09-30 12:20:41 -05:00
Aaron Klinker 4b2012c489 fix: Validate transformed manifest correctly 2023-09-30 10:32:57 -05:00
Aaron Klinker 0591050f31 chore: Store user config metadata in memory
This will be used in #16
2023-09-30 10:30:25 -05:00
15 changed files with 258 additions and 115 deletions
+6
View File
@@ -0,0 +1,6 @@
coverage:
status:
project:
default:
target: 80%
threshold: 1%
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## v0.7.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)
### 🚀 Enhancements
- Content script `cssInjectionMode` ([#141](https://github.com/wxt-dev/wxt/pull/141))
### 🩹 Fixes
- Validate transformed manifest correctly ([4b2012c](https://github.com/wxt-dev/wxt/commit/4b2012c))
- ⚠️ Output content script CSS to `content-scripts/<name>.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))
- Reorder typescript paths to give priority to `@` and `~` over `@@` and `~~` ([#142](https://github.com/wxt-dev/wxt/pull/142))
### 🏡 Chore
- Store user config metadata in memory ([0591050](https://github.com/wxt-dev/wxt/commit/0591050))
#### ⚠️ Breaking Changes
- ⚠️ Output content script CSS to `content-scripts/<name>.css` ([#140](https://github.com/wxt-dev/wxt/pull/140))
## v0.6.6
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.5...v0.6.6)
+3
View File
@@ -34,6 +34,9 @@ export default defineContentScript({
include: undefined | string[],
exclude: undefined | string[],
// Configure how CSS is injected onto the page
cssInjectionMode: undefined | "manifest" | "manual",
main(ctx) {
// Executed when content script is loaded
},
+162 -77
View File
@@ -193,117 +193,202 @@ describe('Manifest Content', () => {
});
});
it('should group content scripts and styles together based on their matches and run_at', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`import "./style.css";
describe('content_scripts', () => {
it('should group content scripts and styles together based on their matches and run_at', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile('entrypoints/one.content/style.css', `body { color: red }`);
project.addFile(
'entrypoints/two.content/index.ts',
`import "./style.css";
);
project.addFile(
'entrypoints/one.content/style.css',
`body { color: red }`,
);
project.addFile(
'entrypoints/two.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body { color: green }`,
);
project.addFile(
'entrypoints/three.content/index.ts',
`import "./style.css";
);
project.addFile(
'entrypoints/two.content/style.css',
`body { color: green }`,
);
project.addFile(
'entrypoints/three.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/three.content/style.css',
`body { color: blue }`,
);
project.addFile(
'entrypoints/four.content/index.ts',
`import "./style.css";
);
project.addFile(
'entrypoints/three.content/style.css',
`body { color: blue }`,
);
project.addFile(
'entrypoints/four.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://duckduckgo.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/four.content/style.css',
`body { color: yellow }`,
);
);
project.addFile(
'entrypoints/four.content/style.css',
`body { color: yellow }`,
);
await project.build();
await project.build();
const manifest = await project.getOutputManifest();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['assets/four.css'],
js: ['content-scripts/four.js'],
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/three.css', 'content-scripts/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['assets/three.css', 'assets/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['assets/one.css'],
js: ['content-scripts/one.js'],
});
});
it('should add to any content scripts declared in wxt.config.ts', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`export default defineContentScript({
it('should add to any content scripts declared in wxt.config.ts', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body {
);
project.addFile(
'entrypoints/two.content/style.css',
`body {
background-color: red;
}`,
);
project.setConfigFileConfig({
manifest: {
content_scripts: [
{
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
},
],
},
);
project.setConfigFileConfig({
manifest: {
content_scripts: [
{
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
},
],
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
});
});
await project.build();
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
const manifest = await project.getOutputManifest();
main() {},
});`,
);
await project.build();
expect(manifest.content_scripts).toContainEqual({
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manifest",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manual",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
`);
});
});
+7 -7
View File
@@ -49,19 +49,19 @@ describe('Output Directory Structure', () => {
'.output/chrome-mv3/content-scripts/two.js',
]),
).toMatchInlineSnapshot(`
".output/chrome-mv3/assets/one.css
".output/chrome-mv3/content-scripts/one.css
----------------------------------------
body{color:#00f}
================================================================================
.output/chrome-mv3/assets/two.css
----------------------------------------
body{color:red}
================================================================================
.output/chrome-mv3/content-scripts/one.js
----------------------------------------
<contents-ignored>
================================================================================
.output/chrome-mv3/content-scripts/two.css
----------------------------------------
body{color:red}
================================================================================
.output/chrome-mv3/content-scripts/two.js
----------------------------------------
@@ -69,7 +69,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
`);
});
+10 -10
View File
@@ -255,14 +255,14 @@ describe('TypeScript Project', () => {
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"],
\\"@\\": [\\"..\\"],
\\"@/*\\": [\\"../*\\"],
\\"~\\": [\\"..\\"],
\\"~/*\\": [\\"../*\\"]
\\"~/*\\": [\\"../*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
}
},
\\"include\\": [
@@ -299,14 +299,14 @@ describe('TypeScript Project', () => {
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
\\"skipLibCheck\\": true,
\\"paths\\": {
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"],
\\"@\\": [\\"../src\\"],
\\"@/*\\": [\\"../src/*\\"],
\\"~\\": [\\"../src\\"],
\\"~/*\\": [\\"../src/*\\"]
\\"~/*\\": [\\"../src/*\\"],
\\"@@\\": [\\"..\\"],
\\"@@/*\\": [\\"../*\\"],
\\"~~\\": [\\"..\\"],
\\"~~/*\\": [\\"../*\\"]
}
},
\\"include\\": [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.6.6",
"version": "0.7.0",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18.16.0",
+9 -4
View File
@@ -84,10 +84,15 @@ async function buildSingleEntrypoint(
config.outDir,
'.js',
),
// Output content script CSS to assets/ with a hash to prevent conflicts. Defaults to
// "[name].[ext]" in lib mode, which usually results in "style.css". That means multiple
// content scripts with styles would overwrite each other if it weren't changed below.
assetFileNames: `assets/${entrypoint.name}.[ext]`,
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
if (entrypoint.type === 'content-script' && name?.endsWith('css')) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
return `assets/${entrypoint.name}.[ext]`;
}
},
},
},
},
+5 -5
View File
@@ -213,14 +213,14 @@ async function writeTsConfigFile(
"lib": ["DOM", "WebWorker"],
"skipLibCheck": true,
"paths": {
"@@": ["${rootPath}"],
"@@/*": ["${rootPath}/*"],
"~~": ["${rootPath}"],
"~~/*": ["${rootPath}/*"],
"@": ["${srcPath}"],
"@/*": ["${srcPath}/*"],
"~": ["${srcPath}"],
"~/*": ["${srcPath}/*"]
"~/*": ["${srcPath}/*"],
"@@": ["${rootPath}"],
"@@/*": ["${rootPath}/*"],
"~~": ["${rootPath}"],
"~~/*": ["${rootPath}/*"]
}
},
"include": [
+6 -3
View File
@@ -101,10 +101,13 @@ export function reloadContentScripts(
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const css = getContentScriptCssFiles([entry], server.currentOutput);
const { include: _1, exclude: _2, ...options } = entry.options;
server.reloadContentScript({
...options,
allFrames: entry.options.allFrames,
excludeMatches: entry.options.excludeMatches,
matches: entry.options.matches,
runAt: entry.options.runAt,
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: entry.options.world,
js,
css,
});
+12
View File
@@ -406,6 +406,18 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
* @default "ISOLATED"
*/
world?: 'ISOLATED' | 'MAIN';
/**
* Customize how imported/generated styles are injected with the content script. Regardless of the
* mode selected, CSS will always be built and included in the output directory.
*
* - `"manifest"` - Include the CSS in the manifest, under the content script's `css` array.
* - `"manual"` - Exclude the CSS from the manifest. You are responsible for manually loading it
* onto the page. Use `browser.runtime.getURL("content-scripts/<name>.css")` to get the file's
* URL
*
* @default "manifest"
*/
cssInjectionMode?: 'manifest' | 'manual';
/**
* Main function executed when the content script is loaded.
*/
+2
View File
@@ -8,6 +8,7 @@ import {
UserManifest,
ExtensionRunnerConfig,
ConfigEnv,
UserConfig,
} from './external';
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from 'c12';
@@ -48,6 +49,7 @@ export interface InternalConfig {
enabled: boolean;
template: NonNullable<PluginVisualizerOptions['template']>;
};
userConfigMetadata: Omit<ResolvedConfig<UserConfig>, 'config'>;
}
export type EntrypointGroup = Entrypoint | Entrypoint[];
+5 -2
View File
@@ -30,13 +30,15 @@ export async function getInternalConfig(
// Load user config
let userConfig: UserConfig = {};
let userConfigMetadata: InternalConfig['userConfigMetadata'] | undefined;
if (inlineConfig.configFile !== false) {
const loaded = await loadConfig<UserConfig>({
const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({
name: 'wxt',
cwd: inlineConfig.root ?? process.cwd(),
rcFile: false,
});
userConfig = loaded.config ?? {};
userConfig = loadedConfig ?? {};
userConfigMetadata = metadata;
}
// Merge it into the inline config
@@ -109,6 +111,7 @@ export async function getInternalConfig(
enabled: mergedConfig.analysis?.enabled ?? false,
template: mergedConfig.analysis?.template ?? 'treemap',
},
userConfigMetadata: userConfigMetadata ?? {},
};
finalConfig.vite = (env) =>
+7 -6
View File
@@ -78,19 +78,19 @@ export async function generateMainfest(
if (config.command === 'serve') addDevModeCsp(manifest, config);
if (config.command === 'serve') addDevModePermissions(manifest, config);
// TODO: transform manifest here.
const finalManifest = produce(manifest, config.transformManifest);
if (manifest.name == null)
if (finalManifest.name == null)
throw Error(
"Manifest 'name' is missing. Either:\n1. Set the name in your <rootDir>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
);
if (manifest.version == null) {
if (finalManifest.version == null) {
throw Error(
"Manifest 'version' is missing. Either:\n1. Add a version in your <rootDir>/package.json\n2. Pass the version via the manifest option in your wxt.config.ts",
);
}
return produce(manifest, config.transformManifest);
return finalManifest;
}
/**
@@ -416,9 +416,10 @@ export function getContentScriptCssFiles(
const allChunks = buildOutput.steps.flatMap((step) => step.chunks);
contentScripts.forEach((script) => {
// TODO: optimize and remove loop with a map
if (script.options.cssInjectionMode === 'manual') return;
const relatedCss = allChunks.find(
(chunk) => chunk.fileName === `assets/${script.name}.css`,
(chunk) => chunk.fileName === `content-scripts/${script.name}.css`,
);
if (relatedCss) css.push(relatedCss.fileName);
});
+1
View File
@@ -240,5 +240,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
name: faker.person.firstName().toLowerCase(),
},
transformManifest: () => {},
userConfigMetadata: {},
};
});