Initial commit
Bootstraped: - CLI script - JS API - Test extension - Config files
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
* text=auto eol=lf
|
||||
pnpm-lock.yaml linguist-generated
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
.exvite
|
||||
.idea
|
||||
.output
|
||||
*.log
|
||||
/.vscode/
|
||||
/docs/.vitepress/cache
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
TODOs.md
|
||||
@@ -0,0 +1,3 @@
|
||||
.output
|
||||
coverage
|
||||
dist
|
||||
@@ -0,0 +1,3 @@
|
||||
singleQuote: true
|
||||
trailingComma: all
|
||||
endOfLine: lf
|
||||
@@ -0,0 +1,15 @@
|
||||
# Exvite
|
||||
|
||||
> Next generation framework for building web extensions. Powered by [Vite](https://vitejs.dev/)
|
||||
|
||||
## Features
|
||||
|
||||
- [x] Supports all browsers
|
||||
- [ ] MV2 & MV3 support
|
||||
- [ ] Directory based entrypoints
|
||||
- [ ] Great DX: auto-imports, TypeScript, HMR, and fast reload for content scripts
|
||||
- [ ] Infer permissions from code
|
||||
- [ ] Automated publishing
|
||||
- [ ] Download and bundle remote dependencies
|
||||
- [ ] Supports all major frontend frameworks (Vue, React, Svelte)
|
||||
- [ ] Bundle analysis
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'exvite';
|
||||
|
||||
export default defineConfig({
|
||||
storeIds: {
|
||||
chrome: '123',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm -w build && exvite",
|
||||
"build": "pnpm -w build && exvite build",
|
||||
"prepare": "pnpm -w build && exvite prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"exvite": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"unimport": "^3.0.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "exvite",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "Modern JS framework for building web extensions",
|
||||
"keywords": [
|
||||
"vite",
|
||||
"chrome",
|
||||
"web",
|
||||
"extension",
|
||||
"browser",
|
||||
"bundler"
|
||||
],
|
||||
"author": "Aaron Klinker",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"bin": "dist/cli.cjs",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/cli.ts",
|
||||
"build": "run-s build:*",
|
||||
"build:js": "tsup src/index.ts --clean --sourcemap --dts --format esm,cjs",
|
||||
"build:cli": "tsup src/cli.ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --write .",
|
||||
"prepare": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"consola": "^3.1.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"unimport": "^3.0.8",
|
||||
"vite": "^4.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"tsup": "^7.0.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.3",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webextension-polyfill": ">=0.10.0"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm pretty-quick --staged"
|
||||
}
|
||||
}
|
||||
Generated
+3051
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- docs
|
||||
- demo
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import cac from 'cac';
|
||||
import { version } from '../package.json';
|
||||
import * as commands from './commands';
|
||||
|
||||
const cli = cac('exvite');
|
||||
cli.help();
|
||||
cli.version(version);
|
||||
|
||||
// DEV
|
||||
cli
|
||||
.command('[root]', 'start dev server')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-b, --browser <browser>', 'specify a browser', {
|
||||
type: ['chrome', 'firefox'],
|
||||
})
|
||||
.option('-m, --mode <mode>', 'set env mode', {
|
||||
default: 'development',
|
||||
})
|
||||
.action(commands.dev);
|
||||
|
||||
// BUILD
|
||||
cli
|
||||
.command('build [root]', 'build for production')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.action(commands.build);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode', {
|
||||
default: 'production',
|
||||
})
|
||||
.action(commands.prepare);
|
||||
|
||||
// PUBLISH
|
||||
cli.command('publish [root]', 'publish to stores').action(commands.publish);
|
||||
|
||||
// INIT
|
||||
cli
|
||||
.command('init [directory]', 'initialize a new project')
|
||||
.action(commands.init);
|
||||
|
||||
cli.parse();
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as exvite from '..';
|
||||
|
||||
export async function build(root: any, { mode, config }: any) {
|
||||
await exvite.build({
|
||||
mode,
|
||||
root,
|
||||
configFile: config,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as exvite from '..';
|
||||
|
||||
export async function dev(root: any, { mode, config }: any) {
|
||||
const server = await exvite.createServer({
|
||||
mode,
|
||||
root,
|
||||
configFile: config,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './build';
|
||||
export * from './dev';
|
||||
export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
@@ -0,0 +1,5 @@
|
||||
import { consola } from 'consola';
|
||||
|
||||
export async function init(directory: any) {
|
||||
consola.warn('exvite init: Not implemented');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { consola } from 'consola';
|
||||
import { createServer } from '..';
|
||||
|
||||
export async function prepare(root: any, { mode, config }: any) {
|
||||
consola.warn('exvite prepare: Not implemented');
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { consola } from 'consola';
|
||||
|
||||
export async function publish(root: any, { mode, config }: any) {
|
||||
consola.warn('exvite publish: Not implemented');
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { BuildOutput, ExviteDevServer, InlineConfig } from './types';
|
||||
import * as vite from 'vite';
|
||||
import { getInternalConfig } from './utils/getInternalConfig';
|
||||
|
||||
export * from './types/external';
|
||||
export * from './utils/defineConfig';
|
||||
|
||||
/**
|
||||
* Bundles the extension for production. Returns a promise of the build result.
|
||||
*/
|
||||
export async function build(config: InlineConfig): Promise<BuildOutput> {
|
||||
const internalConfig = await getInternalConfig(config, 'build');
|
||||
console.log({ internalConfig });
|
||||
throw Error('Not implemented');
|
||||
}
|
||||
|
||||
export async function createServer(
|
||||
config: InlineConfig,
|
||||
): Promise<ExviteDevServer> {
|
||||
const internalConfig = await getInternalConfig(config, 'serve');
|
||||
console.log({ internalConfig });
|
||||
const server = await vite.createServer(internalConfig.vite);
|
||||
throw Error('Not implemented');
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as vite from 'vite';
|
||||
import { UnimportPluginOptions } from 'unimport/unplugin';
|
||||
|
||||
export interface InlineConfig {
|
||||
root?: string;
|
||||
srcDir?: string;
|
||||
configFile?: string | false;
|
||||
storeIds?: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
mode?: string;
|
||||
imports?: Partial<UnimportPluginOptions>;
|
||||
browser?: TargetBrowser;
|
||||
vite?: Omit<vite.InlineConfig, 'root' | 'configFile' | 'mode'>;
|
||||
}
|
||||
|
||||
export interface BuildOutput {}
|
||||
|
||||
export interface ExviteDevServer {}
|
||||
|
||||
export type TargetBrowser = 'chromium' | 'firefox';
|
||||
|
||||
export type UserConfig = Omit<InlineConfig, 'configFile'>;
|
||||
|
||||
export type UserConfigExport =
|
||||
| UserConfig
|
||||
| ((info: { mode: string }) => UserConfig)
|
||||
| ((info: { mode: string }) => Promise<UserConfig>);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './external';
|
||||
export * from './internal';
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as vite from 'vite';
|
||||
import { TargetBrowser } from './external';
|
||||
|
||||
export interface InternalConfig {
|
||||
srcDir: string;
|
||||
storeIds: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
mode: string;
|
||||
browser: TargetBrowser;
|
||||
vite?: vite.InlineConfig;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserConfigExport } from '../types';
|
||||
|
||||
export function defineConfig(config: UserConfigExport): UserConfigExport {
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
UserConfig,
|
||||
UserConfigExport,
|
||||
} from '../types';
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import Unimport, { UnimportPluginOptions } from 'unimport/unplugin';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
* relative paths, and apply any defaults.
|
||||
*/
|
||||
export async function getInternalConfig(
|
||||
config: InlineConfig,
|
||||
command: 'build' | 'serve',
|
||||
): Promise<InternalConfig> {
|
||||
// Apply defaults to a base config
|
||||
const root = config.root ? path.resolve(config.root) : process.cwd();
|
||||
const srcDir =
|
||||
config.srcDir == null ? root : path.resolve(root, config.srcDir);
|
||||
const mode =
|
||||
config.mode ?? (command === 'build' ? 'production' : 'development');
|
||||
|
||||
const baseConfig: InternalConfig = {
|
||||
srcDir,
|
||||
storeIds: config.storeIds ?? {},
|
||||
browser: config.browser ?? 'chromium',
|
||||
mode,
|
||||
vite: config.vite,
|
||||
};
|
||||
|
||||
// Load user config from file
|
||||
let userConfig: UserConfig = {
|
||||
mode: config.mode,
|
||||
};
|
||||
if (config.configFile !== false) {
|
||||
const loadedConfig = await vite.loadConfigFromFile(
|
||||
{ command, mode },
|
||||
config.configFile ?? 'exvite.config.ts',
|
||||
);
|
||||
console.log('Loaded config:', loadedConfig);
|
||||
userConfig = loadedConfig?.config as any;
|
||||
}
|
||||
|
||||
// Merge inline and user configs
|
||||
const merged = vite.mergeConfig(baseConfig, userConfig) as InternalConfig;
|
||||
|
||||
// Customize the default vite config
|
||||
merged.vite ??= {};
|
||||
merged.vite.root = root;
|
||||
merged.vite.configFile = false;
|
||||
merged.vite.plugins ??= [];
|
||||
|
||||
const defaultOptions: UnimportPluginOptions = {
|
||||
include: srcDir,
|
||||
exclude: [],
|
||||
addons: [],
|
||||
debugLog: () => {},
|
||||
dts: path.resolve(root, '.exvite/types/imports.d.ts'),
|
||||
imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }],
|
||||
presets: [],
|
||||
virtualImports: [],
|
||||
warn: () => {},
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
const unimportConfig = vite.mergeConfig(
|
||||
defaultOptions,
|
||||
userConfig.imports ?? {},
|
||||
) as UnimportPluginOptions;
|
||||
const unimport: typeof Unimport.vite =
|
||||
// @ts-expect-error: esm availabe within the default object?
|
||||
Unimport.vite ?? Unimport.default?.vite;
|
||||
merged.vite.plugins.push(unimport(unimportConfig));
|
||||
|
||||
return merged;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import p from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Given a root and path, resolve absolute path to the file.
|
||||
*
|
||||
* - If the path is absolute, return the path.
|
||||
* - If the file exists at `path.resolve(root, ...path)`, return that path
|
||||
* - If the file exists at `path.resolve(...path)`, return that path
|
||||
* - If the file doesn't exist, return `undefined`.
|
||||
*
|
||||
* Any files in the root directory with the specified path take precidence over files in the CWD
|
||||
* with the same path.
|
||||
*/
|
||||
export async function resolveFile(
|
||||
root: string,
|
||||
path: string,
|
||||
): Promise<string | undefined> {
|
||||
const rootPath = p.resolve(root, path);
|
||||
if (await fs.exists(rootPath)) return rootPath;
|
||||
|
||||
const cwdPath = p.resolve(process.cwd(), path);
|
||||
if (await fs.exists(cwdPath)) return cwdPath;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
|
||||
/* Completeness */
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"exclude": ["dist", "demo"]
|
||||
}
|
||||
Reference in New Issue
Block a user