Skip to content

Commit

Permalink
feat: Hooks (#419)
Browse files Browse the repository at this point in the history
Deprecated `transformManifest` since a hook now exists for it.
  • Loading branch information
aklinker1 authored Feb 4, 2024
1 parent bb02264 commit 21636de
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 6 deletions.
99 changes: 99 additions & 0 deletions e2e/tests/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { TestProject } from '../utils';
import { WxtHooks } from '~/types';

const hooks: WxtHooks = {
ready: vi.fn(),
'build:before': vi.fn(),
'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(),
'entrypoints:resolved': vi.fn(),
'entrypoints:grouped': vi.fn(),
};

function expectHooksToBeCalled(called: Record<keyof WxtHooks, boolean>) {
Object.keys(hooks).forEach((key) => {
const hookName = key as keyof WxtHooks;
const times = called[hookName] ? 1 : 0;
expect(
hooks[hookName],
`Expected "${hookName}" to be called ${times} time(s)`,
).toBeCalledTimes(called[hookName] ? 1 : 0);
});
}

describe('Hooks', () => {
beforeEach(() => {
Object.values(hooks).forEach((fn) => fn.mockReset());
});

it('prepare should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');

await project.prepare({ hooks });

expectHooksToBeCalled({
ready: true,
'build:before': false,
'build:done': false,
'build:manifestGenerated': false,
'entrypoints:grouped': false,
'entrypoints:resolved': true,
});
});

it('build should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');

await project.build({ hooks });

expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});

it('zip should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');

await project.zip({ hooks });

expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});

it('server.start should call hooks', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', '<html></html>');

const server = await project.startServer({
hooks,
runner: {
disabled: true,
},
});
await server.stop();

expectHooksToBeCalled({
ready: true,
'build:before': true,
'build:done': true,
'build:manifestGenerated': true,
'entrypoints:grouped': true,
'entrypoints:resolved': true,
});
});
});
35 changes: 30 additions & 5 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { dirname, join, relative, resolve } from 'path';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { execaCommand } from 'execa';
import { InlineConfig, UserConfig, build } from '../src';
import {
InlineConfig,
UserConfig,
build,
createServer,
prepare,
zip,
} from '../src';
import { normalizePath } from '../src/core/utils/paths';
import merge from 'lodash.merge';

Expand Down Expand Up @@ -63,10 +70,29 @@ export class TestProject {
if (filename === 'wxt.config.ts') this.config = {};
}

/**
* Write the files to the test directory install dependencies, and build the project.
*/
async prepare(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await prepare({ ...config, root: this.root });
}

async build(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await build({ ...config, root: this.root });
}

async zip(config: InlineConfig = {}) {
await this.writeProjectToDisk();
await zip({ ...config, root: this.root });
}

async startServer(config: InlineConfig = {}) {
await this.writeProjectToDisk();
const server = await createServer({ ...config, root: this.root });
await server.start();
return server;
}

private async writeProjectToDisk() {
if (this.config == null) this.setConfigFileConfig();

for (const file of this.files) {
Expand All @@ -80,7 +106,6 @@ export class TestProject {
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
cwd: this.root,
});
await build({ ...config, root: this.root });
}

