Compare commits

...

13 Commits

Author SHA1 Message Date
GitHub Actions 5fa5fd01cb chore(release): v0.2.5 2023-07-20 16:19:32 +00:00
Aaron Klinker de16423e02 Add hanging-process reporter to CI to diagnose hung processes 2023-07-20 11:13:55 -05:00
Aaron Klinker aa4c0449e9 Fix failing tests 2023-07-20 11:13:55 -05:00
Aaron Klinker 9d00eb2466 chore: Update templates to wxt@0.2 2023-07-20 11:13:55 -05:00
Aaron Klinker 9ac756fb43 ci: Validate project templates 2023-07-20 11:13:55 -05:00
Aaron Klinker f195aa429c Fix type error 2023-07-20 10:40:03 -05:00
Aaron Klinker ca20a210ea feat: Include background script in dev mode if user doesn't define one 2023-07-20 10:38:53 -05:00
Aaron Klinker 547fee0e0e feat: Auto-import from subdirectories 2023-07-20 09:54:44 -05:00
Aaron Klinker 54b18cc66e fix: Properly load entrypoints that reference import.meta 2023-07-20 09:43:16 -05:00
Aaron Klinker d8c190365a fix: Don't crash when generating types in dev mode 2023-07-20 09:06:32 -05:00
Aaron Klinker b625f41919 Update changelog 2023-07-20 09:06:32 -05:00
GitHub Actions 236a135ac2 chore(release): v0.2.4 2023-07-16 19:41:24 +00:00
Aaron dca43efe0d feat: Add wxt zip command (#47) 2023-07-16 14:38:56 -05:00
33 changed files with 551 additions and 103 deletions
+49 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+13
View File
@@ -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
View File
@@ -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)}`);
+16
View File
@@ -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;
}
+1
View File
@@ -3,3 +3,4 @@ export * from './dev';
export * from './init';
export * from './prepare';
export * from './publish';
export * from './zip';
+31
View File
@@ -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);
});
+10
View File
@@ -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
View File
@@ -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
[
+21 -5
View File
@@ -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',
+4 -44
View File
@@ -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
);
}
+50
View File
@@ -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
);
}
+51
View File
@@ -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
+7
View File
@@ -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[];
+45
View File
@@ -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);
});
});
});
+1 -1
View File
@@ -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(
+22
View File
@@ -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(
+2 -1
View File
@@ -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'),
+1 -19
View File
@@ -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.
+22
View File
@@ -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 {};
}
}
+17
View File
@@ -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,
'',
);
}
+1
View File
@@ -5,3 +5,4 @@ export * from './multipageMove';
export * from './unimport';
export * from './virtualEntrypoint';
export * from './tsconfigPaths';
export * from './noopBackground';
+24
View File
@@ -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';
+75
View File
@@ -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
View File
@@ -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
+7
View File
@@ -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(),
},
}));
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -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"
}
}
+1 -1
View File
@@ -14,6 +14,6 @@
},
"devDependencies": {
"typescript": "^5.1.6",
"wxt": "^0.1.3"
"wxt": "^0.2.0"
}
}
+1 -1
View File
@@ -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"
}
}