@@ -10,7 +10,6 @@ import {
1010 Logger ,
1111 type Plugin ,
1212 type PluginManifest ,
13- Result ,
1413 toBase64Img ,
1514} from "chili-api" ;
1615import type JSZip from "jszip" ;
@@ -25,101 +24,135 @@ export class PluginManager implements IPluginManager {
2524 const JSZip = await import ( "jszip" ) ;
2625 const zip = await JSZip . default . loadAsync ( file ) ;
2726
28- const manifest = await this . readManifest ( zip ) ;
29- if ( ! manifest ) {
30- return ;
31- }
32-
33- if ( await this . loadPluginCode ( zip , manifest ) ) {
34- Logger . info ( `Plugin ${ manifest . name } loaded successfully` ) ;
27+ const manifest = await this . readManifestFromZip ( zip ) ;
28+ if ( manifest ) {
29+ await this . loadPluginCodeFromZip ( zip , manifest ) ;
3530 }
3631 }
3732
3833 async loadFromUrl ( url : string ) {
39- const response = await fetch ( url ) ;
40- if ( ! response . ok ) {
41- alert ( `Failed to fetch plugin from ${ url } : ${ response . statusText } ` ) ;
42- return ;
43- }
44-
45- const arrayBuffer = await response . arrayBuffer ( ) ;
46- const blob = new Blob ( [ arrayBuffer ] , { type : "application/zip" } ) ;
47- const file = new File ( [ blob ] , "plugin.chiliplugin" ) ;
48-
49- await this . loadFromFile ( file ) ;
50- }
51-
52- async loadFromFolder ( folderUrl : string , indexName : string = "plugins.json" ) {
53- try {
54- const response = await fetch ( folderUrl + indexName ) ;
34+ if ( url . endsWith ( ".chiliplugin" ) ) {
35+ const response = await fetch ( url ) ;
5536 if ( ! response . ok ) {
37+ alert ( `Failed to fetch plugin from ${ url } : ${ response . statusText } ` ) ;
5638 return ;
5739 }
58- const config = await response . json ( ) ;
59- const plugins = config . plugins as string [ ] ;
60- const baseUrl = folderUrl . endsWith ( "/" ) ? folderUrl : folderUrl + "/" ;
61- for ( const plugin of plugins ?? [ ] ) {
62- await this . loadFromUrl ( baseUrl + plugin ) ;
40+
41+ const arrayBuffer = await response . arrayBuffer ( ) ;
42+ const blob = new Blob ( [ arrayBuffer ] , { type : "application/zip" } ) ;
43+ const file = new File ( [ blob ] , "plugin.chiliplugin" ) ;
44+
45+ await this . loadFromFile ( file ) ;
46+ } else {
47+ if ( ! url . endsWith ( "/" ) ) url += "/" ;
48+ const manifest = await this . readManifestFromUrl ( url + "manifest.json" ) ;
49+ if ( manifest ) {
50+ await this . loadPluginCodeFromUrl ( manifest . name , url , manifest . main ) ;
6351 }
64- } catch {
65- Logger . warn ( `Failed to load plugins from folder: ${ folderUrl } ` ) ;
6652 }
6753 }
6854
69- private async readManifest ( zip : JSZip ) {
55+ private async readManifestFromZip ( zip : JSZip ) {
7056 const manifestFile = zip . file ( "manifest.json" ) ;
7157 if ( ! manifestFile ) {
7258 alert ( "manifest.json not found in plugin archive" ) ;
7359 return undefined ;
7460 }
61+
7562 const content = await manifestFile . async ( "text" ) ;
7663 const manifest = JSON . parse ( content ) as PluginManifest ;
77- const validation = this . validateManifest ( manifest ) ;
78- if ( ! validation . isOk ) {
79- alert ( validation . error ) ;
64+ return this . validateManifest ( manifest ) ? manifest : undefined ;
65+ }
66+
67+ private async readManifestFromUrl ( url : string ) {
68+ const response = await fetch ( url ) ;
69+ if ( ! response . ok ) {
8070 return undefined ;
8171 }
82-
83- return manifest ;
72+ const manifest : PluginManifest = await response . json ( ) ;
73+ return this . validateManifest ( manifest ) ? manifest : undefined ;
8474 }
8575
86- private async loadPluginCode ( zip : JSZip , manifest : PluginManifest ) {
76+ private async loadPluginCodeFromZip ( zip : JSZip , manifest : PluginManifest ) {
8777 const codeFile = zip . file ( manifest . main ) ;
8878 if ( ! codeFile ) {
8979 alert ( manifest . main + " not found in plugin archive" ) ;
90- return false ;
80+ return ;
9181 }
9282 const code = await codeFile . async ( "text" ) ;
83+ const handlePluginIcon = async ( plugin : Plugin ) => {
84+ await this . transformZipCommandIcon ( zip , plugin ) ;
85+ } ;
86+ await this . loadPluginCode ( manifest . name , code , handlePluginIcon ) ;
87+ }
88+
89+ private async loadPluginCodeFromUrl ( name : string , baseUrl : string , codePath : string ) {
90+ if ( codePath . startsWith ( "/" ) ) codePath = codePath . substring ( 1 ) ;
91+
92+ const fullUrl = baseUrl + codePath ;
93+ const response = await fetch ( fullUrl ) ;
94+ if ( ! response . ok ) {
95+ return undefined ;
96+ }
97+
98+ const code = await response . text ( ) ;
99+ const handlePluginIcon = async ( plugin : Plugin ) => {
100+ await this . transformUrlCommandIcon ( baseUrl , plugin ) ;
101+ } ;
102+ await this . loadPluginCode ( name , code , handlePluginIcon ) ;
103+ }
104+
105+ private async loadPluginCode (
106+ name : string ,
107+ code : string ,
108+ handlePluginIcon : ( plugin : Plugin ) => Promise < void > ,
109+ ) {
93110 const blob = new Blob ( [ code ] , { type : "application/javascript" } ) ;
94111 const blobUrl = URL . createObjectURL ( blob ) ;
95112 await Promise . try ( async ( ) => {
96113 const module = await import ( /*webpackIgnore: true*/ blobUrl ) ;
97- await this . transformCommandIcon ( zip , module . default ) ;
98- this . registerPlugin ( module . default ) ;
99- this . plugins . set ( manifest . name , module . default ) ;
100- } ) . finally ( ( ) => {
101- URL . revokeObjectURL ( blobUrl ) ;
102- } ) ;
103- return true ;
114+ const plugin : Plugin = module . default ;
115+ await handlePluginIcon ( plugin ) ;
116+ this . registerPlugin ( plugin ) ;
117+ this . plugins . set ( name , plugin ) ;
118+
119+ Logger . info ( `Plugin ${ name } loaded successfully` ) ;
120+ } )
121+ . catch ( ( err ) => {
122+ alert ( `Failed to load plugin ${ name } : ${ err } ` ) ;
123+ } )
124+ . finally ( ( ) => {
125+ URL . revokeObjectURL ( blobUrl ) ;
126+ } ) ;
104127 }
105128
106- private async transformCommandIcon ( zip : JSZip , plugin : Plugin ) {
129+ private async transformZipCommandIcon ( zip : JSZip , plugin : Plugin ) {
107130 for ( const command of plugin . commands ?? [ ] ) {
108131 const data = CommandStore . getComandData ( command ) ;
109132 const iconData = data ?. icon as IconPath ;
110- if ( iconData ?. type === "plugin " ) {
111- const codeFile = zip . file ( iconData ?. path ) ;
133+ if ( iconData ?. type === "path " ) {
134+ const codeFile = zip . file ( iconData ?. value ) ;
112135 if ( ! codeFile ) {
113- alert ( `${ iconData . path } not found in plugin archive` ) ;
136+ alert ( `${ iconData . value } not found in plugin archive` ) ;
114137 continue ;
115138 }
116139 const icon = await codeFile . async ( "base64" ) ;
117- const base64 : string = toBase64Img ( iconData . path , icon ) ;
140+ const base64 : string = toBase64Img ( iconData . value , icon ) ;
118141 data ! . icon = { type : "url" , value : base64 } ;
119142 }
120143 }
121144 }
122145
146+ private async transformUrlCommandIcon ( baseUrl : string , plugin : Plugin ) {
147+ for ( const command of plugin . commands ?? [ ] ) {
148+ const data = CommandStore . getComandData ( command ) ;
149+ const iconData = data ?. icon as IconPath ;
150+ if ( iconData ?. type === "path" ) {
151+ data ! . icon = { type : "url" , value : baseUrl + iconData . value } ;
152+ }
153+ }
154+ }
155+
123156 async unload ( pluginName : string ) : Promise < void > {
124157 const plugin = this . plugins . get ( pluginName ) ;
125158 if ( ! plugin ) {
@@ -152,23 +185,38 @@ export class PluginManager implements IPluginManager {
152185 return this . plugins . has ( pluginName ) ;
153186 }
154187
155- private validateManifest ( manifest : PluginManifest ) : Result < boolean > {
156- if ( ! manifest . name ) Result . err ( "Missing required field: name" ) ;
157- if ( ! manifest . version ) Result . err ( "Missing required field: version" ) ;
158- if ( ! manifest . main ) Result . err ( "Missing required field: main" ) ;
188+ private validateManifest ( manifest : PluginManifest ) {
189+ if ( this . manifests . has ( manifest . name ) ) {
190+ alert ( `Plugin ${ manifest . name } already loaded` ) ;
191+ return false ;
192+ }
159193
194+ const errors : string [ ] = [ ] ;
195+ if ( ! manifest . name ) errors . push ( "Missing required field: name" ) ;
196+ if ( ! manifest . version ) errors . push ( "Missing required field: version" ) ;
197+ if ( ! manifest . main ) errors . push ( "Missing required field: main" ) ;
160198 if ( manifest . version && ! this . isValidSemver ( manifest . version ) ) {
161- Result . err ( "Invalid version format (expected semver like 1.0.0)" ) ;
199+ errors . push ( "Invalid version format (expected semver like 1.0.0)" ) ;
162200 }
163-
164201 if ( manifest . engines ?. chili3d ) {
165202 const currentVersion = __APP_VERSION__ ;
166203 if ( ! this . satisfiesVersion ( currentVersion , manifest . engines . chili3d ) ) {
167- Result . err ( `Chili3D version ${ currentVersion } does not satisfy ${ manifest . engines . chili3d } ` ) ;
204+ errors . push ( `Chili3D version ${ currentVersion } does not satisfy ${ manifest . engines . chili3d } ` ) ;
168205 }
169206 }
170207
171- return Result . ok ( true ) ;
208+ if ( errors . length > 0 ) {
209+ alert (
210+ "Load plugin " +
211+ manifest . name +
212+ " failed:\n" +
213+ errors . map ( ( x , i ) => `${ i + 1 } . ${ x } ` ) . join ( "\n" ) ,
214+ ) ;
215+ return false ;
216+ }
217+
218+ this . manifests . set ( manifest . name , manifest ) ;
219+ return true ;
172220 }
173221
174222 private isValidSemver ( version : string ) : boolean {
0 commit comments