Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed04f460a6 | |||
| f810444a4c | |||
| 9fad20332f | |||
| b530815065 | |||
| 09a0bba756 | |||
| 05803b2d59 | |||
| 6cc7ecb252 | |||
| f8804a12c2 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.0",
|
||||
"version": "0.19.1-alpha3",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -230,10 +230,7 @@ export async function createViteBuilder(
|
||||
baseConfig.optimizeDeps.noDiscovery = true;
|
||||
baseConfig.optimizeDeps.include = [];
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.extensionApiMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
],
|
||||
plugins: [wxtPlugins.removeEntrypointMainFunction(wxtConfig, path)],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
|
||||
@@ -14,8 +14,17 @@ export function removeEntrypointMainFunction(
|
||||
const absPath = normalizePath(resolve(config.root, path));
|
||||
return {
|
||||
name: 'wxt:remove-entrypoint-main-function',
|
||||
transform(code, id) {
|
||||
if (id === absPath) return removeMainFunctionCode(code);
|
||||
transform: {
|
||||
order: 'pre',
|
||||
handler(code, id) {
|
||||
if (id === absPath) {
|
||||
const newCode = removeMainFunctionCode(code);
|
||||
config.logger.debug('vite-node transformed entrypoint', path);
|
||||
config.logger.debug(`Original:\n---\n${code}\n---`);
|
||||
config.logger.debug(`Transformed:\n---\n${newCode.code}\n---`);
|
||||
return newCode;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,23 +6,28 @@ describe('Transform Utils', () => {
|
||||
it.each(['defineBackground', 'defineUnlistedScript'])(
|
||||
'should remove the first arrow function argument for %s',
|
||||
(def) => {
|
||||
const input = `export default ${def}(() => {
|
||||
const input = `
|
||||
export default ${def}(() => {
|
||||
console.log();
|
||||
})`;
|
||||
const expected = `export default ${def}(() => {})`;
|
||||
})
|
||||
`;
|
||||
const expected = `export default ${def}();`;
|
||||
|
||||
const actual = removeMainFunctionCode(input).code;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['defineBackground', 'defineUnlistedScript'])(
|
||||
'should remove the first function argument for %s',
|
||||
(def) => {
|
||||
const input = `export default ${def}(function () {
|
||||
const input = `
|
||||
export default ${def}(function () {
|
||||
console.log();
|
||||
})`;
|
||||
const expected = `export default ${def}(function () {})`;
|
||||
})
|
||||
`;
|
||||
const expected = `export default ${def}();`;
|
||||
|
||||
const actual = removeMainFunctionCode(input).code;
|
||||
|
||||
@@ -35,10 +40,12 @@ describe('Transform Utils', () => {
|
||||
'defineContentScript',
|
||||
'defineUnlistedScript',
|
||||
])('should remove the main field from %s', (def) => {
|
||||
const input = `export default ${def}({
|
||||
asdf: "asdf",
|
||||
main: () => {},
|
||||
})`;
|
||||
const input = `
|
||||
export default ${def}({
|
||||
asdf: "asdf",
|
||||
main: () => {},
|
||||
})
|
||||
`;
|
||||
const expected = `export default ${def}({
|
||||
asdf: "asdf"
|
||||
})`;
|
||||
@@ -47,5 +54,39 @@ describe('Transform Utils', () => {
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove unused imports', () => {
|
||||
const input = `
|
||||
import { defineBackground } from "wxt/sandbox"
|
||||
import { test1 } from "somewhere1"
|
||||
import test2 from "somewhere2"
|
||||
|
||||
export default defineBackground(() => {})
|
||||
`;
|
||||
const expected = `import { defineBackground } from "wxt/sandbox"
|
||||
|
||||
export default defineBackground();`;
|
||||
|
||||
const actual = removeMainFunctionCode(input).code;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should remove explict side-effect imports', () => {
|
||||
const input = `
|
||||
import { defineBackground } from "wxt/sandbox"
|
||||
import "my-polyfill"
|
||||
import "./style.css"
|
||||
|
||||
export default defineBackground(() => {})
|
||||
`;
|
||||
const expected = `import { defineBackground } from "wxt/sandbox"
|
||||
|
||||
export default defineBackground();`;
|
||||
|
||||
const actual = removeMainFunctionCode(input).code;
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
import { createExtensionEnvironment } from '../environments';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
@@ -72,47 +73,50 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
|
||||
// Import entrypoints to get their config
|
||||
let hasBackground = false;
|
||||
const entrypoints: Entrypoint[] = await Promise.all(
|
||||
entrypointInfos.map(async (info): Promise<Entrypoint> => {
|
||||
const { type } = info;
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
hasBackground = true;
|
||||
return await getBackgroundEntrypoint(info);
|
||||
case 'content-script':
|
||||
return await getContentScriptEntrypoint(info);
|
||||
case 'unlisted-page':
|
||||
return await getUnlistedPageEntrypoint(info);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(info);
|
||||
case 'content-script-style':
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
const env = createExtensionEnvironment();
|
||||
const entrypoints: Entrypoint[] = await env.run(() =>
|
||||
Promise.all(
|
||||
entrypointInfos.map(async (info): Promise<Entrypoint> => {
|
||||
const { type } = info;
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
hasBackground = true;
|
||||
return await getBackgroundEntrypoint(info);
|
||||
case 'content-script':
|
||||
return await getContentScriptEntrypoint(info);
|
||||
case 'unlisted-page':
|
||||
return await getUnlistedPageEntrypoint(info);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(info);
|
||||
case 'content-script-style':
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...info,
|
||||
type,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (wxt.config.command === 'serve' && !hasBackground) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { createEnvironment, Environment, EnvGlobals } from './environment';
|
||||
|
||||
export function createBrowserEnvironment(): Environment {
|
||||
return createEnvironment(getBrowserEnvironmentGlobals);
|
||||
}
|
||||
|
||||
export function getBrowserEnvironmentGlobals(): EnvGlobals {
|
||||
const { window, document, global } = parseHTML(`
|
||||
<html>
|
||||
<head></head>
|
||||
<body></body>
|
||||
</html>
|
||||
`);
|
||||
return {
|
||||
...global,
|
||||
window,
|
||||
document,
|
||||
self: global,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
export interface Environment {
|
||||
setup: () => () => void;
|
||||
run: <T>(fn: () => Promise<T>) => Promise<T>;
|
||||
}
|
||||
|
||||
export function createEnvironment(getGlobals: () => EnvGlobals): Environment {
|
||||
const setup = () => {
|
||||
const envGlobals = getGlobals();
|
||||
const ogGlobals = getOgGlobals(envGlobals);
|
||||
applyGlobals(envGlobals);
|
||||
|
||||
return () => {
|
||||
applyGlobals(ogGlobals);
|
||||
};
|
||||
};
|
||||
const run = async (fn: () => any) => {
|
||||
const teardown = setup();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
teardown();
|
||||
}
|
||||
};
|
||||
return {
|
||||
setup,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
export type EnvGlobals = Record<string, any>;
|
||||
|
||||
export function getOgGlobals(envGlobals: EnvGlobals): EnvGlobals {
|
||||
return Object.keys(envGlobals).reduce<typeof envGlobals>((acc, key) => {
|
||||
// @ts-expect-error: Untyped key on globalThis
|
||||
acc[key] = globalThis[key];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function applyGlobals(globals: EnvGlobals): void {
|
||||
Object.entries(globals).forEach(([key, envValue]) => {
|
||||
try {
|
||||
// @ts-expect-error: Untyped key on globalThis
|
||||
globalThis[key] = envValue;
|
||||
} catch (err) {
|
||||
// ignore any globals that can't be set
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { getBrowserEnvironmentGlobals } from './browser-environment';
|
||||
import { createEnvironment, Environment } from './environment';
|
||||
|
||||
export function createExtensionEnvironment(): Environment {
|
||||
return createEnvironment(getExtensionEnvironmentGlobals);
|
||||
}
|
||||
|
||||
export function getExtensionEnvironmentGlobals() {
|
||||
return {
|
||||
...getBrowserEnvironmentGlobals(),
|
||||
chrome: fakeBrowser,
|
||||
browser: fakeBrowser,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './browser-environment';
|
||||
export * from './extension-environment';
|
||||
@@ -2,8 +2,9 @@ import { ProxifiedModule, parseModule } from 'magicast';
|
||||
|
||||
/**
|
||||
* Removes any code used at runtime related to an entrypoint's main function.
|
||||
* - Removes or clears out `main` function from returned object
|
||||
* - TODO: Removes unused imports after main function has been removed to prevent importing runtime modules
|
||||
* 1. Removes or clears out `main` function from returned object
|
||||
* 2. Removes unused imports
|
||||
* 3. Removes value-less, side-effect only imports (like `import "./styles.css"` or `import "webextension-polyfill"`)
|
||||
*/
|
||||
export function removeMainFunctionCode(code: string): {
|
||||
code: string;
|
||||
@@ -11,16 +12,18 @@ export function removeMainFunctionCode(code: string): {
|
||||
} {
|
||||
const mod = parseModule(code);
|
||||
emptyMainFunction(mod);
|
||||
removeUnusedImports(mod);
|
||||
removeSideEffectImports(mod);
|
||||
return mod.generate();
|
||||
}
|
||||
|
||||
function emptyMainFunction(mod: ProxifiedModule) {
|
||||
function emptyMainFunction(mod: ProxifiedModule): void {
|
||||
if (mod.exports?.default?.$type === 'function-call') {
|
||||
if (mod.exports.default.$ast?.arguments?.[0]?.body) {
|
||||
// Remove body from function
|
||||
// ex: "fn(() => { ... })" to "fn(() => {})"
|
||||
// ex: "fn(function () { ... })" to "fn(function () {})"
|
||||
mod.exports.default.$ast.arguments[0].body.body = [];
|
||||
// ex: "fn(() => { ... })" to "fn()"
|
||||
// ex: "fn(function () { ... })" to "fn()"
|
||||
delete mod.exports.default.$ast.arguments[0];
|
||||
} else if (mod.exports.default.$ast?.arguments?.[0]?.properties) {
|
||||
// Remove main field from object
|
||||
// ex: "fn({ ..., main: () => {} })" to "fn({ ... })"
|
||||
@@ -31,3 +34,71 @@ function emptyMainFunction(mod: ProxifiedModule) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Do a more complex declaration analysis where shadowed variables are detected and ignored.
|
||||
// Right now, this code assumes there are no shadowed variables.
|
||||
function removeUnusedImports(mod: ProxifiedModule): void {
|
||||
const importSymbols = Object.keys(mod.imports);
|
||||
const usedMap = new Map(importSymbols.map((sym) => [sym, false]));
|
||||
|
||||
const queue: any[] = [getSimpleAstJson(mod.$ast)];
|
||||
for (const item of queue) {
|
||||
if (!item) {
|
||||
continue;
|
||||
} else if (Array.isArray(item)) {
|
||||
queue.push(...item);
|
||||
} else if (item.type === 'ImportDeclaration') {
|
||||
// Exclude looking for identifiers in import statements
|
||||
continue;
|
||||
} else if (item.type === 'Identifier') {
|
||||
// Only track import usages
|
||||
if (usedMap.has(item.name)) usedMap.set(item.name, true);
|
||||
} else if (typeof item === 'object') {
|
||||
queue.push(Object.values(item));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, used] of usedMap.entries()) {
|
||||
if (!used) delete mod.imports[name];
|
||||
}
|
||||
}
|
||||
|
||||
function deleteImportAst(
|
||||
mod: ProxifiedModule,
|
||||
shouldDelete: (node: any) => boolean,
|
||||
): void {
|
||||
const importIndexesToDelete: number[] = [];
|
||||
(mod.$ast as any).body.forEach((node: any, index: number) => {
|
||||
if (node.type === 'ImportDeclaration' && shouldDelete(node)) {
|
||||
importIndexesToDelete.push(index);
|
||||
}
|
||||
});
|
||||
importIndexesToDelete.reverse().forEach((i) => {
|
||||
delete (mod.$ast as any).body[i];
|
||||
});
|
||||
}
|
||||
|
||||
function removeSideEffectImports(mod: ProxifiedModule): void {
|
||||
deleteImportAst(mod, (node) => node.specifiers.length === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Util to get the AST as a simple JSON object, stripping out large objects and file locations to keep it readible
|
||||
*/
|
||||
function getSimpleAstJson(ast: any): any {
|
||||
if (!ast) {
|
||||
return ast;
|
||||
} else if (Array.isArray(ast)) {
|
||||
return ast.map(getSimpleAstJson);
|
||||
} else if (typeof ast === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(ast)
|
||||
.filter(
|
||||
([key, value]) => key !== 'loc' && key !== 'start' && key !== 'end',
|
||||
)
|
||||
.map(([key, value]) => [key, getSimpleAstJson(value)]),
|
||||
);
|
||||
} else {
|
||||
return ast;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ export function defineBackground(
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundDefinition,
|
||||
): BackgroundDefinition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
if (arg == null || typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ export function defineUnlistedScript(
|
||||
export function defineUnlistedScript(
|
||||
arg: (() => void) | UnlistedScriptDefinition,
|
||||
): UnlistedScriptDefinition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
if (arg == null || typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user