feat: Generate type declarations and config for project types and auto-imports
This commit is contained in:
@@ -11,9 +11,15 @@ cyfer, wavex, webext, aura, iron, reyna, aggron, wxt, yiga
|
||||
- [x] Supports all browsers
|
||||
- [x] MV2 & MV3 support
|
||||
- [x] Directory based entrypoints
|
||||
- [ ] Great DX: auto-imports, TypeScript, HMR, and fast reload for content scripts
|
||||
- [ ] ~~_Infer permissions from code_~~
|
||||
- [ ] Open browser during development with extension installed
|
||||
- [ ] Great DX
|
||||
- [x] Auto-imports
|
||||
- [x] TypeScript,
|
||||
- [ ] HMR for HTML pages
|
||||
- [ ] auto-reload for background and content scripts
|
||||
- [ ] Automated publishing
|
||||
- [x] Download and bundle remote dependencies
|
||||
- [ ] Supports all major frontend frameworks (Vue, React, Svelte)
|
||||
- [x] Supports all major frontend frameworks (Vue, React, Svelte)
|
||||
- [ ] Bundle analysis
|
||||
- [ ] Project bootstrap
|
||||
- [ ] ~~_Infer permissions from code_~~ Maybe in the future...
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { defineBackgroundScript } from 'exvite/client';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
export default defineBackgroundScript(() => {
|
||||
console.log(browser.runtime.id);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { defineContentScript, mountContentScriptUi } from 'exvite/client';
|
||||
import browser from 'webextension-polyfill';
|
||||
import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"exclude": [".output"]
|
||||
"extends": ["../tsconfig.base.json", "./.exvite/tsconfig.json"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as exvite from '../..';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export async function build(root: any, { mode, config }: any) {
|
||||
export const build = defineCommand(async (root: any, { mode, config }: any) => {
|
||||
await exvite.build({
|
||||
mode,
|
||||
root,
|
||||
configFile: config,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as exvite from '../..';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export async function dev(root: any, { mode, config }: any) {
|
||||
export const dev = defineCommand(async (root: any, { mode, config }: any) => {
|
||||
await exvite.createServer({
|
||||
mode,
|
||||
root,
|
||||
configFile: config,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export async function init(directory: any) {
|
||||
export const init = defineCommand(async (directory: any) => {
|
||||
consola.warn('exvite init: Not implemented');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { consola } from 'consola';
|
||||
import { getInternalConfig } from '../../utils/getInternalConfig';
|
||||
import { findEntrypoints } from '../../utils/findEntrypoints';
|
||||
import { generateTypesDir } from '../../utils/generateTypesDir';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export async function prepare(root: any, { mode, config }: any) {
|
||||
consola.warn('exvite prepare: Not implemented');
|
||||
}
|
||||
export const prepare = defineCommand(
|
||||
async (root: any, { mode, config }: any) => {
|
||||
const internalConfig = await getInternalConfig(
|
||||
{ root, mode, configFile: config },
|
||||
'build',
|
||||
);
|
||||
internalConfig.logger.info('Generating types...');
|
||||
|
||||
const entrypoints = await findEntrypoints(internalConfig);
|
||||
await generateTypesDir(entrypoints, internalConfig);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export async function publish(root: any, { mode, config }: any) {
|
||||
export const publish = defineCommand(async (root: any, { config }: any) => {
|
||||
consola.warn('exvite publish: Not implemented');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { consola } from 'consola';
|
||||
import { printHeader } from './printHeader';
|
||||
|
||||
export function defineCommand(cb: (...args: any[]) => void | Promise<void>) {
|
||||
return async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
printHeader();
|
||||
|
||||
await cb(...args);
|
||||
|
||||
consola.success(`Done in ${Date.now() - startTime} ms`);
|
||||
} catch (err) {
|
||||
consola.fail(`Command failed after ${Date.now() - startTime} ms`, err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import pc from 'picocolors';
|
||||
import { version } from '../..';
|
||||
import { consola } from 'consola';
|
||||
|
||||
export function printHeader() {
|
||||
consola.log(`\n${pc.dim('Exvite')} ${pc.dim(pc.bold(version))}`);
|
||||
}
|
||||
+4
-1
@@ -5,7 +5,9 @@ import { buildEntrypoints } from './utils/buildEntrypoints';
|
||||
import { generateMainfest, writeManifest } from './utils/manifest';
|
||||
import { printBuildSummary } from './utils/printBuildSummary';
|
||||
import fs from 'fs-extra';
|
||||
import { generateTypesDir } from './utils/generateTypesDir';
|
||||
|
||||
export { version } from '../package.json';
|
||||
export * from './types/external';
|
||||
export * from './utils/defineConfig';
|
||||
|
||||
@@ -18,12 +20,13 @@ export async function build(config: InlineConfig): Promise<BuildOutput> {
|
||||
await fs.ensureDir(internalConfig.outDir);
|
||||
|
||||
const entrypoints = await findEntrypoints(internalConfig);
|
||||
await generateTypesDir(entrypoints, internalConfig);
|
||||
const output = await buildEntrypoints(entrypoints, internalConfig);
|
||||
|
||||
const manifest = await generateMainfest(entrypoints, output, internalConfig);
|
||||
await writeManifest(manifest, internalConfig);
|
||||
|
||||
printBuildSummary(output, internalConfig);
|
||||
await printBuildSummary(output, internalConfig);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as vite from 'vite';
|
||||
import { UnimportPluginOptions } from 'unimport/unplugin';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
|
||||
export interface InlineConfig {
|
||||
root?: string;
|
||||
@@ -13,7 +13,7 @@ export interface InlineConfig {
|
||||
edge?: string;
|
||||
};
|
||||
mode?: string;
|
||||
imports?: Partial<UnimportPluginOptions>;
|
||||
imports?: Partial<UnimportOptions>;
|
||||
browser?: TargetBrowser;
|
||||
manifestVersion?: TargetManifestVersion;
|
||||
logger?: Logger;
|
||||
|
||||
@@ -6,11 +6,15 @@ import {
|
||||
TargetManifestVersion,
|
||||
UserManifest,
|
||||
} from './external';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
|
||||
export interface InternalConfig {
|
||||
root: string;
|
||||
srcDir: string;
|
||||
exviteDir: string;
|
||||
typesDir: string;
|
||||
entrypointsDir: string;
|
||||
outBaseDir: string;
|
||||
outDir: string;
|
||||
storeIds: {
|
||||
chrome?: string;
|
||||
@@ -22,6 +26,7 @@ export interface InternalConfig {
|
||||
browser: TargetBrowser;
|
||||
manifestVersion: TargetManifestVersion;
|
||||
logger: Logger;
|
||||
imports: Partial<UnimportOptions>;
|
||||
vite: vite.InlineConfig;
|
||||
manifest: UserManifest;
|
||||
fsCache: FsCache;
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { UnimportOptions, createUnimport } from 'unimport';
|
||||
import { Entrypoint, InternalConfig } from '../types';
|
||||
import * as vite from 'vite';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from './entrypoints';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
*/
|
||||
export async function generateTypesDir(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<void> {
|
||||
await fs.ensureDir(config.typesDir);
|
||||
|
||||
const references: string[] = [];
|
||||
references.push(await writeImportsDeclarationFile(config));
|
||||
references.push(await writePathsDeclarationFile(entrypoints, config));
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references, config);
|
||||
await writeTsConfigFile(mainReference, config);
|
||||
}
|
||||
|
||||
async function writeImportsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'imports.d.ts');
|
||||
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [
|
||||
{ name: '*', as: 'browser', from: 'webextension-polyfill' },
|
||||
{ name: 'defineConfig', from: 'exvite' },
|
||||
],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
const merged = vite.mergeConfig(
|
||||
defaultOptions,
|
||||
config.imports,
|
||||
) as Partial<UnimportOptions>;
|
||||
|
||||
const unimport = createUnimport(merged);
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
await unimport.scanImportsFromFile(
|
||||
resolve('node_modules/exvite/dist/client/index.js'),
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
['// Generated by exvite', await unimport.generateTypeDeclarations()].join(
|
||||
'\n',
|
||||
) + '\n',
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writePathsDeclarationFile(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'paths.d.ts');
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by exvite',
|
||||
'type EntrypointPath =',
|
||||
...entrypoints
|
||||
.map((entry) => {
|
||||
const path = getEntrypointBundlePath(
|
||||
entry,
|
||||
config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
);
|
||||
return ` | "/${path}"`;
|
||||
})
|
||||
.sort(),
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeMainDeclarationFile(
|
||||
references: string[],
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const dir = config.exviteDir;
|
||||
const filePath = resolve(dir, 'exvite.d.ts');
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by exvite',
|
||||
...references.map(
|
||||
(ref) => `/// <reference types="./${relative(dir, ref)}" />`,
|
||||
),
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeTsConfigFile(
|
||||
mainReference: string,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const dir = config.exviteDir;
|
||||
await fs.writeFile(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
|
||||
/* Completeness */
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"${relative(dir, config.root)}/**/*",
|
||||
"./${relative(dir, mainReference)}"
|
||||
],
|
||||
"exclude": ["${relative(dir, config.outBaseDir)}"]
|
||||
}`,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { InlineConfig, InternalConfig, UserConfig } from '../types';
|
||||
import path from 'node:path';
|
||||
import path, { resolve } from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { consola } from 'consola';
|
||||
import { importTsFile } from './importTsFile';
|
||||
@@ -24,30 +24,33 @@ export async function getInternalConfig(
|
||||
);
|
||||
const mode =
|
||||
config.mode ?? (command === 'build' ? 'production' : 'development');
|
||||
const exviteDir = resolve(srcDir, '.exvite');
|
||||
const typesDir = resolve(exviteDir, 'types');
|
||||
const browser = config.browser ?? 'chromium';
|
||||
const manifestVersion =
|
||||
config.manifestVersion ?? (browser === 'chromium' ? 3 : 2);
|
||||
const outDir = path.resolve(
|
||||
root,
|
||||
'.output',
|
||||
`${browser}-mv${manifestVersion}`,
|
||||
);
|
||||
const outBaseDir = path.resolve(root, '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
const logger = config.logger ?? consola;
|
||||
|
||||
const baseConfig: InternalConfig = {
|
||||
root,
|
||||
srcDir,
|
||||
entrypointsDir,
|
||||
exviteDir,
|
||||
typesDir,
|
||||
outDir,
|
||||
outBaseDir,
|
||||
storeIds: config.storeIds ?? {},
|
||||
browser,
|
||||
manifestVersion,
|
||||
mode,
|
||||
command,
|
||||
outDir,
|
||||
logger,
|
||||
vite: config.vite ?? {},
|
||||
manifest: config.manifest ?? {},
|
||||
fsCache: createFsCache(srcDir),
|
||||
imports: config.imports ?? {},
|
||||
};
|
||||
|
||||
// Load user config from file
|
||||
@@ -66,13 +69,13 @@ export async function getInternalConfig(
|
||||
// Customize the default vite config
|
||||
merged.vite.root = root;
|
||||
merged.vite.configFile = false;
|
||||
merged.vite.logLevel = 'silent';
|
||||
|
||||
merged.vite.build ??= {};
|
||||
merged.vite.build.outDir = outDir;
|
||||
merged.vite.build.emptyOutDir = false;
|
||||
merged.vite.logLevel = 'silent';
|
||||
merged.vite.plugins ??= [];
|
||||
|
||||
merged.vite.plugins.push(plugins.unimport(srcDir, userConfig.imports));
|
||||
merged.vite.plugins ??= [];
|
||||
merged.vite.plugins.push(plugins.download(merged));
|
||||
|
||||
return merged;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { consola } from 'consola';
|
||||
import createJITI from 'jiti';
|
||||
import transform from 'jiti/dist/babel';
|
||||
import { resolve } from 'path';
|
||||
import { scanExports } from 'unimport';
|
||||
|
||||
export async function importTsFile<T>(path: string): Promise<T> {
|
||||
const clientImports = await scanExports(
|
||||
resolve('node_modules/exvite/dist/client/index.js'),
|
||||
);
|
||||
const jiti = createJITI(__filename, {
|
||||
alias: {
|
||||
'webextension-polyfill': 'exvite',
|
||||
'*.css': 'exvite',
|
||||
},
|
||||
cache: false,
|
||||
esmResolve: true,
|
||||
interopDefault: true,
|
||||
@@ -15,6 +16,27 @@ export async function importTsFile<T>(path: string): Promise<T> {
|
||||
transform(opts) {
|
||||
// Remove CSS imports from the source code - Jiti can't handle them.
|
||||
opts.source = opts.source.replace(/^import ['"].*\.css['"];?$/gm, '');
|
||||
opts.source = opts.source.replace(
|
||||
/^import\s+.*\s+from ['"]webextension-polyfill['"];?$/gm,
|
||||
'',
|
||||
);
|
||||
|
||||
// Append any exvite/client functions so babel doesn't complain about undefined variables
|
||||
if (opts.filename === path) {
|
||||
// TODO: Only append import if it isn't already imported
|
||||
const imports =
|
||||
clientImports
|
||||
.map((i) => `import { ${i.name} } from "${i.from}";`)
|
||||
.join('\n') + '\n';
|
||||
opts.source = imports + opts.source;
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// `---\n${path}\n---\n${opts.source}---\n`,
|
||||
// imports,
|
||||
// '\n\n\n\n',
|
||||
// );
|
||||
|
||||
// Call the default babel transformer with our modified source code
|
||||
return transform(opts);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export * from './download';
|
||||
export * from './unimport';
|
||||
export * from './multipageMove';
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Unimport, { UnimportPluginOptions } from 'unimport/unplugin';
|
||||
import { UserConfig } from '../types';
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Apply defaults and returns an instance of the unimport plugin.
|
||||
*
|
||||
* @param root Absolute path to the vite root
|
||||
* @param srcDir Absolute path to the source directory
|
||||
* @param imports Custom import settings defined by the user
|
||||
*/
|
||||
export function unimport(srcDir: string, imports: UserConfig['imports']) {
|
||||
const declartionFile = path.resolve(srcDir, '.exvite/types/imports.d.ts');
|
||||
const declarationDir = path.dirname(declartionFile);
|
||||
fs.ensureDirSync(declarationDir);
|
||||
|
||||
const defaultOptions: UnimportPluginOptions = {
|
||||
include: srcDir,
|
||||
exclude: [],
|
||||
addons: [],
|
||||
debugLog: () => {},
|
||||
dts: declartionFile,
|
||||
imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }],
|
||||
presets: [
|
||||
// Scan for exported functions from the client package
|
||||
{ package: 'exvite/client' },
|
||||
],
|
||||
virtualImports: [],
|
||||
warn: () => {},
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
const unimportConfig = vite.mergeConfig(
|
||||
defaultOptions,
|
||||
imports ?? {},
|
||||
) as UnimportPluginOptions;
|
||||
const unimport: typeof Unimport.vite =
|
||||
// @ts-expect-error: esm availabe within the default object?
|
||||
Unimport.vite ?? Unimport.default?.vite;
|
||||
|
||||
return unimport(unimportConfig);
|
||||
}
|
||||
Reference in New Issue
Block a user