11import { createHash } from 'node:crypto' ;
22import fs from 'node:fs/promises' ;
33import { createRequire } from 'node:module' ;
4- import { pathToFileURL } from 'node:url' ;
4+ import nodePath from 'node:path' ;
5+ import { fileURLToPath , pathToFileURL } from 'node:url' ;
6+ import { gzipSync } from 'node:zlib' ;
57
68import type { CSpellSettings , CSpellVFS } from '@cspell/cspell-types' ;
79import { mergeConfig } from '@cspell/cspell-types' ;
810import type { CSpellConfigFile , CSpellConfigFileReaderWriter , ICSpellConfigFile } from 'cspell-config-lib' ;
11+ import { convertToBTrie } from 'cspell-trie-lib' ;
912
10- export interface CSpellDictionaryBundlerOptions {
11- debug ?: boolean ;
12- }
13+ import type { Options } from './options.ts' ;
14+
15+ export type CSpellDictionaryBundlerOptions = Required <
16+ Pick < Options , 'debug' | 'convertToBTrie' | 'minConvertSize' | 'compress' >
17+ > ;
1318
1419export class CSpellDictionaryBundler {
1520 #loadedConfigs = new Map < string , Promise < ICSpellConfigFile > > ( ) ;
@@ -43,7 +48,7 @@ export class CSpellDictionaryBundler {
4348 const imports = await this . loadImports ( config ) ;
4449 const settings = mergeConfig (
4550 imports . map ( ( f ) => f . settings ) ,
46- await this . resolveDictionaries ( config ) ,
51+ await resolveDictionaries ( config , this . #options ) ,
4752 ) ;
4853 delete settings . import ;
4954 delete settings [ '$schema' ] ;
@@ -53,29 +58,6 @@ export class CSpellDictionaryBundler {
5358 } ;
5459 }
5560
56- async resolveDictionaries ( config : ICSpellConfigFile ) : Promise < CSpellSettings > {
57- const settings = { ...config . settings } ;
58- if ( ! settings . dictionaryDefinitions ) return settings ;
59- // Make a copy of the dictionary definitions and vfs to avoid mutating the original config file.
60- const dictDefs = ( settings . dictionaryDefinitions = [ ...settings . dictionaryDefinitions ] ) ;
61- const vfs : CSpellVFS = ( settings . vfs ??= Object . create ( null ) ) ;
62-
63- for ( let i = 0 ; i < dictDefs . length ; ++ i ) {
64- const def = dictDefs [ i ] ;
65- if ( ! def . path ) continue ;
66- const d = { ...def } ;
67- dictDefs [ i ] = d ;
68- const url = new URL ( def . btrie ?? def . path , config . url ) ;
69- if ( url . protocol !== 'file:' ) continue ;
70- const vfsUrl = await populateVfs ( vfs , url ) ;
71- delete d . file ;
72- delete d . btrie ;
73- d . path = vfsUrl . href ;
74- }
75-
76- return settings ;
77- }
78-
7961 importConfig ( url : URL , content ?: string ) : Promise < CSpellConfigFile > {
8062 if ( content && ! isCodeFile ( url ) ) {
8163 return Promise . resolve ( this . reader . parse ( { url, content } ) ) ;
@@ -89,19 +71,64 @@ export class CSpellDictionaryBundler {
8971 }
9072}
9173
74+ export async function resolveDictionaries (
75+ config : ICSpellConfigFile ,
76+ options : CSpellDictionaryBundlerOptions ,
77+ ) : Promise < CSpellSettings > {
78+ const settings = { ...config . settings } ;
79+ if ( ! settings . dictionaryDefinitions ) return settings ;
80+ if ( config . url . protocol !== 'file:' ) return settings ;
81+ // Make a copy of the dictionary definitions and vfs to avoid mutating the original config file.
82+ const dictDefs = ( settings . dictionaryDefinitions = [ ...settings . dictionaryDefinitions ] ) ;
83+ const vfs : CSpellVFS = ( settings . vfs ??= Object . create ( null ) ) ;
84+ const minConvertSize = options . minConvertSize ?? 1024 ;
85+
86+ for ( let i = 0 ; i < dictDefs . length ; ++ i ) {
87+ const def = dictDefs [ i ] ;
88+ const d = { ...def } ;
89+ if ( ! d . path ) continue ;
90+ dictDefs [ i ] = d ;
91+ const url = resolvePath ( d . btrie ?? d . path , config . url ) ;
92+ if ( url . protocol !== 'file:' ) continue ;
93+ let file = await readFile ( { url } ) ;
94+ file = options . convertToBTrie && fileLength ( file ) >= minConvertSize ? await convert ( file ) : file ;
95+ file = options . compress && fileLength ( file ) >= minConvertSize ? compressFile ( file ) : file ;
96+ const vfsUrl = await populateVfs ( vfs , file ) ;
97+ delete d . file ;
98+ delete d . btrie ;
99+ d . path = vfsUrl . href ;
100+ }
101+
102+ return settings ;
103+ }
104+
105+ interface FileReference {
106+ url : URL ;
107+ content ?: string | Uint8Array < ArrayBuffer > ;
108+ }
109+
110+ interface FileResource extends FileReference {
111+ content : string | Uint8Array < ArrayBuffer > ;
112+ }
113+
114+ async function convert ( file : FileReference ) : Promise < FileResource > {
115+ const resource = await readFile ( file ) ;
116+ return convertToBTrie ( resource , { optimize : true } ) ;
117+ }
118+
92119/**
93120 * Load a file from the file system and populate the virtual file system with its content.
94121 *
95122 * @param vfs - The Virtual Files system data
96123 * @param url - The url to load and store.
97124 * @return The cspell-vfs url that was loaded.
98125 */
99- export async function populateVfs ( vfs : CSpellVFS , url : URL ) : Promise < URL > {
100- const content = await fs . readFile ( url ) ;
126+ export async function populateVfs ( vfs : CSpellVFS , fileRef : FileReference ) : Promise < URL > {
127+ const { url , content } = await readFile ( fileRef ) ;
101128
102129 const hash = createHash ( 'sha256' ) . update ( content ) . digest ( 'hex' ) ;
103130
104- const data = content . toString ( 'base64' ) ;
131+ const data = typeof content === 'string' ? content : Buffer . from ( content ) . toString ( 'base64' ) ;
105132 const vfsUrl = makeVfsUrl ( url , hash . slice ( 0 , 16 ) ) ;
106133 vfs [ vfsUrl . href ] = {
107134 data,
@@ -150,3 +177,38 @@ const isCodeFileRegExp = /\.[cm]?(js|ts)$/i;
150177function isCodeFile ( url : URL ) : boolean {
151178 return isCodeFileRegExp . test ( url . pathname ) ;
152179}
180+
181+ async function readFile ( fileRef : FileReference ) : Promise < FileResource > {
182+ const url = fileRef . url ;
183+ const content = fileRef . content ?? ( await fs . readFile ( url ) ) ;
184+ return { url, content } ;
185+ }
186+
187+ /**
188+ * This is the approximate size of the file in bytes.
189+ * @param file
190+ * @returns
191+ */
192+ function fileLength ( file : FileResource ) : number {
193+ if ( typeof file . content === 'string' ) {
194+ return file . content . length ;
195+ }
196+ return file . content . byteLength ;
197+ }
198+
199+ function compressFile ( file : FileResource ) : FileResource {
200+ if ( file . url . pathname . endsWith ( '.gz' ) ) return file ;
201+ const url = new URL ( file . url . pathname + '.gz' , file . url ) ;
202+ const content = gzipSync ( file . content ) ;
203+ return { url, content } ;
204+ }
205+
206+ function resolvePath ( path : string , base : URL ) : URL {
207+ if ( isUrlLike ( path ) ) {
208+ return new URL ( path ) ;
209+ }
210+
211+ const dir = fileURLToPath ( new URL ( './' , base ) ) ;
212+ const filePath = nodePath . resolve ( dir , path ) ;
213+ return pathToFileURL ( filePath ) ;
214+ }
0 commit comments