diff --git a/README.md b/README.md index ce0ef62..6b436af 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,10 @@ interface TrackModule { relativeModule?: string } +type ResolverFunction = (p: string, extra: IIFEModuleInfo)=> string + interface IModule extends TrackModule{ - [prop: string]: any + resolve: string | ResolverFunction } type Modules = Array diff --git a/__tests__/scanner.spec.ts b/__tests__/scanner.spec.ts index 212a275..36b407d 100644 --- a/__tests__/scanner.spec.ts +++ b/__tests__/scanner.spec.ts @@ -8,9 +8,16 @@ test('scanner dependencies', async (t) => { t.is(scanner.dependencies.has('vue'), true) }) -test('scanner failed', async (t) => { +test('scanner failed', async (t) => { const scanner = createScanner(['vue', 'react']) await scanner.scanAllDependencies() t.is(scanner.failedModules.has('react'), true) t.is(scanner.dependencies.has('vue'), true) }) + + +test('scanner with resolver', async (t) => { + const scanner = createScanner([{ name: 'vue', resolve: (p) => p }]) + await scanner.scanAllDependencies() + t.is(typeof scanner.dependencies.get('vue').resolve === 'function', true) +}) diff --git a/docs/Modules.md b/docs/Modules.md index 3284726..6ecee7f 100644 --- a/docs/Modules.md +++ b/docs/Modules.md @@ -13,3 +13,4 @@ When you pass `string[]` it will be transform as `IModule[]` - global (pacakge global name) - spare (links that need to be bind to the page) - relativeModule (If scanner error, try it) +- resolve (preset source convert) \ No newline at end of file diff --git a/src/inject.ts b/src/inject.ts index 2416c52..4319316 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -2,7 +2,7 @@ import { URL } from 'url' import { Window } from 'happy-dom' import { uniq } from './shared' -import type { CDNPluginOptions, ScriptNode, LinkNode, ModuleInfo, URLFunction } from './interface' +import type { CDNPluginOptions, ScriptNode, LinkNode, ModuleInfo, ResolverFunction } from './interface' function isScript(url: string) { return url.split('.').pop() === 'js' ? 'script' : 'link' @@ -14,16 +14,16 @@ interface Options { } // [baseURL][version][name] -function replaceURL(p: string, url: string | URLFunction, options: Options) { +function replaceURL(p: string, url: string | ResolverFunction, 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 + const { version, name: packageName, relativeModule, resolve } = moduleMeta if (!baseURL) return const u = new URL(`${packageName}@${version}/${relativeModule}`, baseURL).href - if (userURL) return replaceURL(u, userURL, { extra: moduleMeta, baseURL }) + if (resolve) return replaceURL(u, resolve, { extra: moduleMeta, baseURL }) return u } diff --git a/src/interface.ts b/src/interface.ts index 4dc468f..2e72045 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,16 +13,16 @@ export interface IIFEModuleInfo extends TrackModule { jsdelivr?: string } -export type URLFunction = (p: string, extra: IIFEModuleInfo)=> string +export type ResolverFunction = (p: string, extra: IIFEModuleInfo)=> string export interface ModuleInfo extends IIFEModuleInfo{ bindings: Set code?: string - url?: string | URLFunction + resolve?: string | ResolverFunction } export interface IModule extends TrackModule{ - [prop: string]: any + resolve?: string | ResolverFunction } export interface Serialization { diff --git a/src/scanner.ts b/src/scanner.ts index 47b8ec3..8129ceb 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -1,16 +1,14 @@ import fsp from 'fs/promises' import worker_threads from 'worker_threads' import { createConcurrentQueue, createVM, MAX_CONCURRENT } from './vm' -import { is, len, lookup, uniq } from './shared' +import { is, len, lookup } from './shared' import type { MessagePort } from 'worker_threads' -import type { TrackModule, IIFEModuleInfo, ModuleInfo, IModule } from './interface' +import type { TrackModule, IIFEModuleInfo, ModuleInfo, IModule, ResolverFunction } 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) // we consume all expection modules in the plugin itself. // Notice. This file don't handle any logic with script inject. -// If we implement option url for each module. It just a pre check to -// prevent missing parse. interface WorkerData { scannerModule: IModule[] @@ -18,11 +16,22 @@ interface WorkerData { internalThread: boolean } -function createWorkerThreads(scannerModule: TrackModule[]) { - const { port1: mainPort, port2: workerPort } = new worker_threads.MessageChannel() +interface ScannerModule { + modules: Array + resolvers: Record +} + +interface ThreadMessage { + bindings: Map, + failedModules: Map + id: number + error: Error +} +function createWorkerThreads(scannerModule: ScannerModule) { + const { port1: mainPort, port2: workerPort } = new worker_threads.MessageChannel() const worker = new worker_threads.Worker(__filename, { - workerData: { workerPort, internalThread: true, scannerModule }, + workerData: { workerPort, internalThread: true, scannerModule: scannerModule.modules }, transferList: [workerPort], execArgv: [] }) @@ -30,11 +39,17 @@ function createWorkerThreads(scannerModule: TrackModule[]) { const run = () => { worker.postMessage({ id }) return new Promise((resolve, reject) => { - mainPort.on('message', (message) => { + mainPort.on('message', (message: ThreadMessage) => { if (message.id !== id) reject(new Error(`Internal error: Expected id ${id} but got id ${message.id}`)) if (message.error) { reject(message.error) } else { + // Can't copy function reference. So we should bind it again from resolvers + message.bindings.forEach((meta, moduleName) => { + if (scannerModule.resolvers[moduleName]) { + meta.resolve = scannerModule.resolvers[moduleName] + } + }) resolve({ dependencies: message.bindings, failedModules: message.failedModules }) } worker.terminate() @@ -135,30 +150,43 @@ if (worker_threads.workerData?.internalThread) { } class Scanner { - modules: Array + modules: Array dependencies: Map failedModules: Map constructor(modules: Array) { - this.modules = this.serialization(modules) + this.modules = modules this.dependencies = new Map() this.failedModules = new Map() } public async scanAllDependencies() { // we won't throw any exceptions inside this task. - const res = await createWorkerThreads(this.modules) + const res = await createWorkerThreads(this.serialization(this.modules)) this.dependencies = res.dependencies this.failedModules = res.failedModules } - private serialization(modules: Array) { - is(Array.isArray(modules), 'vite-plugin-cdn2: option module must be array') - return uniq(modules) - .map((module) => { - if (typeof module === 'string') return { name: module } - return module - }) - .filter((v) => v.name) + private serialization(input: Array) { + is(Array.isArray(input), 'vite-plugin-cdn2: option module must be array') + const modules: Array = [] + const resolvers: Record = {} + const bucket = new Set() + for (const module of input) { + if (typeof module === 'string') { + if (bucket.has(module) || !module) continue + modules.push({ name: module }) + bucket.add(module) + continue + } + if (!module.name || bucket.has(module.name)) continue + const { resolve, ...rest } = module + if (resolve) { + resolvers[rest.name] = resolve + } + modules.push(rest) + bucket.add(module.name) + } + return { modules, resolvers } } } diff --git a/src/shared.ts b/src/shared.ts index 299378d..f64a7e6 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -13,17 +13,8 @@ export function len>(source: T) { return source.length } -export function uniq(arr: NonNullable[]) { - const result: T[] = [] - const record = new Map() - arr.forEach((item) => { - const key = typeof item === 'object' ? JSON.stringify(item) : item - if (!record.has(key)) { - result.push(item) - record.set(key, true) - } - }) - return result +export function uniq(arr: T[]) { + return Array.from(new Set(arr)) } export function isSupportThreads(): [boolean, string] { @@ -39,3 +30,8 @@ export function is(condit: boolean, message: string) { throw new Error(message) } } + +export function omit, K extends keyof T>(source: T, excludes: K[]) { + return (Object.keys(source) as K[]) + .reduce((acc, cur) => excludes.includes(cur) ? acc : { ...acc, [cur]: d[cur] }, {} as Omit) +}