1
1
import { createHash } from "node:crypto" ;
2
2
import type { FSWatcher , WatchListener , WriteStream } from "node:fs" ;
3
- import { createReadStream , existsSync , statSync , watch } from "node:fs" ;
4
- import { open , readFile , rename , unlink } from "node:fs/promises" ;
3
+ import { createReadStream , existsSync , readFileSync , statSync , watch } from "node:fs" ;
4
+ import { open , readFile , rename , rm , unlink , writeFile } from "node:fs/promises" ;
5
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" ;
9
9
import { extract } from "tar-stream" ;
10
- import { enoent } from "./error.js" ;
10
+ import { enoent , isEnoent } from "./error.js" ;
11
11
import { maybeStat , prepareOutput , visitFiles } from "./files.js" ;
12
12
import { FileWatchers } from "./fileWatchers.js" ;
13
13
import { formatByteSize } from "./format.js" ;
@@ -16,6 +16,7 @@ import {findModule, getFileInfo} from "./javascript/module.js";
16
16
import type { Logger , Writer } from "./logger.js" ;
17
17
import type { MarkdownPage , ParseOptions } from "./markdown.js" ;
18
18
import { parseMarkdown } from "./markdown.js" ;
19
+ import { preview } from "./preview.js" ;
19
20
import type { Params } from "./route.js" ;
20
21
import { isParameterized , requote , route } from "./route.js" ;
21
22
import { cyan , faint , green , red , yellow } from "./tty.js" ;
@@ -50,6 +51,9 @@ const defaultEffects: LoadEffects = {
50
51
export interface LoadOptions {
51
52
/** Whether to use a stale cache; true when building. */
52
53
useStale ?: boolean ;
54
+
55
+ /** An asset server for chained data loaders. */
56
+ FILE_SERVER ?: string ;
53
57
}
54
58
55
59
export interface LoaderOptions {
@@ -60,7 +64,7 @@ export interface LoaderOptions {
60
64
}
61
65
62
66
export class LoaderResolver {
63
- private readonly root : string ;
67
+ readonly root : string ;
64
68
private readonly interpreters : Map < string , string [ ] > ;
65
69
66
70
constructor ( { root, interpreters} : { root : string ; interpreters ?: Record < string , string [ ] | null > } ) {
@@ -303,7 +307,21 @@ export class LoaderResolver {
303
307
const info = getFileInfo ( this . root , path ) ;
304
308
if ( ! info ) return createHash ( "sha256" ) . digest ( "hex" ) ;
305
309
const { hash} = info ;
306
- return path === name ? hash : createHash ( "sha256" ) . update ( hash ) . update ( String ( info . mtimeMs ) ) . digest ( "hex" ) ;
310
+ if ( path === name ) return hash ;
311
+ const hash2 = createHash ( "sha256" ) . update ( hash ) . update ( String ( info . mtimeMs ) ) ;
312
+ try {
313
+ for ( const path of JSON . parse (
314
+ readFileSync ( join ( this . root , ".observablehq" , "cache" , `${ name } __dependencies` ) , "utf-8" )
315
+ ) ) {
316
+ const info = getFileInfo ( this . root , this . getSourceFilePath ( path ) ) ;
317
+ if ( info ) hash2 . update ( info . hash ) . update ( String ( info . mtimeMs ) ) ;
318
+ }
319
+ } catch ( error ) {
320
+ if ( ! isEnoent ( error ) ) {
321
+ throw error ;
322
+ }
323
+ }
324
+ return hash2 . digest ( "hex" ) ;
307
325
}
308
326
309
327
getSourceInfo ( name : string ) : FileInfo | undefined {
@@ -394,12 +412,37 @@ abstract class AbstractLoader implements Loader {
394
412
const outputPath = join ( ".observablehq" , "cache" , this . targetPath ) ;
395
413
const cachePath = join ( this . root , outputPath ) ;
396
414
const loaderStat = await maybeStat ( loaderPath ) ;
397
- const cacheStat = await maybeStat ( cachePath ) ;
398
- if ( ! cacheStat ) effects . output . write ( faint ( "[missing] " ) ) ;
399
- else if ( cacheStat . mtimeMs < loaderStat ! . mtimeMs ) {
400
- if ( useStale ) return effects . output . write ( faint ( "[using stale] " ) ) , outputPath ;
401
- else effects . output . write ( faint ( "[stale] " ) ) ;
402
- } else return effects . output . write ( faint ( "[fresh] " ) ) , outputPath ;
415
+ const paths = new Set ( [ cachePath ] ) ;
416
+ try {
417
+ for ( const path of JSON . parse ( await readFile ( `${ cachePath } __dependencies` , "utf-8" ) ) ) paths . add ( path ) ;
418
+ } catch ( error ) {
419
+ if ( ! isEnoent ( error ) ) {
420
+ throw error ;
421
+ }
422
+ }
423
+
424
+ const FRESH = 0 ;
425
+ const STALE = 1 ;
426
+ const MISSING = 2 ;
427
+ let status = FRESH ;
428
+ for ( const path of paths ) {
429
+ const cacheStat = await maybeStat ( path ) ;
430
+ if ( ! cacheStat ) {
431
+ status = MISSING ;
432
+ break ;
433
+ } else if ( cacheStat . mtimeMs < loaderStat ! . mtimeMs ) status = Math . max ( status , STALE ) ;
434
+ }
435
+ switch ( status ) {
436
+ case FRESH :
437
+ return effects . output . write ( faint ( "[fresh] " ) ) , outputPath ;
438
+ case STALE :
439
+ if ( useStale ) return effects . output . write ( faint ( "[using stale] " ) ) , outputPath ;
440
+ effects . output . write ( faint ( "[stale] " ) ) ;
441
+ break ;
442
+ case MISSING :
443
+ effects . output . write ( faint ( "[missing] " ) ) ;
444
+ break ;
445
+ }
403
446
const tempPath = join ( this . root , ".observablehq" , "cache" , `${ this . targetPath } .${ process . pid } ` ) ;
404
447
const errorPath = tempPath + ".err" ;
405
448
const errorStat = await maybeStat ( errorPath ) ;
@@ -411,15 +454,37 @@ abstract class AbstractLoader implements Loader {
411
454
await prepareOutput ( tempPath ) ;
412
455
await prepareOutput ( cachePath ) ;
413
456
const tempFd = await open ( tempPath , "w" ) ;
457
+
458
+ // Launch a server for chained data loaders. TODO configure host?
459
+ const dependencies = new Set < string > ( ) ;
460
+ const { server} = await preview ( { root : this . root , verbose : false , hostname : "127.0.0.1" , dependencies} ) ;
461
+ const address = server . address ( ) ;
462
+ if ( ! address || typeof address !== "object" )
463
+ throw new Error ( "Couldn't launch server for chained data loaders!" ) ;
464
+ const FILE_SERVER = `http://${ address . address } :${ address . port } /_file/` ;
465
+
414
466
try {
415
- await this . exec ( tempFd . createWriteStream ( { highWaterMark : 1024 * 1024 } ) , { useStale} , effects ) ;
467
+ await this . exec ( tempFd . createWriteStream ( { highWaterMark : 1024 * 1024 } ) , { useStale, FILE_SERVER } , effects ) ;
416
468
await rename ( tempPath , cachePath ) ;
417
469
} catch ( error ) {
418
470
await rename ( tempPath , errorPath ) ;
419
471
throw error ;
420
472
} finally {
421
473
await tempFd . close ( ) ;
422
474
}
475
+
476
+ const cachedeps = `${ cachePath } __dependencies` ;
477
+ if ( dependencies . size ) await writeFile ( cachedeps , JSON . stringify ( [ ...dependencies ] ) , "utf-8" ) ;
478
+ else
479
+ try {
480
+ await rm ( cachedeps ) ;
481
+ } catch ( error ) {
482
+ if ( ! isEnoent ( error ) ) throw error ;
483
+ }
484
+
485
+ // TODO: server.close() might be enough?
486
+ await new Promise ( ( closed ) => server . close ( closed ) ) ;
487
+
423
488
return outputPath ;
424
489
} ) ( ) ;
425
490
command . finally ( ( ) => runningCommands . delete ( key ) ) . catch ( ( ) => { } ) ;
@@ -472,8 +537,12 @@ class CommandLoader extends AbstractLoader {
472
537
this . args = args ;
473
538
}
474
539
475
- async exec ( output : WriteStream ) : Promise < void > {
476
- const subprocess = spawn ( this . command , this . args , { windowsHide : true , stdio : [ "ignore" , output , "inherit" ] } ) ;
540
+ async exec ( output : WriteStream , { FILE_SERVER } ) : Promise < void > {
541
+ const subprocess = spawn ( this . command , this . args , {
542
+ windowsHide : true ,
543
+ stdio : [ "ignore" , output , "inherit" ] ,
544
+ env : { ...process . env , FILE_SERVER }
545
+ } ) ;
477
546
const code = await new Promise ( ( resolve , reject ) => {
478
547
subprocess . on ( "error" , reject ) ;
479
548
subprocess . on ( "close" , resolve ) ;
0 commit comments