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, getLocalModuleHash, getModuleHash} from "./java
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 { getModuleResolver , resolveImportPath } from "./resolvers.js" ;
20
21
import type { Params } from "./route.js" ;
21
22
import { isParameterized , requote , route } from "./route.js" ;
@@ -51,6 +52,9 @@ const defaultEffects: LoadEffects = {
51
52
export interface LoadOptions {
52
53
/** Whether to use a stale cache; true when building. */
53
54
useStale ?: boolean ;
55
+
56
+ /** An asset server for chained data loaders. */
57
+ FILE_SERVER ?: string ;
54
58
}
55
59
56
60
export interface LoaderOptions {
@@ -61,7 +65,7 @@ export interface LoaderOptions {
61
65
}
62
66
63
67
export class LoaderResolver {
64
- private readonly root : string ;
68
+ readonly root : string ;
65
69
private readonly interpreters : Map < string , string [ ] > ;
66
70
67
71
constructor ( { root, interpreters} : { root : string ; interpreters ?: Record < string , string [ ] | null > } ) {
@@ -304,7 +308,21 @@ export class LoaderResolver {
304
308
const info = getFileInfo ( this . root , path ) ;
305
309
if ( ! info ) return createHash ( "sha256" ) . digest ( "hex" ) ;
306
310
const { hash} = info ;
307
- return path === name ? hash : createHash ( "sha256" ) . update ( hash ) . update ( String ( info . mtimeMs ) ) . digest ( "hex" ) ;
311
+ if ( path === name ) return hash ;
312
+ const hash2 = createHash ( "sha256" ) . update ( hash ) . update ( String ( info . mtimeMs ) ) ;
313
+ try {
314
+ for ( const path of JSON . parse (
315
+ readFileSync ( join ( this . root , ".observablehq" , "cache" , `${ name } __dependencies` ) , "utf-8" )
316
+ ) ) {
317
+ const info = getFileInfo ( this . root , this . getSourceFilePath ( path ) ) ;
318
+ if ( info ) hash2 . update ( info . hash ) . update ( String ( info . mtimeMs ) ) ;
319
+ }
320
+ } catch ( error ) {
321
+ if ( ! isEnoent ( error ) ) {
322
+ throw error ;
323
+ }
324
+ }
325
+ return hash2 . digest ( "hex" ) ;
308
326
}
309
327
310
328
getOutputFileHash ( name : string ) : string {
@@ -417,12 +435,37 @@ abstract class AbstractLoader implements Loader {
417
435
const outputPath = join ( ".observablehq" , "cache" , this . targetPath ) ;
418
436
const cachePath = join ( this . root , outputPath ) ;
419
437
const loaderStat = await maybeStat ( loaderPath ) ;
420
- const cacheStat = await maybeStat ( cachePath ) ;
421
- if ( ! cacheStat ) effects . output . write ( faint ( "[missing] " ) ) ;
422
- else if ( cacheStat . mtimeMs < loaderStat ! . mtimeMs ) {
423
- if ( useStale ) return effects . output . write ( faint ( "[using stale] " ) ) , outputPath ;
424
- else effects . output . write ( faint ( "[stale] " ) ) ;
425
- } else return effects . output . write ( faint ( "[fresh] " ) ) , outputPath ;
438
+ const paths = new Set ( [ cachePath ] ) ;
439
+ try {
440
+ for ( const path of JSON . parse ( await readFile ( `${ cachePath } __dependencies` , "utf-8" ) ) ) paths . add ( path ) ;
441
+ } catch ( error ) {
442
+ if ( ! isEnoent ( error ) ) {
443
+ throw error ;
444
+ }
445
+ }
446
+
447
+ const FRESH = 0 ;
448
+ const STALE = 1 ;
449
+ const MISSING = 2 ;
450
+ let status = FRESH ;
451
+ for ( const path of paths ) {
452
+ const cacheStat = await maybeStat ( path ) ;
453
+ if ( ! cacheStat ) {
454
+ status = MISSING ;
455
+ break ;
456
+ } else if ( cacheStat . mtimeMs < loaderStat ! . mtimeMs ) status = Math . max ( status , STALE ) ;
457
+ }
458
+ switch ( status ) {
459
+ case FRESH :
460
+ return effects . output . write ( faint ( "[fresh] " ) ) , outputPath ;
461
+ case STALE :
462
+ if ( useStale ) return effects . output . write ( faint ( "[using stale] " ) ) , outputPath ;
463
+ effects . output . write ( faint ( "[stale] " ) ) ;
464
+ break ;
465
+ case MISSING :
466
+ effects . output . write ( faint ( "[missing] " ) ) ;
467
+ break ;
468
+ }
426
469
const tempPath = join ( this . root , ".observablehq" , "cache" , `${ this . targetPath } .${ process . pid } ` ) ;
427
470
const errorPath = tempPath + ".err" ;
428
471
const errorStat = await maybeStat ( errorPath ) ;
@@ -434,15 +477,37 @@ abstract class AbstractLoader implements Loader {
434
477
await prepareOutput ( tempPath ) ;
435
478
await prepareOutput ( cachePath ) ;
436
479
const tempFd = await open ( tempPath , "w" ) ;
480
+
481
+ // Launch a server for chained data loaders. TODO configure host?
482
+ const dependencies = new Set < string > ( ) ;
483
+ const { server} = await preview ( { root : this . root , verbose : false , hostname : "127.0.0.1" , dependencies} ) ;
484
+ const address = server . address ( ) ;
485
+ if ( ! address || typeof address !== "object" )
486
+ throw new Error ( "Couldn't launch server for chained data loaders!" ) ;
487
+ const FILE_SERVER = `http://${ address . address } :${ address . port } /_file/` ;
488
+
437
489
try {
438
- await this . exec ( tempFd . createWriteStream ( { highWaterMark : 1024 * 1024 } ) , { useStale} , effects ) ;
490
+ await this . exec ( tempFd . createWriteStream ( { highWaterMark : 1024 * 1024 } ) , { useStale, FILE_SERVER } , effects ) ;
439
491
await rename ( tempPath , cachePath ) ;
440
492
} catch ( error ) {
441
493
await rename ( tempPath , errorPath ) ;
442
494
throw error ;
443
495
} finally {
444
496
await tempFd . close ( ) ;
445
497
}
498
+
499
+ const cachedeps = `${ cachePath } __dependencies` ;
500
+ if ( dependencies . size ) await writeFile ( cachedeps , JSON . stringify ( [ ...dependencies ] ) , "utf-8" ) ;
501
+ else
502
+ try {
503
+ await rm ( cachedeps ) ;
504
+ } catch ( error ) {
505
+ if ( ! isEnoent ( error ) ) throw error ;
506
+ }
507
+
508
+ // TODO: server.close() might be enough?
509
+ await new Promise ( ( closed ) => server . close ( closed ) ) ;
510
+
446
511
return outputPath ;
447
512
} ) ( ) ;
448
513
command . finally ( ( ) => runningCommands . delete ( key ) ) . catch ( ( ) => { } ) ;
@@ -495,8 +560,12 @@ class CommandLoader extends AbstractLoader {
495
560
this . args = args ;
496
561
}
497
562
498
- async exec ( output : WriteStream ) : Promise < void > {
499
- const subprocess = spawn ( this . command , this . args , { windowsHide : true , stdio : [ "ignore" , output , "inherit" ] } ) ;
563
+ async exec ( output : WriteStream , { FILE_SERVER } ) : Promise < void > {
564
+ const subprocess = spawn ( this . command , this . args , {
565
+ windowsHide : true ,
566
+ stdio : [ "ignore" , output , "inherit" ] ,
567
+ env : { ...process . env , FILE_SERVER }
568
+ } ) ;
500
569
const code = await new Promise ( ( resolve , reject ) => {
501
570
subprocess . on ( "error" , reject ) ;
502
571
subprocess . on ( "close" , resolve ) ;
0 commit comments