@@ -4,81 +4,81 @@ import { pathToFileURL } from "url"
44import { isPathPluginSpec , parsePluginSpecifier , resolvePathPluginTarget } from "@/plugin/shared"
55import path from "path"
66
7- export namespace ConfigPlugin {
8- const Options = z . record ( z . string ( ) , z . unknown ( ) )
9- export type Options = z . infer < typeof Options >
10-
11- // Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
12- // It answers "what should we load?" but says nothing about where that value came from.
13- export const Spec = z . union ( [ z . string ( ) , z . tuple ( [ z . string ( ) , Options ] ) ] )
14- export type Spec = z . infer < typeof Spec >
15-
16- export type Scope = "global" | "local"
17-
18- // Origin keeps the original config provenance attached to a spec.
19- // After multiple config files are merged, callers still need to know which file declared the plugin
20- // and whether it should behave like a global or project-local plugin.
21- export type Origin = {
22- spec : Spec
23- source : string
24- scope : Scope
25- }
7+ const Options = z . record ( z . string ( ) , z . unknown ( ) )
8+ export type Options = z . infer < typeof Options >
269
27- export async function load ( dir : string ) {
28- const plugins : ConfigPlugin . Spec [ ] = [ ]
29-
30- for ( const item of await Glob . scan ( "{plugin,plugins}/*.{ts,js}" , {
31- cwd : dir ,
32- absolute : true ,
33- dot : true ,
34- symlink : true ,
35- } ) ) {
36- plugins . push ( pathToFileURL ( item ) . href )
37- }
38- return plugins
39- }
10+ // Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
11+ // It answers "what should we load?" but says nothing about where that value came from.
12+ export const Spec = z . union ( [ z . string ( ) , z . tuple ( [ z . string ( ) , Options ] ) ] )
13+ export type Spec = z . infer < typeof Spec >
4014
41- export function pluginSpecifier ( plugin : Spec ) : string {
42- return Array . isArray ( plugin ) ? plugin [ 0 ] : plugin
43- }
15+ export type Scope = "global" | "local"
16+
17+ // Origin keeps the original config provenance attached to a spec.
18+ // After multiple config files are merged, callers still need to know which file declared the plugin
19+ // and whether it should behave like a global or project-local plugin.
20+ export type Origin = {
21+ spec : Spec
22+ source : string
23+ scope : Scope
24+ }
25+
26+ export async function load ( dir : string ) {
27+ const plugins : Spec [ ] = [ ]
4428
45- export function pluginOptions ( plugin : Spec ) : Options | undefined {
46- return Array . isArray ( plugin ) ? plugin [ 1 ] : undefined
29+ for ( const item of await Glob . scan ( "{plugin,plugins}/*.{ts,js}" , {
30+ cwd : dir ,
31+ absolute : true ,
32+ dot : true ,
33+ symlink : true ,
34+ } ) ) {
35+ plugins . push ( pathToFileURL ( item ) . href )
4736 }
37+ return plugins
38+ }
39+
40+ export function pluginSpecifier ( plugin : Spec ) : string {
41+ return Array . isArray ( plugin ) ? plugin [ 0 ] : plugin
42+ }
43+
44+ export function pluginOptions ( plugin : Spec ) : Options | undefined {
45+ return Array . isArray ( plugin ) ? plugin [ 1 ] : undefined
46+ }
4847
49- // Path-like specs are resolved relative to the config file that declared them so merges later on do not
50- // accidentally reinterpret `./plugin.ts` relative to some other directory.
51- export async function resolvePluginSpec ( plugin : Spec , configFilepath : string ) : Promise < Spec > {
52- const spec = pluginSpecifier ( plugin )
53- if ( ! isPathPluginSpec ( spec ) ) return plugin
48+ // Path-like specs are resolved relative to the config file that declared them so merges later on do not
49+ // accidentally reinterpret `./plugin.ts` relative to some other directory.
50+ export async function resolvePluginSpec ( plugin : Spec , configFilepath : string ) : Promise < Spec > {
51+ const spec = pluginSpecifier ( plugin )
52+ if ( ! isPathPluginSpec ( spec ) ) return plugin
5453
55- const base = path . dirname ( configFilepath )
56- const file = ( ( ) => {
57- if ( spec . startsWith ( "file://" ) ) return spec
58- if ( path . isAbsolute ( spec ) || / ^ [ A - Z a - z ] : [ \\ / ] / . test ( spec ) ) return pathToFileURL ( spec ) . href
59- return pathToFileURL ( path . resolve ( base , spec ) ) . href
60- } ) ( )
54+ const base = path . dirname ( configFilepath )
55+ const file = ( ( ) => {
56+ if ( spec . startsWith ( "file://" ) ) return spec
57+ if ( path . isAbsolute ( spec ) || / ^ [ A - Z a - z ] : [ \\ / ] / . test ( spec ) ) return pathToFileURL ( spec ) . href
58+ return pathToFileURL ( path . resolve ( base , spec ) ) . href
59+ } ) ( )
6160
62- const resolved = await resolvePathPluginTarget ( file ) . catch ( ( ) => file )
61+ const resolved = await resolvePathPluginTarget ( file ) . catch ( ( ) => file )
6362
64- if ( Array . isArray ( plugin ) ) return [ resolved , plugin [ 1 ] ]
65- return resolved
66- }
63+ if ( Array . isArray ( plugin ) ) return [ resolved , plugin [ 1 ] ]
64+ return resolved
65+ }
66+
67+ // Dedupe on the load identity (package name for npm specs, exact file URL for local specs), but keep the
68+ // full Origin so downstream code still knows which config file won and where follow-up writes should go.
69+ export function deduplicatePluginOrigins ( plugins : Origin [ ] ) : Origin [ ] {
70+ const seen = new Set < string > ( )
71+ const list : Origin [ ] = [ ]
6772
68- // Dedupe on the load identity (package name for npm specs, exact file URL for local specs), but keep the
69- // full Origin so downstream code still knows which config file won and where follow-up writes should go.
70- export function deduplicatePluginOrigins ( plugins : Origin [ ] ) : Origin [ ] {
71- const seen = new Set < string > ( )
72- const list : Origin [ ] = [ ]
73-
74- for ( const plugin of plugins . toReversed ( ) ) {
75- const spec = pluginSpecifier ( plugin . spec )
76- const name = spec . startsWith ( "file://" ) ? spec : parsePluginSpecifier ( spec ) . pkg
77- if ( seen . has ( name ) ) continue
78- seen . add ( name )
79- list . push ( plugin )
80- }
81-
82- return list . toReversed ( )
73+ for ( const plugin of plugins . toReversed ( ) ) {
74+ const spec = pluginSpecifier ( plugin . spec )
75+ const name = spec . startsWith ( "file://" ) ? spec : parsePluginSpecifier ( spec ) . pkg
76+ if ( seen . has ( name ) ) continue
77+ seen . add ( name )
78+ list . push ( plugin )
8379 }
80+
81+ return list . toReversed ( )
8482}
83+
84+ export * as ConfigPlugin from "./plugin"
0 commit comments