feat: CSS entrypoints (#61)

This commit is contained in:
Aaron
2023-07-21 12:57:09 -05:00
committed by GitHub
parent 5b269f4369
commit 044a24fd6a
18 changed files with 259 additions and 20 deletions
+1
View File
@@ -20,6 +20,7 @@
},
"devDependencies": {
"@types/webextension-polyfill": "^0.10.0",
"sass": "^1.64.0",
"wxt": "workspace:*"
}
}
+3
View File
@@ -0,0 +1,3 @@
body {
background-color: red;
}
@@ -0,0 +1,3 @@
body {
color: blue;
}
+1
View File
@@ -54,6 +54,7 @@ export default defineConfig({
{ text: 'Background', link: '/guide/background.md' },
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
{ text: 'CSS', link: '/guide/css.md' },
{ text: 'Devtools', link: '/guide/devtools.md' },
{ text: 'History', link: '/guide/history.md' },
{ text: 'Newtab', link: '/guide/newtab.md' },
+16 -1
View File
@@ -38,7 +38,7 @@ When defining multiple content scripts, content script entrypoints that have the
## CSS
To include CSS with your content script, import the CSS file at the top of your entrypoint:
To include CSS with your content script, import the CSS file at the top of your entrypoint.
```
@@ -60,3 +60,18 @@ export default defineContentScript({
},
});
```
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
```json
// .output/chrome-mv3/manifest.json
{
"content_scripts": [
{
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
"js": ["content-scripts/overlay.js"],
"css": ["content-scripts/overlay.css"]
}
]
}
```
+41
View File
@@ -0,0 +1,41 @@
# CSS
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
See [Content Script CSS](/guide/content-scripts.md#css) documentation for the recomended approach to include CSS with a content script.
:::info
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/config.md#transformmanifest) to manually add your CSS file to the manifest.
:::
## Filenames
When a filename matches the pattern below, WXT will transform it and output it with the rest of your extension.
- `entrypoints/content.(css|scss|sass|less|styl|stylus)`
- `entrypoints/<name>.(css|scss|sass|less|styl|stylus)`
- `entrypoints/<name>/index.(css|scss|sass|less|styl|stylus)`
- `entrypoints/<name>.content.(css|scss|sass|less|styl|stylus)`
- `entrypoints/<name>.content/index.(css|scss|sass|less|styl|stylus)`
## Definition
```css
body {
/* Plain CSS file */
}
```
Follow Vite's guide to setup a preprocessor: https://vitejs.dev/guide/features.html#css-pre-processors
```sh
pnpm i sass
```
```scss
body {
h1 {
/* ...*/
}
}
```
+1 -3
View File
@@ -78,7 +78,6 @@
"linkedom": "^0.14.26",
"minimatch": "^9.0.3",
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"unimport": "^3.0.8",
"vite": "^4.3.9",
"vite-tsconfig-paths": "^4.2.0",
@@ -91,7 +90,6 @@
"@types/fs-extra": "^11.0.1",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.3.1",
"@types/picomatch": "^2.3.0",
"@vitest/coverage-v8": "^0.32.2",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
@@ -125,4 +123,4 @@
]
}
}
}
}
+17 -10
View File
@@ -56,9 +56,6 @@ importers:
picocolors:
specifier: ^1.0.0
version: 1.0.0
picomatch:
specifier: ^2.3.1
version: 2.3.1
unimport:
specifier: ^3.0.8
version: 3.0.8
@@ -90,9 +87,6 @@ importers:
'@types/node':
specifier: ^20.3.1
version: 20.3.1
'@types/picomatch':
specifier: ^2.3.0
version: 2.3.0
'@vitest/coverage-v8':
specifier: ^0.32.2
version: 0.32.2(vitest@0.32.4)
@@ -142,6 +136,9 @@ importers:
'@types/webextension-polyfill':
specifier: ^0.10.0
version: 0.10.0
sass:
specifier: ^1.64.0
version: 1.64.0
wxt:
specifier: workspace:*
version: link:..
@@ -1046,10 +1043,6 @@ packages:
/@types/node@20.3.1:
resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==}
/@types/picomatch@2.3.0:
resolution: {integrity: sha512-O397rnSS9iQI4OirieAtsDqvCj4+3eY1J+EPdNTKuHuRWIfUoGyzX294o8C4KJYaLqgSrd2o60c5EqCU8Zv02g==}
dev: true
/@types/web-bluetooth@0.0.17:
resolution: {integrity: sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==}
dev: true
@@ -3172,6 +3165,10 @@ packages:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/immutable@4.3.1:
resolution: {integrity: sha512-lj9cnmB/kVS0QHsJnYKD1uo3o39nrbKxszjnqS9Fr6NB7bZzW45U6WSGBPKXDL/CvDKqDNPA4r3DoDQ8GTxo2A==}
dev: true
/import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
@@ -4803,6 +4800,16 @@ packages:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false
/sass@1.64.0:
resolution: {integrity: sha512-m7YtAGmQta9uANIUJwXesAJMSncqH+3INc8kdVXs6eV6GUC8Qu2IYKQSN8PRLgiQfpca697G94klm2leYMxSHw==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
chokidar: 3.5.3
immutable: 4.3.1
source-map-js: 1.0.2
dev: true
/sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
dev: false
@@ -433,6 +433,73 @@ describe('findEntrypoints', () => {
outputDir: config.outDir,
},
],
[
'injected/index.ts',
{
type: 'unlisted-script',
name: 'injected',
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
outputDir: config.outDir,
},
],
// unlisted-style
[
'iframe.scss',
{
type: 'unlisted-style',
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
outputDir: config.outDir,
},
],
[
'iframe.css',
{
type: 'unlisted-style',
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
outputDir: config.outDir,
},
],
// content-script-style
[
'content.css',
{
type: 'content-script-style',
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'overlay.content.css',
{
type: 'content-script-style',
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'content/index.css',
{
type: 'content-script-style',
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'overlay.content/index.css',
{
type: 'content-script-style',
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
])('should find entrypoint for %s', async (path, expected) => {
globMock.mockResolvedValueOnce([path]);
+11 -2
View File
@@ -6,7 +6,7 @@ import {
EntrypointGroup,
InternalConfig,
} from '../types';
import * as plugins from '../vite-plugins';
import * as wxtPlugins from '../vite-plugins';
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import fs from 'fs-extra';
@@ -45,7 +45,16 @@ async function buildSingleEntrypoint(
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
if (
entrypoint.type === 'content-script-style' ||
entrypoint.type === 'unlisted-style'
) {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
}
const libMode: vite.UserConfig = {
plugins,
build: {
lib: {
entry,
@@ -90,7 +99,7 @@ async function buildMultipleEntrypoints(
config: InternalConfig,
): Promise<BuildStepOutput> {
const multiPage: vite.UserConfig = {
plugins: [plugins.multipageMove(entrypoints, config)],
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
+21 -3
View File
@@ -10,13 +10,14 @@ import {
PopupEntrypoint,
} from '../types';
import fs from 'fs-extra';
import picomatch from 'picomatch';
import { minimatch } from 'minimatch';
import { parseHTML } from 'linkedom';
import JSON5 from 'json5';
import { importTsFile } from '../utils/importTsFile';
import glob from 'fast-glob';
import { getEntrypointName } from '../utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
import { CSS_EXTENSIONS_PATTERN } from '../utils/paths';
/**
* Return entrypoints and their configuration by looking through the project's files.
@@ -39,7 +40,7 @@ export async function findEntrypoints(
relativePaths.map(async (relativePath) => {
const path = resolve(config.entrypointsDir, relativePath);
const matchingGlob = pathGlobs.find((glob) =>
picomatch.isMatch(relativePath, glob),
minimatch(relativePath, glob),
);
if (matchingGlob == null) {
@@ -74,6 +75,14 @@ export async function findEntrypoints(
path,
);
break;
case 'content-script-style':
entrypoint = {
type,
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
};
break;
default:
entrypoint = {
type,
@@ -243,7 +252,7 @@ async function getContentScriptEntrypoint(
type: 'content-script',
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: resolve(config.outDir, 'content-scripts'),
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options,
};
}
@@ -278,6 +287,10 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'content/index.ts?(x)': 'content-script',
'*.content.ts?(x)': 'content-script',
'*.content/index.ts?(x)': 'content-script',
[`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
'popup.html': 'popup',
'popup/index.html': 'popup',
@@ -288,7 +301,12 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'*.html': 'unlisted-page',
'*/index.html': 'unlisted-page',
'*.ts': 'unlisted-script',
'*/index.ts': 'unlisted-script',
[`*.${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',
};
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
+3 -1
View File
@@ -261,7 +261,9 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script';
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
@@ -56,6 +56,18 @@ const sandbox2: Entrypoint = {
inputPath: '/sandbox2.html',
outputDir: '/.output/sandbox2',
};
const unlistedStyle: Entrypoint = {
type: 'unlisted-style',
name: 'injected',
inputPath: '/injected.scss',
outputDir: '/.output',
};
const contentScriptStyle: Entrypoint = {
type: 'content-script-style',
name: 'injected',
inputPath: '/overlay.content.scss',
outputDir: '/.output/content-scripts',
};
describe('groupEntrypoints', () => {
it('should keep scripts separate', () => {
@@ -72,6 +84,19 @@ describe('groupEntrypoints', () => {
expect(actual).toEqual(expected);
});
it('should keep styles separate', () => {
const entrypoints: Entrypoint[] = [
unlistedStyle,
contentScriptStyle,
popup,
];
const expected = [unlistedStyle, contentScriptStyle, [popup]];
const actual = groupEntrypoints(entrypoints);
expect(actual).toEqual(expected);
});
it('should group extension pages together', () => {
const entrypoints: Entrypoint[] = [
popup,
+2
View File
@@ -43,6 +43,8 @@ const ENTRY_TYPE_TO_GROUP_MAP: Record<Entrypoint['type'], Group> = {
background: 'no-group',
'content-script': 'no-group',
'unlisted-script': 'no-group',
'unlisted-style': 'no-group',
'content-script-style': 'no-group',
};
type Group = 'extension-page' | 'sandbox-page' | 'no-group';
+5
View File
@@ -15,3 +15,8 @@ export function normalizePath(path: string): string {
export function unnormalizePath(path: string): string {
return nodePath.normalize(path);
}
export const CSS_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'];
// .module.css files are not supported because these are global CSS files, so using CSS modules doesn't make sense.
export const CSS_EXTENSIONS_PATTERN = `+(${CSS_EXTENSIONS.join('|')})`;
+39
View File
@@ -0,0 +1,39 @@
import * as vite from 'vite';
import { Entrypoint, InternalConfig } from '../types';
import { getEntrypointBundlePath } from '../utils/entrypoints';
/**
* Rename CSS entrypoint outputs to ensure a JS file is not generated, and that the CSS file is
* placed in the correct place.
*
* It:
* 1. Renames CSS files to their final paths
* 2. Removes the JS file that get's output by lib mode
*
* THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be added to every build.
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
config: InternalConfig,
): vite.Plugin {
return {
name: 'wxt:css-entrypoint',
config() {
return {
build: {
rollupOptions: {
output: {
assetFileNames: () =>
getEntrypointBundlePath(entrypoint, config.outDir, '.css'),
},
},
},
};
},
generateBundle(_, bundle) {
Object.keys(bundle).forEach((file) => {
if (file.endsWith('.js')) delete bundle[file];
});
},
};
}
+1
View File
@@ -6,3 +6,4 @@ export * from './unimport';
export * from './virtualEntrypoint';
export * from './tsconfigPaths';
export * from './noopBackground';
export * from './cssEntrypoints';
+2
View File
@@ -15,6 +15,8 @@ import { normalizePath } from '../utils/paths';
* Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't need to be modified.
* HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML
* files either.
*
* THIS PLUGIN SHOULD ONLY BE APPLIED TO MULTIPAGE BUILDS. It should not be added to every build.
*/
export function multipageMove(
entrypoints: Entrypoint[],