1+ import latestSemver from 'latest-semver' ;
12import * as fsSync from 'node:fs' ;
23import * as fs from 'node:fs/promises' ;
34import * as os from 'node:os' ;
45import path from 'node:path' ;
5- import { Readable } from 'node:stream' ;
6- import { finished } from 'node:stream/promises' ;
76
7+ import { ApiClient } from '../api/index.js' ;
8+ import { PluginInfo } from '../api/types.js' ;
89import { ctx } from '../events/context.js' ;
910import { Plugin } from './plugin.js' ;
1011
11- const DEFAULT_PLUGIN_URL = 'https://codify-plugin-library.s3.amazonaws.com/codify-core/index.js' ;
1212const PLUGIN_CACHE_DIR = path . resolve ( os . homedir ( ) , '.codify/plugins' )
1313
1414export class PluginResolver {
1515
16- static async resolve ( name : string , version : string ) : Promise < Plugin > {
16+ static async resolveAll ( definitions : Record < string , string > ) : Promise < Plugin [ ] > {
1717 await PluginResolver . checkAndCreateCacheDirIfNotExists ( )
1818
19- let directoryStat ;
20- try {
21- directoryStat = await fs . stat ( version ) ;
22- } catch {
23- }
19+ const localPluginDefs = Object . entries ( definitions )
20+ . filter ( ( [ k , v ] ) => v . endsWith ( '.js' ) || v . endsWith ( '.ts' ) )
21+ const localPlugins = await Promise . all ( localPluginDefs . map ( ( [ name , path ] ) =>
22+ PluginResolver . resolveLocalPlugin ( name , path )
23+ ) )
2424
25- // For easier development. A direct js file can be specified for the plugin.
26- if ( directoryStat && directoryStat . isFile ( ) ) {
27- return PluginResolver . resolvePluginFs ( name , version )
28- }
25+ const networkPluginDefs = Object . entries ( definitions )
26+ . filter ( ( [ k ] ) => ! localPluginDefs . some ( ( [ lk ] ) => k === lk ) )
2927
30- if ( name === 'default' ) {
31- return PluginResolver . resolvePluginDefault ( name , version )
28+ if ( networkPluginDefs . length === 0 ) {
29+ return localPlugins ;
3230 }
3331
34- throw new Error ( 'Non-default plugins are not currently supported' ) ;
35- }
36-
37- static async resolveExisting ( exclude : string [ ] ) : Promise < Plugin [ ] > {
38- let files ;
39- try {
40- files = await fs . readdir ( PLUGIN_CACHE_DIR ) ;
41- } catch {
42- }
32+ // Fetch the latest plugin info from the server
33+ const latestPluginInfo = await ApiClient
34+ . searchPlugins ( networkPluginDefs . map ( ( [ name , version ] ) => ( { name, version } ) ) )
35+ . catch ( ( e : Error ) => {
36+ console . warn ( 'Unable to fetch latest plugin info' ) ;
37+ ctx . debug ( `Unable to fetch latest plugin info:\n${ e . message } ` ) ;
38+ } ) ?? undefined ;
4339
44- if ( ! files ) {
45- return [ ] ;
46- }
40+ const networkPlugins = await Promise . all ( networkPluginDefs . map ( ( [ name , version ] ) =>
41+ PluginResolver . resolvePluginNetwork ( name , version , latestPluginInfo ?. [ name ] )
42+ ) )
4743
48- return files
49- . filter ( ( f ) => f . endsWith ( '.js' ) )
50- . filter ( ( f ) => ! exclude . includes ( getPluginName ( f ) ) )
51- . map ( ( f ) => {
52- const name = getPluginName ( f ) ;
53- const p = path . join ( PLUGIN_CACHE_DIR , f ) ;
54-
55- return new Plugin ( name , '' , p ) ;
56- } )
57-
58- function getPluginName ( fileName : string ) {
59- return fileName . split ( '.' ) [ 0 ] ;
60- }
44+ return [ ...networkPlugins , ...localPlugins ] ;
6145 }
6246
63- private static async resolvePluginFs ( name : string , filePath : string ) : Promise < Plugin > {
47+ static async resolveLocalPlugin ( name : string , filePath : string , version ? : string ) : Promise < Plugin > {
6448 const fileExtension = filePath . slice ( filePath . lastIndexOf ( '.' ) )
6549 if ( fileExtension !== '.js' && fileExtension !== '.ts' ) {
6650 throw new Error ( `Only .js and .ts plugins are support currently. Can't resolve ${ filePath } ` ) ;
6751 }
6852
53+ let stats : fsSync . Stats ;
54+ try {
55+ stats = await fs . stat ( filePath ) ;
56+ } catch ( e ) {
57+ throw new Error ( `Unable to find plugin file path ${ filePath } ` )
58+ }
59+
60+ if ( ! stats . isFile ( ) ) {
61+ throw new Error ( `Provided plugin path ${ filePath } does not reference a file` ) ;
62+ }
63+
6964 return new Plugin (
7065 name ,
71- '0.0.0' ,
66+ version ?? '0.0.0' ,
7267 filePath ,
7368 )
7469 }
7570
76- private static async resolvePluginDefault ( name : string , version : string ) : Promise < Plugin > {
77- const { body } = await fetch ( DEFAULT_PLUGIN_URL )
78- if ( ! body ) {
79- throw new Error ( 'Un- able to fetch the default plugin (body not found). Exiting' ) ;
71+ private static async resolvePluginNetwork ( name : string , version : string , latestInfoFromNetwork ?: PluginInfo ) : Promise < Plugin > {
72+ const resolvedVersion = ( version === 'latest' ) ? await PluginResolver . resolveLatestLocalVersion ( name ) : version ;
73+ if ( ! resolvedVersion && ! latestInfoFromNetwork ) {
74+ throw new Error ( `Plugin ${ name } not found and not able to download from registry. Please try again at a later time` ) ;
8075 }
8176
82- const filePath = path . join ( PLUGIN_CACHE_DIR , 'default.js' ) ;
83- const ws = fsSync . createWriteStream ( filePath )
77+ if ( ! resolvedVersion ) {
78+ return downloadFreshPlugin ( ) ;
79+ }
8480
85- // Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
86- await finished ( Readable . fromWeb ( body as never ) . pipe ( ws ) ) ;
81+ const localPluginExists = await PluginResolver . localPluginExists ( name , resolvedVersion ) ;
82+ if ( ! localPluginExists && ! latestInfoFromNetwork ) {
83+ throw new Error ( `Plugin ${ name } not found and not able to download from registry. Please try again at a later time` ) ;
84+ }
8785
88- return new Plugin (
89- name ,
90- version ,
91- filePath ,
92- )
86+ // Plugin already exists, then no need to download. OR couldn't fetch plugin info from online then just resolve local version. OR we already have the latest version
87+ if ( ( version !== 'latest' && localPluginExists )
88+ || ( localPluginExists && ! latestInfoFromNetwork )
89+ || ( resolvedVersion === latestInfoFromNetwork ! . version )
90+ ) {
91+ return PluginResolver . resolveLocalPlugin ( name , `${ PLUGIN_CACHE_DIR } /${ name } /${ version } /index.js` ) ;
92+ }
93+
94+ return downloadFreshPlugin ( ) ;
95+
96+ // Set up folders and download plugin from the network.
97+ async function downloadFreshPlugin ( ) : Promise < Plugin > {
98+ const filePath = `${ PLUGIN_CACHE_DIR } /${ name } /${ version } /index.js` ;
99+ await ApiClient . downloadPlugin ( filePath , latestInfoFromNetwork ! . downloadLink ) ;
100+
101+ return new Plugin (
102+ name ,
103+ version ,
104+ filePath ,
105+ )
106+ }
93107 }
94108
95109 private static async checkAndCreateCacheDirIfNotExists ( ) {
@@ -111,4 +125,24 @@ export class PluginResolver {
111125 ctx . log ( 'Creating a new cache dir for codify' ) ;
112126 await fs . mkdir ( PLUGIN_CACHE_DIR , { recursive : true } ) ;
113127 }
128+
129+ private static async resolveLatestLocalVersion ( name : string ) : Promise < string | undefined > {
130+ try {
131+ const pluginPath = path . join ( PLUGIN_CACHE_DIR , name ) ;
132+ const versions = await fs . readdir ( pluginPath ) ;
133+ return latestSemver ( versions ) ;
134+ } catch ( e ) {
135+ return undefined ;
136+ }
137+ }
138+
139+ private static async localPluginExists ( name : string , version : string ) : Promise < boolean > {
140+ const pluginPath = path . join ( PLUGIN_CACHE_DIR , name , version , 'index.js' ) ;
141+ try {
142+ const fileStats = await fs . stat ( pluginPath )
143+ return fileStats . isFile ( ) ;
144+ } catch ( e ) {
145+ return false ;
146+ }
147+ }
114148}
0 commit comments