@@ -2,6 +2,7 @@ import * as fs from 'node:fs'
22import * as path from 'node:path'
33
44import { generateId } from './id'
5+ import { compareVersions } from './utils'
56
67const d = require ( 'debug' ) ( 'electron-chrome-web-store:loader' )
78
@@ -20,6 +21,25 @@ const manifestExists = async (dirPath: string) => {
2021 }
2122}
2223
24+ /**
25+ * DFS directories for extension manifests.
26+ */
27+ async function extensionSearch ( dirPath : string , depth : number = 0 ) : Promise < string [ ] > {
28+ if ( depth >= 2 ) return [ ]
29+ const results = [ ]
30+ const dirEntries = await fs . promises . readdir ( dirPath , { withFileTypes : true } )
31+ for ( const entry of dirEntries ) {
32+ if ( entry . isDirectory ( ) ) {
33+ if ( await manifestExists ( path . join ( dirPath , entry . name ) ) ) {
34+ results . push ( path . join ( dirPath , entry . name ) )
35+ } else {
36+ results . push ( ...( await extensionSearch ( path . join ( dirPath , entry . name ) , depth + 1 ) ) )
37+ }
38+ }
39+ }
40+ return results
41+ }
42+
2343/**
2444 * Discover list of extensions in the given path.
2545 */
@@ -35,63 +55,27 @@ async function discoverExtensions(extensionsPath: string): Promise<ExtensionPath
3555 return [ ]
3656 }
3757
38- // Get top level directories
39- const subDirectories = await fs . promises . readdir ( extensionsPath , {
40- withFileTypes : true ,
41- } )
42-
43- // Find all directories containing extension manifest.json
44- // Limits search depth to 1-2.
45- const extensionDirectories = await Promise . all (
46- subDirectories
47- . filter ( ( dirEnt ) => dirEnt . isDirectory ( ) )
48- . map ( async ( dirEnt ) => {
49- const extPath = path . join ( extensionsPath , dirEnt . name )
50-
51- // Check if manifest exists in root directory
52- if ( await manifestExists ( extPath ) ) {
53- return extPath
54- }
55-
56- // Check one level deeper
57- const extSubDirs = await fs . promises . readdir ( extPath , {
58- withFileTypes : true ,
59- } )
60-
61- // Look for manifest in each subdirectory
62- for ( const subDir of extSubDirs ) {
63- if ( ! subDir . isDirectory ( ) ) continue
64-
65- const subDirPath = path . join ( extPath , subDir . name )
66- if ( await manifestExists ( subDirPath ) ) {
67- return subDirPath
68- }
69- }
70- } ) ,
71- )
72-
58+ const extensionDirectories = await extensionSearch ( extensionsPath )
7359 const results : ExtensionPathInfo [ ] = [ ]
7460
7561 for ( const extPath of extensionDirectories . filter ( Boolean ) ) {
76- console . log ( `Loading extension from ${ extPath } ` )
7762 try {
7863 const manifestPath = path . join ( extPath ! , 'manifest.json' )
7964 const manifestJson = ( await fs . promises . readFile ( manifestPath ) ) . toString ( )
8065 const manifest : chrome . runtime . Manifest = JSON . parse ( manifestJson )
81- if ( manifest . key ) {
82- results . push ( {
83- type : 'store' ,
84- path : extPath ! ,
85- manifest,
86- id : generateId ( manifest . key ) ,
87- } )
88- } else {
89- results . push ( {
90- type : 'unpacked' ,
91- path : extPath ! ,
92- manifest,
93- } )
94- }
66+ const result = manifest . key
67+ ? {
68+ type : 'store' as const ,
69+ path : extPath ! ,
70+ manifest,
71+ id : generateId ( manifest . key ) ,
72+ }
73+ : {
74+ type : 'unpacked' as const ,
75+ path : extPath ! ,
76+ manifest,
77+ }
78+ results . push ( result )
9579 } catch ( e ) {
9680 console . error ( e )
9781 }
@@ -100,6 +84,35 @@ async function discoverExtensions(extensionsPath: string): Promise<ExtensionPath
10084 return results
10185}
10286
87+ /**
88+ * Filter any outdated extensions in the case of duplicate installations.
89+ */
90+ function filterOutdatedExtensions ( extensions : ExtensionPathInfo [ ] ) : ExtensionPathInfo [ ] {
91+ const uniqueExtensions : ExtensionPathInfo [ ] = [ ]
92+ const storeExtMap = new Map < ExtensionId , ExtensionPathInfo > ( )
93+
94+ for ( const ext of extensions ) {
95+ if ( ext . type === 'unpacked' ) {
96+ // Unpacked extensions are always unique to their path
97+ uniqueExtensions . push ( ext )
98+ } else if ( ! storeExtMap . has ( ext . id ) ) {
99+ // New store extension
100+ storeExtMap . set ( ext . id , ext )
101+ } else {
102+ // Existing store extension, compare with existing version
103+ const latestExt = storeExtMap . get ( ext . id ) !
104+ if ( compareVersions ( latestExt . manifest . version , ext . manifest . version ) < 0 ) {
105+ storeExtMap . set ( ext . id , ext )
106+ }
107+ }
108+ }
109+
110+ // Append up to date store extensions
111+ storeExtMap . forEach ( ( ext ) => uniqueExtensions . push ( ext ) )
112+
113+ return uniqueExtensions
114+ }
115+
103116/**
104117 * Load all extensions from the given directory.
105118 */
@@ -110,7 +123,8 @@ export async function loadAllExtensions(
110123 allowUnpacked ?: boolean
111124 } = { } ,
112125) {
113- const extensions = await discoverExtensions ( extensionsPath )
126+ let extensions = await discoverExtensions ( extensionsPath )
127+ extensions = filterOutdatedExtensions ( extensions )
114128 d ( 'discovered %d extension(s) in %s' , extensions . length , extensionsPath )
115129
116130 for ( const ext of extensions ) {
0 commit comments