Initial commit

Bootstraped:
- CLI script
- JS API
- Test extension
- Config files
This commit is contained in:
Aaron Klinker
2023-06-20 17:46:49 -05:00
parent ee7c2cd3af
commit 24a561e97e
27 changed files with 3461 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
* text=auto eol=lf
pnpm-lock.yaml linguist-generated
+13
View File
@@ -0,0 +1,13 @@
.DS_Store
.env
.env.*
.exvite
.idea
.output
*.log
/.vscode/
/docs/.vitepress/cache
coverage
dist
node_modules
TODOs.md
+2
View File
@@ -0,0 +1,2 @@
ignore-workspace-root-check=true
use-node-version=18.16.0
+3
View File
@@ -0,0 +1,3 @@
.output
coverage
dist
+3
View File
@@ -0,0 +1,3 @@
singleQuote: true
trailingComma: all
endOfLine: lf
+15
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'exvite';
export default defineConfig({
storeIds: {
chrome: '123',
},
});
+14
View File
@@ -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"
}
}
+65
View File
@@ -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"
}
}
+3051
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
packages:
- docs
- demo
+47
View File
@@ -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();
+9
View File
@@ -0,0 +1,9 @@
import * as exvite from '..';
export async function build(root: any, { mode, config }: any) {
await exvite.build({
mode,
root,
configFile: config,
});
}
+9
View File
@@ -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,
});
}
+5
View File
@@ -0,0 +1,5 @@
export * from './build';
export * from './dev';
export * from './init';
export * from './prepare';
export * from './publish';
+5
View File
@@ -0,0 +1,5 @@
import { consola } from 'consola';
export async function init(directory: any) {
consola.warn('exvite init: Not implemented');
}
+6
View File
@@ -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');
}
+5
View File
@@ -0,0 +1,5 @@
import { consola } from 'consola';
export async function publish(root: any, { mode, config }: any) {
consola.warn('exvite publish: Not implemented');
}
+24
View File
@@ -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');
}
+30
View File
@@ -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>);
+2
View File
@@ -0,0 +1,2 @@
export * from './external';
export * from './internal';
+14
View File
@@ -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;
}
+5
View File
@@ -0,0 +1,5 @@
import { UserConfigExport } from '../types';
export function defineConfig(config: UserConfigExport): UserConfigExport {
return config;
}
+78
View File
@@ -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;
}
+24
View File
@@ -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;
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
/* Completeness */
"skipLibCheck": true
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.base.json",
"exclude": ["dist", "demo"]
}