feat: Generate type declarations and config for project types and auto-imports

This commit is contained in:
Aaron Klinker
2023-06-23 13:12:51 -05:00
parent 523c7df519
commit 21debadcce
19 changed files with 249 additions and 83 deletions
+9 -3
View File
@@ -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...
-3
View File
@@ -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
View File
@@ -1,4 +1,3 @@
{
"extends": "../tsconfig.base.json",
"exclude": [".output"]
"extends": ["../tsconfig.base.json", "./.exvite/tsconfig.json"]
}
+3 -2
View File
@@ -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,
});
}
});
+3 -2
View File
@@ -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,
});
}
});
+3 -2
View File
@@ -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');
}
});
+16 -4
View File
@@ -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);
},
);
+3 -2
View File
@@ -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');
}
});
+18
View File
@@ -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);
}
};
}
+7
View File
@@ -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
View File
@@ -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;
}
+2 -2
View File
@@ -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;
+5
View File
@@ -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;
+136
View File
@@ -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)}"]
}`,
);
}
+13 -10
View File
@@ -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;
+26 -4
View File
@@ -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
View File
@@ -1,3 +1,2 @@
export * from './download';
export * from './unimport';
export * from './multipageMove';
-43
View File
@@ -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);
}