diff --git a/.eslintrc.js b/.eslintrc.js index 5f45058..3a77ae7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,10 @@ module.exports = { extends: ['kagura/typescript'], rules: { - '@typescript-eslint/space-infix-ops': 'error' + '@typescript-eslint/space-infix-ops': 'error', + '@typescript-eslint/type-annotation-spacing': ['error', { + before: false, + after: true + }] } } diff --git a/README.md b/README.md index 9194f9a..ce0ef62 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,99 @@ export default defineConfig({ }) ``` -### Options +## Options -| params | type | default | description | -| ---------------------- | --------------------------------------------- | ----------------- | -------------------------------------------------------------- | -| `include` | `string \| RegExp \| Array` | `/\.(mjs|js|ts|vue|jsx|tsx)(\?.*|)$/` | Include all assets matching any of these conditions. | -| `exclude` | `string \| RegExp \| Array` | `-` | Exclude all assets matching any of these conditions. | -| `url` | `string` | `jsdelivr` | cdn source url | -| `modules` | `Array\| Array` | `[]` | modules to be processed | -| `transform` | `()=>InjectVisitor` | `-` | Transform can replace the capture result. and rewrite them. | -| `logLevel` | `'slient'|'warn'` | `warn` | Adjust console output verbosity | +- [`include`](#include) +- [`exclude`](#exclude) +- [`modules`](#modules) +- [`url`](#url) +- [`transform`](#transform) +- [`logLevel`](#logLevel) +### include + +Type: + +```ts + +type FilterPattern = ReadonlyArray | string | RegExp | null + +``` +Default: `/\.(mjs|js|ts|vue|jsx|tsx)(\?.*|)$/` + +Include all assets matching any of these conditions. + +### exclude + +Type: + +```ts + +type FilterPattern = ReadonlyArray | string | RegExp | null + +``` +Default: `undefined` + +Exclude all assets matching any of these conditions. + +### modules + +Type: + +```ts + +interface TrackModule { + name: string + global?: string + spare?: Array | string + relativeModule?: string +} + +interface IModule extends TrackModule{ + [prop: string]: any +} + +type Modules = Array + +``` +Default: `[]` + +Modules to be processed. Details see [Modules](./docs/Modules.md). + +### url + +Type: string + +Default: `https://cdn.jsdelivr.net/npm/` + +CDN url. Details see [URL](./docs/URL.md). + +### transform + +Type: + +```ts + +interface InjectVisitor { + script?: (node: ScriptNode)=> void + link?: (node: LinkNode)=> void +} + +type Trasnform = ()=> InjectVisitor + +``` + +Default: `undefined` + +Transform is a overwrite. + +### logLevel + +Type: `slient` | `warn` + +Default: `warn` + +Adjust console output verbosity ### Acknowledgements diff --git a/docs/Modules.md b/docs/Modules.md new file mode 100644 index 0000000..3284726 --- /dev/null +++ b/docs/Modules.md @@ -0,0 +1,15 @@ +# Modules + +Module accept `string[]` or `IModule[]`. + +## String + +When you pass `string[]` it will be transform as `IModule[]` + + +## IModule + +- name (package entry name) +- global (pacakge global name) +- spare (links that need to be bind to the page) +- relativeModule (If scanner error, try it) diff --git a/docs/URL.md b/docs/URL.md new file mode 100644 index 0000000..299bddc --- /dev/null +++ b/docs/URL.md @@ -0,0 +1,13 @@ +# URL + +`vite-plugin-cdn2` provide two preset source. `jsdelivr` and `unpkg`. you can using it like this way. + +```js +import { cdn } from 'vite-plugin-cdn2' +import { unpkg } from 'vite-plugin-cdn2/url' + +cdn({url:unpkg,modules:['vue']}) + +``` + +Then all of source will bind with unpkg. \ No newline at end of file diff --git a/example/vite.config.js b/example/vite.config.js index e7272d6..751e037 100644 --- a/example/vite.config.js +++ b/example/vite.config.js @@ -14,7 +14,7 @@ export default defineConfig(({ command }) => { Components({ resolvers: [VarletUIResolver()] }), { ...cdn({ - modules: ['vue', 'vue-demi', 'pinia', '@varlet/ui', 'axios'] + modules: ['vue', 'vue-demi', 'pinia', '@varlet/ui', 'axios', 'react'] }), apply: command }, diff --git a/src/code-gen.ts b/src/code-gen.ts index 4400151..eb28854 100644 --- a/src/code-gen.ts +++ b/src/code-gen.ts @@ -5,17 +5,17 @@ import type { NodePath } from '@babel/core' import type { ModuleInfo } from './interface' -function isTopLevelCalled(p:NodePath) { +function isTopLevelCalled(p: NodePath) { return t.isProgram(p.parent) || t.isExportDefaultDeclaration(p.parent) || t.isExportNamedDeclaration(p.parent) } export class CodeGen { - private dependencies:Map - injectDependencies(dependencies:Map) { + private dependencies: Map + injectDependencies(dependencies: Map) { this.dependencies = dependencies } - filter(code:string, id:string) { + filter(code: string, id: string) { const { output } = esModuleLexer({ input: [{ filename: id, code }] }) if (!len(output)) return false const { imports } = output[0] @@ -27,7 +27,7 @@ export class CodeGen { return false } - private scanImportDeclarationAndRecord(path:NodePath, references:Map) { + private scanImportDeclarationAndRecord(path: NodePath, references: Map) { const { global: globalName } = this.dependencies.get(path.node.source.value) for (const specifier of path.node.specifiers) { switch (specifier.type) { @@ -43,14 +43,14 @@ export class CodeGen { } } - private overWriteExportNamedDeclaration(path:NodePath, references:Map) { - const nodes:Array = [] - const natives:Array = [] + private overWriteExportNamedDeclaration(path: NodePath, references: Map) { + const nodes: Array = [] + const natives: Array = [] const hasBindings = path.node.source const globalName = hasBindings ? this.dependencies.get(path.node.source.value).global : '' - const bindings:Set = hasBindings ? this.dependencies.get(path.node.source.value).bindings : new Set() + const bindings: Set = hasBindings ? this.dependencies.get(path.node.source.value).bindings : new Set() - const scanNamedExportsWithSource = (l:t.Identifier, e:t.Identifier, specifier:t.ExportSpecifier) => { + const scanNamedExportsWithSource = (l: t.Identifier, e: t.Identifier, specifier: t.ExportSpecifier) => { if (!bindings.size) return if (l.name === 'default' && l.name !== e.name) { const memberExpression = (p) => t.memberExpression(t.identifier(globalName), t.identifier(p)) @@ -82,7 +82,7 @@ export class CodeGen { natives.push(specifier) } - const scanNamedExportsWithoutSource = (l:t.Identifier, e:t.Identifier, specifier:t.ExportSpecifier) => { + const scanNamedExportsWithoutSource = (l: t.Identifier, e: t.Identifier, specifier: t.ExportSpecifier) => { if (references.has(l.name)) { const [o, p] = references.get(l.name).split('.') if (e.name === 'default') { @@ -130,8 +130,8 @@ export class CodeGen { // export { A , B } from 'module' // export * as default from 'module' // export * as xx from 'module' - const variableDeclaratorNodes = nodes.filter((node):node is t.VariableDeclarator => node.type === 'VariableDeclarator') - const objectOrMemberExpression = nodes.filter((node):node is t.ObjectExpression | t.MemberExpression => node.type !== 'VariableDeclarator') + const variableDeclaratorNodes = nodes.filter((node): node is t.VariableDeclarator => node.type === 'VariableDeclarator') + const objectOrMemberExpression = nodes.filter((node): node is t.ObjectExpression | t.MemberExpression => node.type !== 'VariableDeclarator') if (len(objectOrMemberExpression)) { const exportDefaultDeclaration = t.exportDefaultDeclaration(objectOrMemberExpression[0]) if (len(variableDeclaratorNodes) || len(natives)) { @@ -152,8 +152,8 @@ export class CodeGen { } } - private overWriteExportAllDeclaration(path:NodePath) { - const nodes:Array = [] + private overWriteExportAllDeclaration(path: NodePath) { + const nodes: Array = [] const { bindings } = this.dependencies.get(path.node.source.value) bindings.forEach((binding) => { const identifier = t.identifier(binding) @@ -203,10 +203,10 @@ export class CodeGen { } } - async transform(code:string) { + async transform(code: string) { const ast = await babelParse(code, { babelrc: false, configFile: false }) - const references:Map = new Map() - const declarations:Map> = new Map() + const references: Map = new Map() + const declarations: Map> = new Map() traverse(ast, { ImportDeclaration: { enter: (path) => { diff --git a/src/index.ts b/src/index.ts index 0137686..378141d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,7 @@ function cdn(opts: CDNPluginOptions = {}): Plugin { if (logLevel === 'warn') { scanner.failedModules.forEach((errorMessage, name) => config.logger.error(`vite-plugin-cdn2: ${name} ${errorMessage ? errorMessage : 'resolved failed.Please check it.'}`)) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error) { config.logger.error(error) } }, diff --git a/src/inject.ts b/src/inject.ts index 8821336..2416c52 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -2,19 +2,32 @@ import { URL } from 'url' import { Window } from 'happy-dom' import { uniq } from './shared' -import type { IIFEModuleInfo, CDNPluginOptions, ScriptNode, LinkNode } from './interface' +import type { CDNPluginOptions, ScriptNode, LinkNode, ModuleInfo, URLFunction } from './interface' function isScript(url: string) { return url.split('.').pop() === 'js' ? 'script' : 'link' } -function makeURL(moduleMeta: IIFEModuleInfo, baseURL:string) { - const { version, name: packageName, relativeModule } = moduleMeta +interface Options { + extra: ModuleInfo, + baseURL: string +} + +// [baseURL][version][name] +function replaceURL(p: string, url: string | URLFunction, options: Options) { + const template = typeof url === 'function' ? url(p, options.extra) : url + return template.replace(/\[version\]/, options.extra.version).replace(/\[baseURL\]/, options.baseURL).replace(/\[name\]/, options.extra.name) +} + +function makeURL(moduleMeta: ModuleInfo, baseURL: string) { + const { version, name: packageName, relativeModule, url: userURL } = moduleMeta if (!baseURL) return - return new URL(`${packageName}@${version}/${relativeModule}`, baseURL).href + const u = new URL(`${packageName}@${version}/${relativeModule}`, baseURL).href + if (userURL) return replaceURL(u, userURL, { extra: moduleMeta, baseURL }) + return u } -function makeNode(moduleInfo:IIFEModuleInfo):ScriptNode | LinkNode { +function makeNode(moduleInfo: ModuleInfo): ScriptNode | LinkNode { return { tag: 'link', url: new Set(), @@ -24,10 +37,9 @@ function makeNode(moduleInfo:IIFEModuleInfo):ScriptNode | LinkNode { } class InjectScript { - private modules:Map - + private modules: Map private window: Window - constructor(modules: Map, url: string) { + constructor(modules: Map, url: string) { this.modules = this.prepareSource(modules, url) this.window = new Window() } @@ -73,11 +85,11 @@ class InjectScript { return document.body.innerHTML } - private prepareSource(modules: Map, baseURL: string) { - const container:Map = new Map() + private prepareSource(modules: Map, baseURL: string) { + const container: Map = new Map() - const traverseModule = (moduleMeta: IIFEModuleInfo, moduleName: string) => { - const { spare } = moduleMeta + const traverseModule = (moduleMeta: ModuleInfo, moduleName: string) => { + const { spare } = moduleMeta if (!spare) return if (Array.isArray(spare)) { for (const s of uniq(spare)) { @@ -101,6 +113,7 @@ class InjectScript { modules.forEach((meta, moduleName) => { const node = makeNode(meta) const url = makeURL(meta, baseURL) + if (!url) return node.url.add(url) node.tag = isScript(url) const mark = `__${moduleName}__${node.tag}__` @@ -112,7 +125,7 @@ class InjectScript { } export function createInjectScript( - dependModules: Map, + dependModules: Map, url: string ) { return new InjectScript(dependModules, url) diff --git a/src/interface.ts b/src/interface.ts index d49a7a0..4dc468f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,33 +1,28 @@ -import type { Plugin } from 'vite' import type { FilterPattern } from '@rollup/pluginutils' - -// eslint-disable-next-line @typescript-eslint/ban-types -type ObjectHook = T | ({ handler: T; order?: 'pre' | 'post' | null } & O); - - -type HookHandler = T extends ObjectHook ? H : T - - -export type RollupTransformHookContext = ThisParameterType>> - - export interface TrackModule { name: string global?: string spare?: Array | string + relativeModule?: string } export interface IIFEModuleInfo extends TrackModule { version: string - relativeModule: string unpkg?: string jsdelivr?: string } +export type URLFunction = (p: string, extra: IIFEModuleInfo)=> string + export interface ModuleInfo extends IIFEModuleInfo{ - bindings:Set - code?:string + bindings: Set + code?: string + url?: string | URLFunction +} + +export interface IModule extends TrackModule{ + [prop: string]: any } export interface Serialization { @@ -35,7 +30,7 @@ export interface Serialization { type?: string name: string tag: 'link' | 'script' - extra:Record + extra: Record } @@ -75,16 +70,16 @@ export type LinkNode = LinkAttrobites & } export interface InjectVisitor { - script?: (node: ScriptNode) => void - link?: (node: LinkNode) => void + script?: (node: ScriptNode)=> void + link?: (node: LinkNode)=> void } export interface CDNPluginOptions { - modules?: Array + modules?: Array url?: string - transform?: () => InjectVisitor + transform?: ()=> InjectVisitor include?: FilterPattern exclude?: FilterPattern - logLevel?:'slient' | 'warn' + logLevel?: 'slient' | 'warn' } diff --git a/src/scanner.ts b/src/scanner.ts index a0e23f3..47b8ec3 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -3,7 +3,7 @@ import worker_threads from 'worker_threads' import { createConcurrentQueue, createVM, MAX_CONCURRENT } from './vm' import { is, len, lookup, uniq } from './shared' import type { MessagePort } from 'worker_threads' -import type { TrackModule, IIFEModuleInfo, ModuleInfo } from './interface' +import type { TrackModule, IIFEModuleInfo, ModuleInfo, IModule } from './interface' // This file is a simply dependencies scanner. // We won't throw any error unless it's an internal thread error(such as pid not equal) @@ -13,7 +13,7 @@ import type { TrackModule, IIFEModuleInfo, ModuleInfo } from './interface' // prevent missing parse. interface WorkerData { - scannerModule: TrackModule[] + scannerModule: IModule[] workerPort: MessagePort internalThread: boolean } @@ -41,23 +41,25 @@ function createWorkerThreads(scannerModule: TrackModule[]) { }) }) } - return run() as Promise<{dependencies:Map, failedModules:Map}> + return run() as Promise<{dependencies: Map, failedModules: Map}> } async function tryResolveModule( - module: TrackModule, + module: IModule, dependenciesMap: Map, failedModules: Map ) { - const { name: moduleName, ...rest } = module + const { name: moduleName, relativeModule, ...rest } = module try { const modulePath = require.resolve(moduleName) const packageJsonPath = lookup(modulePath, 'package.json') const str = await fsp.readFile(packageJsonPath, 'utf8') - const packageJSON:IIFEModuleInfo = JSON.parse(str) + const packageJSON: IIFEModuleInfo = JSON.parse(str) const { version, name, unpkg, jsdelivr } = packageJSON - const meta:ModuleInfo = Object.create(null) - const iifeRelativePath = jsdelivr || unpkg + const meta: ModuleInfo = Object.create(null) + // Most of package has jsdelivr or unpkg field + // but a small part is not. so we should accept user define. + const iifeRelativePath = jsdelivr || unpkg || relativeModule if (!iifeRelativePath) throw new Error('try resolve file failed.') if (rest.global) { Object.assign(meta, { name, version, relativeModule: iifeRelativePath, ...rest }) @@ -80,9 +82,7 @@ async function tryResolveModule( const message = (() => { if (error instanceof Error) { if ('code' in error) { - if (error.code === 'MODULE_NOT_FOUND') { - return `can't find module '${moduleName}'.` - } + if (error.code === 'MODULE_NOT_FOUND') return 'can\'t find module.' } return error.message } @@ -98,7 +98,7 @@ function startAsyncThreads() { const { parentPort } = worker_threads const vm = createVM() const dependenciesMap: Map = new Map() - const failedModules:Map = new Map() + const failedModules: Map = new Map() parentPort?.on('message', (msg) => { (async () => { const { id } = msg @@ -116,7 +116,7 @@ function startAsyncThreads() { vm.bindings.set(name, rest) continue } - vm.run(code, rest, (err:Error) => { + vm.run(code, rest, (err: Error) => { failedModules.set(err.message, 'try resolve global name failed.') }) } @@ -135,10 +135,10 @@ if (worker_threads.workerData?.internalThread) { } class Scanner { - modules: Array + modules: Array dependencies: Map failedModules: Map - constructor(modules: Array) { + constructor(modules: Array) { this.modules = this.serialization(modules) this.dependencies = new Map() this.failedModules = new Map() @@ -151,7 +151,7 @@ class Scanner { this.failedModules = res.failedModules } - private serialization(modules: Array) { + private serialization(modules: Array) { is(Array.isArray(modules), 'vite-plugin-cdn2: option module must be array') return uniq(modules) .map((module) => { @@ -162,6 +162,6 @@ class Scanner { } } -export function createScanner(modules: Array) { +export function createScanner(modules: Array) { return new Scanner(modules) } diff --git a/src/vm.ts b/src/vm.ts index 11a3069..78c570f 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -5,14 +5,14 @@ import { len } from './shared' import type { ModuleInfo } from './interface' export function createVM() { - const bindings:Map = new Map() + const bindings: Map = new Map() const window = new Window() const context = vm.createContext({}) - let _meta:ModuleInfo = null + let _meta: ModuleInfo = null let id = 0 let callerId = 0 - const updateBindings = (name:string, meta:ModuleInfo) => { + const updateBindings = (name: string, meta: ModuleInfo) => { bindings.set(meta.name, { ...meta, global: name }) } const shadow = new Proxy(window, { @@ -24,7 +24,7 @@ export function createVM() { } }) - const run = (code: string, meta: ModuleInfo, handler:(err:Error)=>void) => { + const run = (code: string, meta: ModuleInfo, handler: (err: Error)=> void) => { _meta = meta try { vm.runInContext(code, context) @@ -64,7 +64,7 @@ export const MAX_CONCURRENT = (() => { class Queue { maxConcurrent: number - queue: Array<() => Promise> + queue: Array<()=> Promise> running: number errors: Error[] constructor(maxConcurrent: number) { @@ -74,7 +74,7 @@ class Queue { this.errors = [] } - enqueue(task: () => Promise) { + enqueue(task: ()=> Promise) { this.queue.push(task) this.run() }