/**
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.3",
"hookable": "^5.5.3",
"immer": "^10.0.3",
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/core/utils/building/find-entrypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
return true;
});
wxt.logger.debug(`${wxt.config.browser} entrypoints:`, targetEntrypoints);
await wxt.hooks.callHook('entrypoints:resolved', wxt, targetEntrypoints);

return targetEntrypoints;
}

Expand Down
5 changes: 5 additions & 0 deletions src/core/utils/building/internal-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import { wxt } from '../../wxt';
* 3. Prints the summary
*/
export async function internalBuild(): Promise<BuildOutput> {
await wxt.hooks.callHook('build:before', wxt);

const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
wxt.logger.info(
Expand Down Expand Up @@ -57,7 +59,10 @@ export async function internalBuild(): Promise<BuildOutput> {
}

const groups = groupEntrypoints(entrypoints);
await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);

const { output, warnings } = await rebuild(entrypoints, groups, undefined);
await wxt.hooks.callHook('build:done', wxt, output);

// Post-build
await printBuildSummary(
Expand Down
6 changes: 6 additions & 0 deletions src/core/utils/building/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export async function resolveConfig(
dev: {
reloadCommand,
},
hooks: mergedConfig.hooks ?? {},
};

const builder = await createViteBuilder(
Expand Down Expand Up @@ -191,6 +192,10 @@ function mergeInlineConfig(
inlineConfig.zip ?? {},
userConfig.zip ?? {},
);
const hooks: InlineConfig['hooks'] = defu(
inlineConfig.hooks ?? {},
userConfig.hooks ?? {},
);

return {
root: inlineConfig.root ?? userConfig.root,
Expand Down Expand Up @@ -229,6 +234,7 @@ function mergeInlineConfig(
...userConfig.dev,
...inlineConfig.dev,
},
hooks,
};
}

Expand Down
1 change: 1 addition & 0 deletions src/core/utils/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export async function generateManifest(
if (wxt.config.command === 'serve') addDevModePermissions(manifest);

const finalManifest = produce(manifest, wxt.config.transformManifest);
await wxt.hooks.callHook('build:manifestGenerated', wxt, finalManifest);

if (finalManifest.name == null)
throw Error(
Expand Down
9 changes: 8 additions & 1 deletion src/core/wxt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InlineConfig, Wxt, WxtDevServer } from '~/types';
import { InlineConfig, Wxt, WxtDevServer, WxtHooks } from '~/types';
import { resolveConfig } from './utils/building';
import { createHooks } from 'hookable';

/**
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
Expand All @@ -16,16 +17,22 @@ export async function registerWxt(
server?: WxtDevServer,
): Promise<void> {
const config = await resolveConfig(inlineConfig, command, server);
const hooks = createHooks<WxtHooks>();

wxt = {
config,
hooks,
get logger() {
return config.logger;
},
async reloadConfig() {
wxt.config = await resolveConfig(inlineConfig, command, server);
},
};

// Initialize hooks
wxt.hooks.addHooks(config.hooks);
await wxt.hooks.callHook('ready', wxt);
}

/**
Expand Down
53 changes: 53 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ContentScriptContext } from '../client/content-scripts/content-script-c
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
import type { FSWatcher } from 'chokidar';
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
import { Hookable, NestedHooks } from 'hookable';

export interface InlineConfig {
/**
Expand Down Expand Up @@ -173,6 +174,9 @@ export interface InlineConfig {
};

/**
* @deprecated Use `hooks.build.manifestGenerated` to modify your manifest instead. This option
* will be removed in v1.0
*
* Transform the final manifest before it's written to the file system. Edit the `manifest`
* parameter directly, do not return a new object. Return values are ignored.
*
Expand Down Expand Up @@ -261,6 +265,10 @@ export interface InlineConfig {
*/
reloadCommand?: string | false;
};
/**
* Project hooks for running logic during the build process.
*/
hooks?: NestedHooks<WxtHooks>;
}

// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
Expand Down Expand Up @@ -797,8 +805,52 @@ export interface ServerInfo {

export type HookResult = Promise<void> | void;

export interface WxtHooks {
/**
* Called after WXT initialization, when the WXT instance is ready to work.
* @param wxt The configured WXT object
* @returns Promise
*/
ready: (wxt: Wxt) => HookResult;
/**
* Called before the build is started in both dev mode and build mode.
*
* @param wxt The configured WXT object
*/
'build:before': (wxt: Wxt) => HookResult;
/**
* Called once the build process has finished.
* @param wxt The configured WXT object
* @param output The results of the build
*/
'build:done': (wxt: Wxt, output: Readonly<BuildOutput>) => HookResult;
/**
* Called once the manifest has been generated. Used to transform the manifest by reference before
* it is written to the output directory.
* @param wxt The configured WXT object
* @param manifest The manifest that was generated
*/
'build:manifestGenerated': (
wxt: Wxt,
manifest: Manifest.WebExtensionManifest,
) => HookResult;
/**
* Called once all entrypoints have been loaded from the `entrypointsDir`.
* @param wxt The configured WXT object
* @param entrypoints The list of entrypoints to be built
*/
'entrypoints:resolved': (wxt: Wxt, entrypoints: Entrypoint[]) => HookResult;
/**
* Called once all entrypoints have been grouped into their build groups.
* @param wxt The configured WXT object
* @param entrypoints The list of groups to build in each build step
*/
'entrypoints:grouped': (wxt: Wxt, groups: EntrypointGroup[]) => HookResult;
}

export interface Wxt {
config: ResolvedConfig;
hooks: Hookable<WxtHooks>;
/**
* Alias for config.logger
*/
Expand Down Expand Up @@ -856,6 +908,7 @@ export interface ResolvedConfig {
dev: {
reloadCommand: string | false;
};
hooks: Partial<WxtHooks>;
}

export interface FsCache {
Expand Down

0 comments on commit 21636de

Please sign in to comment.