@@ -2,7 +2,7 @@ import {createHash} from "node:crypto";
2
2
import type { WriteStream } from "node:fs" ;
3
3
import { createReadStream , existsSync , statSync } from "node:fs" ;
4
4
import { open , readFile , rename , unlink } from "node:fs/promises" ;
5
- import { dirname , extname , join , relative } from "node:path/posix" ;
5
+ import { dirname , extname , join } from "node:path/posix" ;
6
6
import { createGunzip } from "node:zlib" ;
7
7
import { spawn } from "cross-spawn" ;
8
8
import JSZip from "jszip" ;
@@ -46,6 +46,7 @@ const defaultEffects: LoadEffects = {
46
46
export interface LoaderOptions {
47
47
root : string ;
48
48
path : string ;
49
+ params ?: Params ;
49
50
targetPath : string ;
50
51
useStale : boolean ;
51
52
}
@@ -70,7 +71,7 @@ export class LoaderResolver {
70
71
* abort if we find a matching folder or reach the source root; for example,
71
72
* if src/data exists, we won’t look for a src/data.zip.
72
73
*/
73
- find ( targetPath : string , { useStale = false } = { } ) : Asset | Loader | undefined {
74
+ find ( targetPath : string , { useStale = false } = { } ) : Loader | undefined {
74
75
return this . findFile ( targetPath , { useStale} ) ?? this . findArchive ( targetPath , { useStale} ) ;
75
76
}
76
77
@@ -91,20 +92,21 @@ export class LoaderResolver {
91
92
// - /[param1]/[param2]/file.csv.js
92
93
// - /[param1]/[param2]/[param3].csv
93
94
// - /[param1]/[param2]/[param3].csv.js
94
- private findFile ( targetPath : string , { useStale} ) : Asset | Loader | undefined {
95
+ private findFile ( targetPath : string , { useStale} ) : Loader | undefined {
95
96
const ext = extname ( targetPath ) ;
96
97
const exts = [ ext , ...Array . from ( this . interpreters . keys ( ) , ( iext ) => ext + iext ) ] ;
97
98
const found = route ( this . root , targetPath . slice ( 0 , - ext . length ) , exts ) ;
98
99
if ( ! found ) return ;
99
100
const { path, params, ext : fext } = found ;
100
- const foundPath = join ( this . root , path ) ;
101
- if ( fext === ext ) return { path : foundPath } ;
101
+ if ( fext === ext ) return new StaticLoader ( { root : this . root , path, params } ) ;
102
+ const commandPath = join ( this . root , path ) ;
102
103
const [ command , ...args ] = this . interpreters . get ( fext . slice ( ext . length ) ) ! ;
103
- if ( command != null ) args . push ( foundPath ) ;
104
+ if ( command != null ) args . push ( commandPath ) ;
104
105
return new CommandLoader ( {
105
- command : command ?? foundPath ,
106
+ command : command ?? commandPath ,
106
107
args : params ? args . concat ( defineParams ( params ) ) : args ,
107
- path : foundPath ,
108
+ path,
109
+ params,
108
110
root : this . root ,
109
111
targetPath,
110
112
useStale
@@ -136,33 +138,35 @@ export class LoaderResolver {
136
138
// - /[param].tgz
137
139
// - /[param].zip.js
138
140
// - /[param].tgz.js
139
- private findArchive ( targetPath : string , { useStale} ) : Asset | Loader | undefined {
141
+ private findArchive ( targetPath : string , { useStale} ) : Loader | undefined {
140
142
const exts = this . getArchiveExtensions ( ) ;
141
143
for ( let dir = dirname ( targetPath ) , parent : string ; ( parent = dirname ( dir ) ) !== dir ; dir = parent ) {
142
144
const found = route ( this . root , dir , exts ) ;
143
145
if ( ! found ) continue ;
144
146
const { path, params, ext : fext } = found ;
145
- const foundPath = join ( this . root , path ) ;
146
147
const inflatePath = targetPath . slice ( dir . length + 1 ) ; // file.jpeg
147
148
if ( extractors . has ( fext ) ) {
148
149
const Extractor = extractors . get ( fext ) ! ;
149
150
return new Extractor ( {
150
151
preload : async ( ) => path , // /path/to.zip
151
152
inflatePath,
152
- path : foundPath ,
153
+ path,
154
+ params,
153
155
root : this . root ,
154
156
targetPath, // /path/to/file.jpg
155
157
useStale
156
158
} ) ;
157
159
}
158
160
const iext = extname ( fext ) ;
161
+ const commandPath = join ( this . root , path ) ;
159
162
const [ command , ...args ] = this . interpreters . get ( iext ) ! ;
160
- if ( command != null ) args . push ( foundPath ) ;
163
+ if ( command != null ) args . push ( commandPath ) ;
161
164
const eext = fext . slice ( 0 , - iext . length ) ; // .zip
162
165
const loader = new CommandLoader ( {
163
- command : command ?? foundPath ,
166
+ command : command ?? commandPath ,
164
167
args : params ? args . concat ( defineParams ( params ) ) : args ,
165
- path : foundPath ,
168
+ path,
169
+ params,
166
170
root : this . root ,
167
171
targetPath : dir + eext , // /path/to.zip
168
172
useStale
@@ -172,6 +176,7 @@ export class LoaderResolver {
172
176
preload : async ( options ) => loader . load ( options ) , // /path/to.zip.js
173
177
inflatePath,
174
178
path : loader . path ,
179
+ params,
175
180
root : this . root ,
176
181
targetPath,
177
182
useStale
@@ -186,42 +191,47 @@ export class LoaderResolver {
186
191
return exts ;
187
192
}
188
193
194
+ /**
195
+ * Returns the path to watch, relative to the current working directory, for
196
+ * the specified source path, relative to the source root.
197
+ */
189
198
getWatchPath ( path : string ) : string | undefined {
190
199
const exactPath = join ( this . root , path ) ;
191
200
if ( existsSync ( exactPath ) ) return exactPath ;
192
201
if ( exactPath . endsWith ( ".js" ) ) {
193
202
const jsxPath = exactPath + "x" ;
194
203
if ( existsSync ( jsxPath ) ) return jsxPath ;
204
+ return ; // loaders aren’t supported for .js
195
205
}
196
- return this . find ( path ) ?. path ;
206
+ const foundPath = this . find ( path ) ?. path ;
207
+ if ( foundPath ) return join ( this . root , foundPath ) ;
197
208
}
198
209
199
210
watchFiles ( path : string , watchPaths : Iterable < string > , callback : ( name : string ) => void ) {
200
211
return FileWatchers . of ( this , path , watchPaths , callback ) ;
201
212
}
202
213
203
214
/**
204
- * Returns the path to the backing file during preview, which is the source
205
- * file for the associated data loader if the file is generated by a loader.
215
+ * Returns the path to the backing file during preview, relative to the source
216
+ * root, which is the source file for the associated data loader if the file
217
+ * is generated by a loader.
206
218
*/
207
- private getSourceFilePath ( name : string ) : string {
208
- let path = name ;
219
+ private getSourceFilePath ( path : string ) : string {
209
220
if ( ! existsSync ( join ( this . root , path ) ) ) {
210
221
const loader = this . find ( path ) ;
211
- if ( loader ) path = relative ( this . root , loader . path ) ;
222
+ if ( loader ) return loader . path ;
212
223
}
213
224
return path ;
214
225
}
215
226
216
227
/**
217
- * Returns the path to the backing file during build, which is the cached
218
- * output file if the file is generated by a loader.
228
+ * Returns the path to the backing file during build, relative to the source
229
+ * root, which is the cached output file if the file is generated by a loader.
219
230
*/
220
- private getOutputFilePath ( name : string ) : string {
221
- let path = name ;
231
+ private getOutputFilePath ( path : string ) : string {
222
232
if ( ! existsSync ( join ( this . root , path ) ) ) {
223
233
const loader = this . find ( path ) ;
224
- if ( loader ) path = join ( ".observablehq" , "cache" , name ) ;
234
+ if ( loader ) return join ( ".observablehq" , "cache" , path ) ; // TODO Is this true for static files?
225
235
}
226
236
return path ;
227
237
}
@@ -262,25 +272,52 @@ function defineParams(params: Params): string[] {
262
272
. flatMap ( ( [ name , value ] ) => [ `--${ name } ` , value ] ) ;
263
273
}
264
274
265
- /** Used by LoaderResolver.find to represent a static file resolution. */
266
- export interface Asset {
267
- /** The path to the file relative to the current working directory. */
268
- path : string ;
269
- }
270
-
271
- export abstract class Loader {
275
+ export interface Loader {
272
276
/**
273
277
* The source root relative to the current working directory, such as src.
274
278
*/
275
279
readonly root : string ;
276
280
277
281
/**
278
- * The path to the loader script or executable relative to the current working
279
- * directory. This is exposed so that clients can check which file to watch to
280
- * see if the loader is edited (and in which case it needs to be re-run).
282
+ * The path to the loader script or executable relative to the source root.
283
+ * This is exposed so that clients can check which file to watch to see if the
284
+ * loader is edited (and in which case it needs to be re-run).
281
285
*/
282
286
readonly path : string ;
283
287
288
+ /** TODO */
289
+ readonly params : Params | undefined ;
290
+
291
+ /**
292
+ * Runs this loader, returning the path to the generated output file relative
293
+ * to the source root; this is typically within the .observablehq/cache folder
294
+ * within the source root.
295
+ */
296
+ load ( effects ?: LoadEffects ) : Promise < string > ;
297
+ }
298
+
299
+ /** Used by LoaderResolver.find to represent a static file resolution. */
300
+ class StaticLoader implements Loader {
301
+ readonly root : string ;
302
+ readonly path : string ;
303
+ readonly params : Params | undefined ;
304
+
305
+ constructor ( { root, path, params} : Omit < LoaderOptions , "targetPath" | "useStale" > ) {
306
+ this . root = root ;
307
+ this . path = path ;
308
+ this . params = params ;
309
+ }
310
+
311
+ async load ( ) {
312
+ return this . path ;
313
+ }
314
+ }
315
+
316
+ abstract class AbstractLoader implements Loader {
317
+ readonly root : string ;
318
+ readonly path : string ;
319
+ readonly params : Params | undefined ;
320
+
284
321
/**
285
322
* The path to the loader script’s output relative to the destination root.
286
323
* This is where the loader’s output is served, but the loader generates the
@@ -289,30 +326,27 @@ export abstract class Loader {
289
326
readonly targetPath : string ;
290
327
291
328
/**
292
- * Should the loader use a stale cache. true when building.
329
+ * Whether the loader should use a stale cache; true when building.
293
330
*/
294
331
readonly useStale ?: boolean ;
295
332
296
- constructor ( { root, path, targetPath, useStale} : LoaderOptions ) {
333
+ constructor ( { root, path, params , targetPath, useStale} : LoaderOptions ) {
297
334
this . root = root ;
298
335
this . path = path ;
336
+ this . params = params ;
299
337
this . targetPath = targetPath ;
300
338
this . useStale = useStale ;
301
339
}
302
340
303
- /**
304
- * Runs this loader, returning the path to the generated output file relative
305
- * to the source root; this is within the .observablehq/cache folder within
306
- * the source root.
307
- */
308
341
async load ( effects = defaultEffects ) : Promise < string > {
342
+ const loaderPath = join ( this . root , this . path ) ;
309
343
const key = join ( this . root , this . targetPath ) ;
310
344
let command = runningCommands . get ( key ) ;
311
345
if ( ! command ) {
312
346
command = ( async ( ) => {
313
347
const outputPath = join ( ".observablehq" , "cache" , this . targetPath ) ;
314
348
const cachePath = join ( this . root , outputPath ) ;
315
- const loaderStat = await maybeStat ( this . path ) ;
349
+ const loaderStat = await maybeStat ( loaderPath ) ;
316
350
const cacheStat = await maybeStat ( cachePath ) ;
317
351
if ( ! cacheStat ) effects . output . write ( faint ( "[missing] " ) ) ;
318
352
else if ( cacheStat . mtimeMs < loaderStat ! . mtimeMs ) {
@@ -344,7 +378,7 @@ export abstract class Loader {
344
378
command . finally ( ( ) => runningCommands . delete ( key ) ) . catch ( ( ) => { } ) ;
345
379
runningCommands . set ( key , command ) ;
346
380
}
347
- effects . output . write ( `${ cyan ( "load" ) } ${ this . path } ${ faint ( "→" ) } ` ) ;
381
+ effects . output . write ( `${ cyan ( "load" ) } ${ loaderPath } ${ faint ( "→" ) } ` ) ;
348
382
const start = performance . now ( ) ;
349
383
command . then (
350
384
( path ) => {
@@ -370,7 +404,7 @@ interface CommandLoaderOptions extends LoaderOptions {
370
404
args : string [ ] ;
371
405
}
372
406
373
- class CommandLoader extends Loader {
407
+ class CommandLoader extends AbstractLoader {
374
408
/**
375
409
* The command to run, such as "node" for a JavaScript loader, "tsx" for
376
410
* TypeScript, and "sh" for a shell script. "noop" when we only need to
@@ -408,7 +442,7 @@ interface ZipExtractorOptions extends LoaderOptions {
408
442
inflatePath : string ;
409
443
}
410
444
411
- class ZipExtractor extends Loader {
445
+ class ZipExtractor extends AbstractLoader {
412
446
private readonly preload : Loader [ "load" ] ;
413
447
private readonly inflatePath : string ;
414
448
@@ -433,7 +467,7 @@ interface TarExtractorOptions extends LoaderOptions {
433
467
gunzip ?: boolean ;
434
468
}
435
469
436
- class TarExtractor extends Loader {
470
+ class TarExtractor extends AbstractLoader {
437
471
private readonly preload : Loader [ "load" ] ;
438
472
private readonly inflatePath : string ;
439
473
private readonly gunzip : boolean ;
0 commit comments