Skip to content

Commit

Permalink
feat: implement resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
nonzzz committed Jul 21, 2023
1 parent ba792c6 commit d521bee
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 39 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IModule | string>
Expand Down
9 changes: 8 additions & 1 deletion __tests__/scanner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
1 change: 1 addition & 0 deletions docs/Modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
8 changes: 4 additions & 4 deletions src/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
}

Expand Down
6 changes: 3 additions & 3 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
code?: string
url?: string | URLFunction
resolve?: string | ResolverFunction
}

export interface IModule extends TrackModule{
[prop: string]: any
resolve?: string | ResolverFunction
}

export interface Serialization {
Expand Down
66 changes: 47 additions & 19 deletions src/scanner.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
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[]
workerPort: MessagePort
internalThread: boolean
}

function createWorkerThreads(scannerModule: TrackModule[]) {
const { port1: mainPort, port2: workerPort } = new worker_threads.MessageChannel()
interface ScannerModule {
modules: Array<TrackModule>
resolvers: Record<string, string | ResolverFunction>
}

interface ThreadMessage {
bindings: Map<string, ModuleInfo>,
failedModules: Map<string, string>
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: []
})
const id = 0
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()
Expand Down Expand Up @@ -135,30 +150,43 @@ if (worker_threads.workerData?.internalThread) {
}

class Scanner {
modules: Array<IModule>
modules: Array<IModule | string>
dependencies: Map<string, ModuleInfo>
failedModules: Map<string, string>
constructor(modules: Array<IModule | string>) {
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<IModule | string>) {
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<IModule | string>) {
is(Array.isArray(input), 'vite-plugin-cdn2: option module must be array')
const modules: Array<IModule> = []
const resolvers: Record<string, string | ResolverFunction> = {}
const bucket = new Set<string>()
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 }
}
}

Expand Down
18 changes: 7 additions & 11 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,8 @@ export function len<T extends ArrayLike<unknown>>(source: T) {
return source.length
}

export function uniq<T>(arr: NonNullable<T>[]) {
const result: T[] = []
const record = new Map<unknown, boolean>()
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<T>(arr: T[]) {
return Array.from(new Set(arr))
}

export function isSupportThreads(): [boolean, string] {
Expand All @@ -39,3 +30,8 @@ export function is(condit: boolean, message: string) {
throw new Error(message)
}
}

export function omit<T extends Record<string, unknown>, 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<T, K>)
}

0 comments on commit d521bee

Please sign in to comment.