Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa5fd01cb | |||
| de16423e02 | |||
| aa4c0449e9 | |||
| 9d00eb2466 | |||
| 9ac756fb43 | |||
| f195aa429c | |||
| ca20a210ea | |||
| 547fee0e0e | |||
| 54b18cc66e | |||
| d8c190365a | |||
| b625f41919 | |||
| 236a135ac2 | |||
| dca43efe0d |
@@ -7,7 +7,8 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
wxt:
|
||||
name: WXT
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -45,4 +46,50 @@ jobs:
|
||||
working-directory: demo
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Validate Vanilla
|
||||
working-directory: templates/vanilla
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate Vue
|
||||
working-directory: templates/vue
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate React
|
||||
working-directory: templates/react
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate Svelte
|
||||
working-directory: templates/svelte
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run check
|
||||
|
||||
+38
-2
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.4...v0.2.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Auto-import from subdirectories ([547fee0](https://github.com/aklinker1/wxt/commit/547fee0))
|
||||
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/aklinker1/wxt/commit/ca20a21))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/aklinker1/wxt/commit/d8c1903))
|
||||
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/aklinker1/wxt/commit/54b18cc))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/aklinker1/wxt/commit/9d00eb2))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate project templates ([9ac756f](https://github.com/aklinker1/wxt/commit/9ac756f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `wxt zip` command ([#47](https://github.com/aklinker1/wxt/pull/47))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.2...v0.2.3)
|
||||
@@ -12,7 +50,6 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.2
|
||||
@@ -146,7 +183,6 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.2
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Demo",
|
||||
"name": "WXT Demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo extension for WXT",
|
||||
"type": "module",
|
||||
@@ -11,6 +11,7 @@
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
+4
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.5",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -73,13 +73,15 @@
|
||||
"jiti": "^1.18.2",
|
||||
"json5": "^2.2.3",
|
||||
"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",
|
||||
"web-ext": "^7.6.2",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
|
||||
Generated
+13
@@ -50,6 +50,9 @@ importers:
|
||||
linkedom:
|
||||
specifier: ^0.14.26
|
||||
version: 0.14.26
|
||||
minimatch:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
picocolors:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -71,6 +74,9 @@ importers:
|
||||
webextension-polyfill:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
zip-dir:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
devDependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.0.2
|
||||
@@ -3851,6 +3857,13 @@ packages:
|
||||
brace-expansion: 2.0.1
|
||||
dev: false
|
||||
|
||||
/minimatch@9.0.3:
|
||||
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
dev: false
|
||||
|
||||
/minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: false
|
||||
|
||||
+3
-16
@@ -1,7 +1,6 @@
|
||||
import tsup from 'tsup';
|
||||
import glob from 'fast-glob';
|
||||
import pc from 'picocolors';
|
||||
import path from 'path';
|
||||
import { printFileList } from '../src/core/log/printFileList';
|
||||
import { formatDuration } from '../src/core/utils/formatDuration';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
@@ -73,18 +72,6 @@ await Promise.all([
|
||||
spinner.succeed();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const outFiles = await glob(`${outDir}/**`);
|
||||
outFiles.forEach((file, i) => {
|
||||
const color = file.endsWith('.map')
|
||||
? pc.dim
|
||||
: file.endsWith('.d.ts')
|
||||
? pc.blue
|
||||
: pc.cyan;
|
||||
const prefix = i === outFiles.length - 1 ? ' └─' : ' ├─';
|
||||
console.log(
|
||||
`${pc.gray(prefix)} ${pc.dim(outDir + path.sep)}${color(
|
||||
path.relative(outDir, file),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
const outFiles = await glob(`${outDir}/**`, { absolute: true });
|
||||
await printFileList(consola.log, outDir, outFiles);
|
||||
consola.success(`Finished in ${formatDuration(duration)}`);
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
declare module 'zip-dir' {
|
||||
// Represents the options object for zipdir function
|
||||
interface ZipDirOptions {
|
||||
saveTo?: string;
|
||||
filter?: (path: string, stat: import('fs').Stats) => boolean;
|
||||
each?: (path: string) => void;
|
||||
}
|
||||
|
||||
function zipdir(
|
||||
dirPath: string,
|
||||
options?: ZipDirOptions,
|
||||
callback?: (error: Error | null, buffer: Buffer) => void,
|
||||
): Promise<Buffer>;
|
||||
|
||||
export = zipdir;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './dev';
|
||||
export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as wxt from '../..';
|
||||
import { buildInternal } from '../../core/build';
|
||||
import { getInternalConfig } from '../../core/utils/getInternalConfig';
|
||||
import { zipExtension } from '../../core/zip';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const zip = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
const output = await buildInternal(config);
|
||||
await zipExtension(config, output);
|
||||
});
|
||||
@@ -28,6 +28,16 @@ cli
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
cli
|
||||
.command('zip [root]', 'build for production and zip output')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.zip);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
|
||||
+6
-2
@@ -39,7 +39,7 @@ export async function buildInternal(
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups);
|
||||
const { output } = await rebuild(config, groups, undefined);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
@@ -70,7 +70,11 @@ export async function rebuild(
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config);
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
config.logger.warn('Failed to update .wxt directory:', err);
|
||||
// Throw the error if doing a regular build, don't for dev mode.
|
||||
if (config.command === 'build') throw err;
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('findEntrypoints', () => {
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -210,10 +211,10 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load content script entrypoint config from %s',
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: ContentScriptEntrypoint['options'] = {
|
||||
matches: ['<all_urls>'],
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
@@ -226,6 +227,24 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
|
||||
globMock.mockResolvedValueOnce([]);
|
||||
|
||||
const entrypoints = await findEntrypoints({
|
||||
...config,
|
||||
command: 'serve',
|
||||
});
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({
|
||||
type: 'background',
|
||||
inputPath: 'virtual:user-background',
|
||||
name: 'background',
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, GenericEntrypoint]>([
|
||||
// Sandbox
|
||||
[
|
||||
|
||||
@@ -16,9 +16,10 @@ 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';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
*/
|
||||
export async function findEntrypoints(
|
||||
config: InternalConfig,
|
||||
@@ -33,6 +34,7 @@ export async function findEntrypoints(
|
||||
const existingNames: Record<string, Entrypoint | undefined> = {};
|
||||
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
let hasBackground = false;
|
||||
await Promise.all(
|
||||
relativePaths.map(async (relativePath) => {
|
||||
const path = resolve(config.entrypointsDir, relativePath);
|
||||
@@ -63,6 +65,7 @@ export async function findEntrypoints(
|
||||
break;
|
||||
case 'background':
|
||||
entrypoint = await getBackgroundEntrypoint(config, path);
|
||||
hasBackground = true;
|
||||
break;
|
||||
case 'content-script':
|
||||
entrypoint = await getContentScriptEntrypoint(
|
||||
@@ -95,6 +98,11 @@ export async function findEntrypoints(
|
||||
existingNames[entrypoint.name] = entrypoint;
|
||||
}),
|
||||
);
|
||||
if (config.command === 'serve' && !hasBackground) {
|
||||
entrypoints.push(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
);
|
||||
}
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
@@ -195,10 +203,17 @@ async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importTsFile<BackgroundScriptDefintition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
options = moduleOptions;
|
||||
}
|
||||
return {
|
||||
type: 'background',
|
||||
@@ -257,6 +272,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
'content/index.ts?(x)': 'content-script',
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import path, { relative, resolve } from 'path';
|
||||
import { resolve } from 'path';
|
||||
import { BuildOutput, InternalConfig } from '../types';
|
||||
import { printTable } from './printTable';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printFileList } from './printFileList';
|
||||
|
||||
export async function printBuildSummary(
|
||||
output: BuildOutput,
|
||||
@@ -20,31 +17,8 @@ export async function printBuildSummary(
|
||||
return l.fileName.localeCompare(r.fileName);
|
||||
});
|
||||
|
||||
let totalSize = 0;
|
||||
|
||||
const chunkRows: string[][] = await Promise.all(
|
||||
chunks.map(async (chunk, i) => {
|
||||
const file = [
|
||||
relative(process.cwd(), config.outDir) + path.sep,
|
||||
chunk.fileName,
|
||||
];
|
||||
const prefix = i === chunks.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(chunk.fileName);
|
||||
const stats = await fs.lstat(resolve(config.outDir, chunk.fileName));
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(file[0])}${color(file[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(config.logger.log, chunkRows);
|
||||
|
||||
config.logger.log(
|
||||
`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`,
|
||||
);
|
||||
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
|
||||
await printFileList(config.logger.log, config.outDir, files);
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_WEIGHT = 100;
|
||||
@@ -62,17 +36,3 @@ function getChunkSortWeight(filename: string) {
|
||||
)?.[1] ?? DEFAULT_SORT_WEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import path from 'node:path';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printTable } from './printTable';
|
||||
|
||||
export async function printFileList(
|
||||
log: (message: string) => void,
|
||||
baseDir: string,
|
||||
files: string[],
|
||||
): Promise<void> {
|
||||
let totalSize = 0;
|
||||
|
||||
const fileRows: string[][] = await Promise.all(
|
||||
files.map(async (file, i) => {
|
||||
const parts = [
|
||||
path.relative(process.cwd(), baseDir) + path.sep,
|
||||
path.relative(baseDir, file),
|
||||
];
|
||||
const prefix = i === files.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(file);
|
||||
const stats = await fs.lstat(file);
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(parts[0])}${color(parts[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(log, fileRows);
|
||||
|
||||
log(`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
'.zip': pc.yellow,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
@@ -92,6 +92,57 @@ export interface InlineConfig {
|
||||
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
|
||||
*/
|
||||
runner?: ExtensionRunnerConfig;
|
||||
zip?: {
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}} - The version_name or version from the manifest
|
||||
* - `{{browser}} - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{{name}}-{{version}}-{{browser}}.zip"
|
||||
*/
|
||||
artifactTemplate?: string;
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}} - The version_name or version from the manifest
|
||||
* - `{{browser}} - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{{name}}-{{version}}-sources.zip"
|
||||
*/
|
||||
sourcesTemplate?: string;
|
||||
/**
|
||||
* Override the artifactTemplate's `{{name}}` template variable. Defaults to the package.json's
|
||||
* name, or if that doesn't exist, the current working directories name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Root directory to ZIP. The ZIP can be uploaded to the Firefox Addon Store as your source
|
||||
* code. Defaults to the `config.root` directory.
|
||||
*/
|
||||
sourcesRoot?: string;
|
||||
/**
|
||||
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
* creating a ZIP of all your source code for Firfox. Patterns are relative to your
|
||||
* `config.zip.sourcesRoot`.
|
||||
*
|
||||
* Hidden files, node_modules, and tests are ignored by default.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
|
||||
* ]
|
||||
*/
|
||||
ignoredSources?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
|
||||
@@ -36,6 +36,13 @@ export interface InternalConfig {
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
runnerConfig: ResolvedConfig<ExtensionRunnerConfig>;
|
||||
zip: {
|
||||
name?: string;
|
||||
artifactTemplate: string;
|
||||
sourcesTemplate: string;
|
||||
ignoredSources: string[];
|
||||
sourcesRoot: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { kebabCaseAlphanumeric, removeImportStatements } from '../strings';
|
||||
|
||||
describe('String utils', () => {
|
||||
describe('kebabCaseAlphanumeric', () => {
|
||||
it.each([
|
||||
['HELLO', 'hello'],
|
||||
['Hello, World!', 'hello-world'],
|
||||
['hello123', 'hello123'],
|
||||
['Hello World This Is A Test', 'hello-world-this-is-a-test'],
|
||||
['Hello World', 'hello-world'],
|
||||
['hello-world', 'hello-world'], // Ensure hyphens are preserved
|
||||
])('should convert "%s" to "%s"', (input, expected) => {
|
||||
expect(kebabCaseAlphanumeric(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeImportStatements', () => {
|
||||
it('should remove all import formats', () => {
|
||||
const imports = `
|
||||
import { registerGithubService, createGithubApi } from "@/utils/github";
|
||||
import {
|
||||
registerGithubService,
|
||||
createGithubApi
|
||||
} from "@/utils/github";
|
||||
import{ registerGithubService, createGithubApi }from "@/utils/github";
|
||||
import GitHub from "@/utils/github";
|
||||
import "@/utils/github";
|
||||
import '@/utils/github';
|
||||
import"@/utils/github"
|
||||
import'@/utils/github';
|
||||
`;
|
||||
expect(removeImportStatements(imports).trim()).toEqual('');
|
||||
});
|
||||
|
||||
it('should not remove import.meta or inline import statements', () => {
|
||||
const imports = `
|
||||
import.meta.env.DEV
|
||||
const a = await import("example");
|
||||
import("example");
|
||||
`;
|
||||
expect(removeImportStatements(imports)).toEqual(imports);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export function getUnimportOptions(
|
||||
],
|
||||
presets: [{ package: 'wxt/client' }],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
dirs: ['./components/*', './composables/*', './hooks/*', './utils/*'],
|
||||
};
|
||||
|
||||
return mergeConfig(
|
||||
|
||||
@@ -99,6 +99,26 @@ export async function getInternalConfig(
|
||||
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 ?? []),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Customize the default vite config
|
||||
@@ -122,6 +142,7 @@ export async function getInternalConfig(
|
||||
);
|
||||
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) => {
|
||||
@@ -144,6 +165,7 @@ type InternalConfigNoUserDirs = Omit<
|
||||
| 'typesDir'
|
||||
| 'fsCache'
|
||||
| 'manifest'
|
||||
| 'zip'
|
||||
>;
|
||||
|
||||
async function resolveManifestConfig(
|
||||
|
||||
@@ -5,6 +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';
|
||||
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
@@ -36,7 +37,7 @@ export async function importTsFile<T>(
|
||||
await unimport.init();
|
||||
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = text.replace(/import.*[\n;]/gm, '');
|
||||
const textNoImports = removeImportStatements(text);
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
config.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
hashContentScriptOptions,
|
||||
mapWxtOptionsToContentScript,
|
||||
} from './content-scripts';
|
||||
import { getPackageJson } from './package';
|
||||
|
||||
/**
|
||||
* Writes the manifest to the output directory and the build output.
|
||||
@@ -88,25 +89,6 @@ export async function generateMainfest(
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the package.json from the current directory.
|
||||
*
|
||||
* TODO: look in root and up directories until it's found
|
||||
*/
|
||||
async function getPackageJson(
|
||||
config: InternalConfig,
|
||||
): Promise<Partial<Record<string, any>> | undefined> {
|
||||
const file = resolve(config.root, 'package.json');
|
||||
try {
|
||||
return await fs.readJson(file);
|
||||
} catch (err) {
|
||||
config.logger.debug(
|
||||
`Failed to read package.json at: ${file}. Returning undefined.`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes suffixes from the version, like X.Y.Z-alpha1 (which brosers don't allow), so it's a
|
||||
* simple version number, like X or X.Y or X.Y.Z, which browsers allow.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { InternalConfig } from '../types';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Read the project's package.json.
|
||||
*
|
||||
* TODO: look in root and up directories until it's found
|
||||
*/
|
||||
export async function getPackageJson(
|
||||
config: InternalConfig,
|
||||
): Promise<Partial<Record<string, any>> | undefined> {
|
||||
const file = resolve(config.root, 'package.json');
|
||||
try {
|
||||
return await fs.readJson(file);
|
||||
} catch (err) {
|
||||
config.logger.debug(
|
||||
`Failed to read package.json at: ${file}. Returning undefined.`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function kebabCaseAlphanumeric(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-\s]/g, '') // Remove all non-alphanumeric, non-hyphen characters
|
||||
.replace(/\s+/g, '-'); // Replace spaces with hyphens
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes import statements from the top of a file. Keeps import.meta and inline, async `import()`
|
||||
* calls.
|
||||
*/
|
||||
export function removeImportStatements(text: string): string {
|
||||
return text.replace(
|
||||
/(import\s?[{\w][\s\S]*?from\s?["'][\s\S]*?["'];?|import\s?["'][\s\S]*?["'];?)/gm,
|
||||
'',
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './virtualEntrypoint';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* In dev mode, if there's not a background script listed, we need to add one.
|
||||
*
|
||||
* This define's a virtual module that is basically just a noop.
|
||||
*/
|
||||
export function noopBackground(): Plugin {
|
||||
const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
return {
|
||||
name: 'wxt:noop-background',
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `import { defineBackground } from 'wxt/client';\nexport default defineBackground(() => void 0)`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
@@ -0,0 +1,75 @@
|
||||
import { BuildOutput, InternalConfig } from './types';
|
||||
import zipdir from 'zip-dir';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
import { kebabCaseAlphanumeric } from './utils/strings';
|
||||
import { getPackageJson } from './utils/package';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { formatDuration } from './utils/formatDuration';
|
||||
import { printFileList } from './log/printFileList';
|
||||
|
||||
/**
|
||||
* Zip the extension for distribution. Does not build, just zips the output that should exist for
|
||||
* the given config.
|
||||
*
|
||||
* Returns a list of ZIP files that were created.
|
||||
*/
|
||||
export async function zipExtension(
|
||||
config: InternalConfig,
|
||||
buildOutput: BuildOutput,
|
||||
): Promise<string[]> {
|
||||
const start = Date.now();
|
||||
config.logger.info('Zipping extension...');
|
||||
const zipFiles: string[] = [];
|
||||
|
||||
const projectName =
|
||||
config.zip.name ??
|
||||
kebabCaseAlphanumeric(
|
||||
(await getPackageJson(config))?.name || dirname(process.cwd()),
|
||||
);
|
||||
const applyTemplate = (template: string): string =>
|
||||
template
|
||||
.replaceAll('{{name}}', projectName)
|
||||
.replaceAll('{{browser}}', config.browser)
|
||||
.replaceAll(
|
||||
'{{version}}',
|
||||
buildOutput.manifest.version_name ?? buildOutput.manifest.version,
|
||||
)
|
||||
.replaceAll('{{manifestVersion}}', `mv${config.manifestVersion}`);
|
||||
|
||||
await fs.ensureDir(config.outBaseDir);
|
||||
|
||||
// ZIP output directory
|
||||
|
||||
const outZipFilename = applyTemplate(config.zip.artifactTemplate);
|
||||
const outZipPath = resolve(config.outBaseDir, outZipFilename);
|
||||
await zipdir(config.outDir, {
|
||||
saveTo: outZipPath,
|
||||
});
|
||||
zipFiles.push(outZipPath);
|
||||
|
||||
// ZIP sources for Firefox
|
||||
|
||||
if (config.browser === 'firefox') {
|
||||
const sourcesZipFilename = applyTemplate(config.zip.sourcesTemplate);
|
||||
const sourcesZipPath = resolve(config.outBaseDir, sourcesZipFilename);
|
||||
await zipdir(config.zip.sourcesRoot, {
|
||||
saveTo: sourcesZipPath,
|
||||
filter(path) {
|
||||
const relativePath = relative(config.zip.sourcesRoot, path);
|
||||
const matchedPattern = config.zip.ignoredSources.find((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
);
|
||||
return matchedPattern == null;
|
||||
},
|
||||
});
|
||||
zipFiles.push(sourcesZipPath);
|
||||
}
|
||||
|
||||
config.logger.success(
|
||||
`Zipped extension in ${formatDuration(Date.now() - start)}`,
|
||||
);
|
||||
await printFileList(config.logger.log, config.outBaseDir, zipFiles);
|
||||
|
||||
return zipFiles;
|
||||
}
|
||||
+2
-1
@@ -59,8 +59,9 @@ export async function createServer(
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
const fileChanges = changeQueue.splice(0, changeQueue.length);
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
|
||||
@@ -218,4 +218,11 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
|
||||
vite: {},
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
ignoredSources: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.5"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user