Compare commits

...

8 Commits

Author SHA1 Message Date
Aaron Klinker ed04f460a6 0.19.1-alpha3 2024-07-27 14:22:31 -05:00
Aaron Klinker f810444a4c Remove unused imports from entrypoints when using vite-node loader 2024-07-27 14:22:05 -05:00
Aaron Klinker 9fad20332f 0.19.1-alpha2 2024-07-27 08:58:58 -05:00
Aaron Klinker b530815065 Remove main function from entrypoint before auto-imports 2024-07-27 08:58:08 -05:00
Aaron Klinker 09a0bba756 0.19.1-alpha1 2024-07-26 15:09:39 -05:00
Aaron Klinker 05803b2d59 cleanup 2024-07-26 15:06:37 -05:00
Aaron Klinker 6cc7ecb252 Cleanup 2024-07-26 15:05:30 -05:00
Aaron Klinker f8804a12c2 fix: web globals not defined when importing entrypoints 2024-07-26 15:03:54 -05:00
12 changed files with 275 additions and 66 deletions
+1 -1
View File
@@ -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",
+1 -4
View File
@@ -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';
+77 -6
View File
@@ -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;
}