fix!: Remove deprecated jiti entrypoint loader (#1087)

This commit is contained in:
Aaron
2024-10-19 05:44:54 -05:00
parent c712175445
commit 65dfded682
12 changed files with 39 additions and 333 deletions
@@ -17,63 +17,3 @@ If you're running into errors while importing entrypoints, run `wxt prepare --de
:::
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
There are two options for loading your entrypoints:
1. `vite-node` - default as of `v0.19.0`
2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0`
## vite-node
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader.
## jiti
To enable `jiti`:
```ts
export default defineConfig({
entrypointLoader: 'jiti',
});
```
This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code.
That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options:
```ts
// entrypoints/content.ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
main() {
// ...
},
});
```
```
$ wxt build
wxt build
WXT 0.14.1
Building chrome-mv3 for production with Vite 5.0.5
✖ Command failed after 360 ms
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function.
```
Usually, this error occurs when you try to extract options into a shared file or when running code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
export default defineContentScript({
matches: GOOGLE_MATCHES, // [!code --]
matches: ['*//*.google.com/*'], // [!code ++]
main() {
// ...
},
});
```
+22 -25
View File
@@ -1,36 +1,33 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe.each(['vite-node', 'jiti'] as const)(
'Manifest Content (Vite runtime? %s)',
(entrypointImporter) => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
describe('Manifest Content', () => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
persistent: {
firefox: true,
safari: false,
},
main: () => {},
})`,
);
await project.build({ browser, experimental: { entrypointImporter } });
);
await project.build({ browser });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
},
);
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
});
-1
View File
@@ -100,7 +100,6 @@
"giget": "^1.2.3",
"hookable": "^5.5.3",
"is-wsl": "^3.1.0",
"jiti": "^1.21.6",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.18.5",
+17 -36
View File
@@ -20,7 +20,6 @@ import {
import { Hookable } from 'hookable';
import { toArray } from '../../utils/arrays';
import { safeVarName } from '../../utils/strings';
import { importEntrypointFile } from '../../utils/building';
import { ViteNodeServer } from 'vite-node/server';
import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
@@ -280,44 +279,26 @@ export async function createViteBuilder(
version: vite.version,
async importEntrypoint(path) {
const env = createExtensionEnvironment();
switch (wxtConfig.entrypointLoader) {
default:
case 'jiti': {
return await env.run(() => importEntrypointFile(path));
}
case 'vite-node': {
const { runner, server } = await createViteNodeImporter([path]);
const res = await env.run(() => runner.executeFile(path));
await server.close();
requireDefaultExport(path, res);
return res.default;
}
}
const { runner, server } = await createViteNodeImporter([path]);
const res = await env.run(() => runner.executeFile(path));
await server.close();
requireDefaultExport(path, res);
return res.default;
},
async importEntrypoints(paths) {
const env = createExtensionEnvironment();
switch (wxtConfig.entrypointLoader) {
default:
case 'jiti': {
return await env.run(() =>
Promise.all(paths.map(importEntrypointFile)),
);
}
case 'vite-node': {
const { runner, server } = await createViteNodeImporter(paths);
const res = await env.run(() =>
Promise.all(
paths.map(async (path) => {
const mod = await runner.executeFile(path);
requireDefaultExport(path, mod);
return mod.default;
}),
),
);
await server.close();
return res;
}
}
const { runner, server } = await createViteNodeImporter(paths);
const res = await env.run(() =>
Promise.all(
paths.map(async (path) => {
const mod = await runner.executeFile(path);
requireDefaultExport(path, mod);
return mod.default;
}),
),
);
await server.close();
return res;
},
async build(group) {
let entryConfig;
-1
View File
@@ -201,7 +201,6 @@ export async function resolveConfig(
analysis: resolveAnalysisConfig(root, mergedConfig),
userConfigMetadata: userConfigMetadata ?? {},
alias,
entrypointLoader: mergedConfig.entrypointLoader ?? 'vite-node',
experimental: defu(mergedConfig.experimental, {}),
dev: {
server: devServerConfig,
@@ -1,52 +0,0 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { importEntrypointFile } from '../../../utils/building';
import { resolve } from 'node:path';
import { setFakeWxt } from '../../../utils/testing/fake-objects';
const entrypointPath = (filename: string) =>
resolve(__dirname, 'test-entrypoints', filename);
describe('importEntrypointFile', () => {
beforeEach(() => {
setFakeWxt({
config: {
imports: false,
debug: false,
// Run inside the demo folder so that wxt is in the node_modules
// WXT must also be built for these tests to pass
root: 'demo',
},
});
});
it.each([
['background.ts', { main: expect.any(Function) }],
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
['unlisted.ts', { main: expect.any(Function) }],
['react.tsx', { main: expect.any(Function) }],
['with-named.ts', { main: expect.any(Function) }],
])(
'should return the default export of test-entrypoints/%s',
async (file, expected) => {
const actual = await importEntrypointFile(entrypointPath(file));
expect(actual).toEqual(expected);
},
);
it('should return undefined when there is no default export', async () => {
const actual = await importEntrypointFile(
entrypointPath('no-default-export.ts'),
);
expect(actual).toBeUndefined();
});
it('should throw a custom error message when an imported variable is used before main', async () => {
await expect(() =>
importEntrypointFile(entrypointPath('imported-option.ts')),
).rejects.toThrowError(
`imported-option.ts: Cannot use imported variable "faker" outside the main function.`,
);
});
});
@@ -1,127 +0,0 @@
import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
import { createUnimport } from 'unimport';
import fs from 'fs-extra';
import { relative, resolve } from 'node:path';
import { removeProjectImportStatements } from '../../utils/strings';
import { normalizePath } from '../../utils/paths';
import { TransformOptions, transformSync } from 'esbuild';
import { fileURLToPath } from 'node:url';
import { wxt } from '../../wxt';
/**
* Get the value from the default export of a `path`.
*
* It works by:
*
* 1. Reading the file text
* 2. Stripping all imports from it via regex
* 3. Auto-import only the client helper functions
*
* This prevents resolving imports of imports, speeding things up and preventing "xxx is not
* defined" errors.
*
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
*/
export async function importEntrypointFile<T>(path: string): Promise<T> {
wxt.logger.debug('Loading file metadata:', path);
// JITI & Babel uses normalized paths.
const normalPath = normalizePath(path);
const unimport = createUnimport({
...wxt.config.imports,
// Only allow specific imports, not all from the project
dirs: [],
});
await unimport.init();
const text = await fs.readFile(path, 'utf-8');
const textNoImports = removeProjectImportStatements(text);
const { code } = await unimport.injectImports(textNoImports);
wxt.logger.debug(
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
);
const jiti = createJITI(
typeof __filename !== 'undefined'
? __filename
: fileURLToPath(import.meta.url),
{
cache: false,
debug: wxt.config.debug,
esmResolve: true,
alias: {
'webextension-polyfill': resolve(
wxt.config.wxtModuleDir,
'dist/virtual/mock-browser.mjs',
),
// TODO: Resolve this virtual module to some file with
// `export default {}` instead of this hack of using another file with
// a default export.
'virtual:app-config': resolve(
wxt.config.wxtModuleDir,
'dist/virtual/mock-browser.mjs',
),
},
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
// respect the custom transform function when using it's native bun option.
experimentalBun: false,
// List of extensions to transform with esbuild
extensions: [
'.ts',
'.cts',
'.mts',
'.tsx',
'.js',
'.cjs',
'.mjs',
'.jsx',
],
transform(opts) {
const isEntrypoint = opts.filename === normalPath;
return transformSync(
// Use modified source code for entrypoints
isEntrypoint ? code : opts.source,
getEsbuildOptions(opts),
);
},
},
);
try {
const res = await jiti(path);
return res.default;
} catch (err) {
const filePath = relative(wxt.config.root, path);
if (err instanceof ReferenceError) {
// "XXX is not defined" - usually due to WXT removing imports
const variableName = err.message.replace(' is not defined', '');
throw Error(
`${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/go-further/entrypoint-side-effects.html`,
{ cause: err },
);
} else {
wxt.logger.error(err);
throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
}
}
}
function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
const isJsx = opts.filename?.endsWith('x');
return {
format: 'cjs',
loader: isJsx ? 'tsx' : 'ts',
define: {
'import.meta.env.ENTRYPOINT': '"build"',
},
...(isJsx
? {
// `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while
// grabbing the entrypoint's options.
jsxFactory: 'h',
jsxFragment: 'Fragment',
}
: undefined),
};
}
@@ -2,6 +2,5 @@ export * from './build-entrypoints';
export * from './detect-dev-changes';
export * from './find-entrypoints';
export * from './group-entrypoints';
export * from './import-entrypoint';
export * from './internal-build';
export * from './rebuild';
-11
View File
@@ -34,14 +34,3 @@ export function removeImportStatements(text: string): string {
'',
);
}
/**
* Removes imports, ensuring that some of WXT's client imports are present, so that entrypoints can be parsed if auto-imports are disabled.
*/
export function removeProjectImportStatements(text: string): string {
const noImports = removeImportStatements(text);
return `import { defineUnlistedScript, defineContentScript, defineBackground } from 'wxt/sandbox';
${noImports}`;
}
@@ -296,7 +296,6 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
transformManifest: () => {},
userConfigMetadata: {},
alias: {},
entrypointLoader: 'vite-node',
experimental: {},
dev: {
reloadCommand: 'Alt+R',
-15
View File
@@ -326,20 +326,6 @@ export interface InlineConfig {
* }
*/
alias?: Record<string, string>;
/**
* @deprecated Will be removed in v0.20.0, please migrate to using `vite-node`, the new default.
*
* Method used to import entrypoint files during the build process to extract their options.
*
* - `"vite-node"` (default as of 0.19.0): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
* - `"jiti"`: Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function
*
* @see {@link https://wxt.dev/guide/go-further/entrypoint-importers.html|Entrypoint Importers}
*
* @default "vite-node"
* @since 0.19.0
*/
entrypointLoader?: 'vite-node' | 'jiti';
/**
* Experimental settings - use with caution.
*/
@@ -1368,7 +1354,6 @@ export interface ResolvedConfig {
* Import aliases to absolute paths.
*/
alias: Record<string, string>;
entrypointLoader: 'vite-node' | 'jiti';
experimental: {};
dev: {
/** Only defined during dev command */
-3
View File
@@ -399,9 +399,6 @@ importers:
is-wsl:
specifier: ^3.1.0
version: 3.1.0
jiti:
specifier: ^1.21.6
version: 1.21.6
json5:
specifier: ^2.2.3
version: 2.2.3