Compare commits

...

2 Commits

Author SHA1 Message Date
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
20 changed files with 353 additions and 82 deletions
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## 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)
+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.4",
"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')
+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[];
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { kebabCaseAlphanumeric } 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);
});
});
});
+21
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
@@ -144,6 +164,7 @@ type InternalConfigNoUserDirs = Omit<
| 'typesDir'
| 'fsCache'
| 'manifest'
| 'zip'
>;
async function resolveManifestConfig(
+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 {};
}
}
+6
View File
@@ -0,0 +1,6 @@
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
}
+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;
}
+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(),
},
}));