Skip to content

Commit

Permalink
refactor!: load plugins via config file
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Feb 9, 2025
1 parent 6541e10 commit b78910e
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 138 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"release": "bumpp && pnpm publish",
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"unconfig": "^7.0.0"
},
"devDependencies": {
"@sxzz/eslint-config": "^5.0.1",
"@sxzz/prettier-config": "^2.1.1",
Expand Down
55 changes: 23 additions & 32 deletions playground/demo.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
// @ts-check

import type { Plugin, PluginContext, PluginEntry } from '../dist/index.d.ts'
import type { Plugin, PluginContext } from '../dist/index.d.ts'

export function demoPlugin(): PluginEntry<Data> {
return {
name: 'demo',
entry: import.meta.url,
data: { count: 10 },
}
}
let context: PluginContext

export interface Data {
count: number
}
export function demoPlugin(): Plugin {
return {
name: 'demo-plugin',
buildStart(_context) {
context = _context
context.log('hello world')
},
async resolveId(source, importer, options) {
if (source.startsWith('node:')) return

let context: PluginContext<Data>
if (source === 'virtual-mod') {
return 'file:///virtual-mod.ts'
}

const plugin: Plugin<Data> = {
buildStart(_context) {
context = _context
context.log(`count is ${context.data.count}`)
},
async resolveId(source, importer, options) {
if (source === 'virtual-mod') {
return 'file:///virtual-mod.ts'
}
const result = await this.resolve(`${source}.ts`, importer, options)
if (result) return result
},
load(id) {
if (id === 'file:///virtual-mod.ts') {
return { code: 'export const count = 42' }
}
},
const result = await this.resolve(`${source}.ts`, importer, options)
if (result) return result
},
load(id) {
if (id === 'file:///virtual-mod.ts') {
return { code: 'export const count = 42' }
}
},
}
}

// eslint-disable-next-line import/no-default-export
export default plugin
5 changes: 2 additions & 3 deletions playground/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// @ts-check
import { register } from '../dist/index.js'
import { demoPlugin } from './demo.ts'

register([demoPlugin()])
register()

const mod = await import('../src/utils') // no .ts suffix
const mod = await import('../src/loader/config') // no .ts suffix
console.info(mod)

const mod2 = await import('virtual-mod')
Expand Down
13 changes: 13 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 5 additions & 40 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,9 @@
import module from 'node:module'
import process from 'node:process'
import type { Data } from './loader/index.ts'
import type { MessageLog } from './loader/rpc.ts'
import type { PluginEntry } from './plugin.ts'
import type { UnloaderConfig } from './loader/config.ts'

export * from './plugin.ts'
export * from './register.ts'
export * from './loader/config.ts'

export function register(plugins: PluginEntry[] = []): void {
if (!module.register) {
throw new Error(
`This version of Node.js (${process.version}) does not support module.register(). Please upgrade to Node v18.19 or v20.6 and above.`,
)
}

const { port1, port2 } = new MessageChannel()
const data: Data = {
port: port2,
plugins: Object.create(null),
}
const transferList = [port2]

for (const plugin of plugins) {
data.plugins[plugin.name] = {
entry: plugin.entry,
data: plugin.data,
}
transferList.push(...(plugin.transferList || []))
}

module.register('./loader/index.js', {
parentURL: import.meta.url,
data,
transferList,
})

port1.on('message', (message: MessageLog) => {
switch (message.type) {
case 'log':
console.info('[port log]', message.message)
}
})
port1.unref()
export function defineConfig(config: UnloaderConfig): UnloaderConfig {
return config
}
22 changes: 22 additions & 0 deletions src/loader/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import process from 'node:process'
import { loadConfig as unconfig } from 'unconfig'
import type { Plugin } from '../plugin.ts'

export interface UnloaderConfig {
plugins: Plugin[]
}

export async function loadConfig(): Promise<UnloaderConfig> {
const { config } = await unconfig<UnloaderConfig>({
sources: [
{
files: 'unloader.config',
extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json', ''],
},
],
cwd: process.cwd(),
defaults: { plugins: [] },
})

return config
}
95 changes: 46 additions & 49 deletions src/loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { interopDefault } from '../utils.ts'
import type {
FalsyValue,
LoadResult,
Plugin,
PluginEntry,
ResolvedId,
ResolveMeta,
} from '../plugin'
import { loadConfig } from './config.ts'
import { log } from './rpc.ts'
import type {
InitializeHook,
Expand All @@ -19,31 +18,59 @@ import type { MessagePort } from 'node:worker_threads'

export interface Data {
port: MessagePort
plugins: Record<string, Pick<PluginEntry, 'entry' | 'data'>>
}

// eslint-disable-next-line import/no-mutable-exports
export let data: Data
const plugins: Record<string, Plugin> = Object.create(null)
let plugins: Plugin[]

export const initialize: InitializeHook = async (_data: Data) => {
data = _data
const { port } = data

for (const [name, plugin] of Object.entries(data.plugins)) {
const mod: Plugin = interopDefault(await import(plugin.entry))
await mod.buildStart?.({
port,
data: plugin.data,
log,
})
const config = await loadConfig()
for (const plugin of config.plugins) {
await plugin.buildStart?.({ port, log })

plugins[name] = mod
log(`loaded plugin ${name}`)
log(`loaded plugin ${plugin.name}`)
}

plugins = config.plugins
}

export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
if (plugins) {
for (const plugin of plugins) {
const result = await plugin.resolveId?.call(
{ resolve },
specifier,
context.parentURL,
{
conditions: context.conditions,
attributes: context.importAttributes,
},
)

if (result) {
if (typeof result === 'string')
return {
url: result,
importAttributes: context.importAttributes,
shortCircuit: true,
}

return {
url: result.id,
format: result.format,
importAttributes: result.attributes || context.importAttributes,
shortCircuit: true,
}
}
}
}

return nextResolve(specifier, context)

async function resolve(
source: string,
importer?: string,
Expand All @@ -64,46 +91,17 @@ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
return null
}
}

for (const name of Object.keys(data.plugins)) {
if (!plugins[name]) continue

const result = await plugins[name].resolveId?.call(
{ resolve },
specifier,
context.parentURL,
{ conditions: context.conditions, attributes: context.importAttributes },
)

if (result) {
if (typeof result === 'string')
return {
url: result,
importAttributes: context.importAttributes,
shortCircuit: true,
}

return {
url: result.id,
format: result.format,
importAttributes: result.attributes || context.importAttributes,
shortCircuit: true,
}
}
}

return nextResolve(specifier, context)
}

export const load: LoadHook = async (url, context, nextLoad) => {
if (!plugins) return nextLoad(url, context)

let result: LoadFnOutput | undefined
const defaultFormat = context.format || 'module'

// load hook
for (const name of Object.keys(data.plugins)) {
if (!plugins[name]) continue

const loadResult = await plugins[name].load?.(url, {
for (const plugin of plugins) {
const loadResult = await plugin.load?.(url, {
format: context.format,
conditions: context.conditions,
attributes: context.importAttributes,
Expand Down Expand Up @@ -134,10 +132,9 @@ export const load: LoadHook = async (url, context, nextLoad) => {
result ||= await nextLoad(url, context)

// transform hook
for (const name of Object.keys(data.plugins)) {
if (!plugins[name]) continue
for (const plugin of plugins) {
const transformResult: ModuleSource | LoadResult | FalsyValue =
await plugins[name].transform?.(result.source, url, {
await plugin.transform?.(result.source, url, {
format: result.format,
conditions: context.conditions,
attributes: context.importAttributes,
Expand Down
15 changes: 4 additions & 11 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export interface LoadResult {
format?: ModuleFormat
}

export interface PluginContext<T> {
data: T
export interface PluginContext {
port: MessagePort
log: (message: any, transferList?: TransferListItem[]) => void
}
Expand All @@ -48,8 +47,9 @@ export type ResolveFn = (
options?: ResolveMeta,
) => Promise<ResolvedId | null>

export interface Plugin<T = any> {
buildStart?: (context: PluginContext<T>) => Awaitable<void>
export interface Plugin {
name: string
buildStart?: (context: PluginContext) => Awaitable<void>
resolveId?: (
this: { resolve: ResolveFn },
source: string,
Expand All @@ -66,10 +66,3 @@ export interface Plugin<T = any> {
options: ResolveMeta & { format: ModuleFormat | null | undefined },
) => Awaitable<ModuleSource | LoadResult | FalsyValue>
}

export interface PluginEntry<T = any> {
name: string
entry: string
data?: T
transferList?: any[]
}
Loading

0 comments on commit b78910e

Please sign in to comment.