99
1010import { createHash } from 'node:crypto' ;
1111import { readFile } from 'node:fs/promises' ;
12- import { globSync , statSync } from 'node:fs' ;
13- import { dirname , resolve } from 'node:path' ;
12+ import { existsSync , globSync , readFileSync , statSync } from 'node:fs' ;
13+ import { dirname , relative , resolve } from 'node:path' ;
1414import { fileURLToPath } from 'node:url' ;
1515
1616const appRoot = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' ) ;
@@ -19,9 +19,9 @@ export const webRoot = resolve(repoRoot, 'apps/pythinker-web');
1919export const bundleRoot = resolve ( appRoot , 'dist-web' ) ;
2020export const MANIFEST_NAME = '.web-bundle-manifest.json' ;
2121
22- // Everything the Vite build reads. `public/` is copied verbatim into the
23- // bundle, and the configs decide how the source is compiled, so a change to
24- // any of them makes the committed bundle stale.
22+ // Everything the Vite build reads out of a package . `public/` is copied
23+ // verbatim into the bundle, and the configs decide how the source is compiled,
24+ // so a change to any of them makes the committed bundle stale.
2525const INPUT_GLOBS = [
2626 'src/**/*' ,
2727 'public/**/*' ,
@@ -31,15 +31,57 @@ const INPUT_GLOBS = [
3131 'package.json' ,
3232] ;
3333
34- /** Sorted list of build-input paths, relative to apps/pythinker-web. */
34+ // The web app compiles workspace dependencies from source (they publish
35+ // `./src/index.ts` directly), so their files are build inputs too. Hashing only
36+ // apps/pythinker-web would let a change in one of them alter the bundle while
37+ // the fingerprint stayed put. The lockfile is included because a dependency
38+ // resolution change alters the bundle without touching any tracked source.
39+ const EXTRA_INPUTS = [ 'pnpm-lock.yaml' ] ;
40+
41+ /** Workspace package roots the web app depends on, transitively. */
42+ function workspaceDependencyRoots ( ) {
43+ const roots = [ ] ;
44+ const pending = [ webRoot ] ;
45+ const seen = new Set ( [ webRoot ] ) ;
46+ while ( pending . length > 0 ) {
47+ const root = pending . pop ( ) ;
48+ let manifest ;
49+ try {
50+ manifest = JSON . parse ( readFileSync ( resolve ( root , 'package.json' ) , 'utf8' ) ) ;
51+ } catch {
52+ continue ;
53+ }
54+ const deps = { ...manifest . dependencies , ...manifest . devDependencies } ;
55+ for ( const [ name , range ] of Object . entries ( deps ) ) {
56+ if ( typeof range !== 'string' || ! range . startsWith ( 'workspace:' ) ) continue ;
57+ const dir = name . replace ( / ^ @ [ ^ / ] + \/ / u, '' ) ;
58+ for ( const candidate of [ resolve ( repoRoot , 'packages' , dir ) , resolve ( repoRoot , 'apps' , dir ) ] ) {
59+ if ( seen . has ( candidate ) || ! existsSync ( resolve ( candidate , 'package.json' ) ) ) continue ;
60+ seen . add ( candidate ) ;
61+ roots . push ( candidate ) ;
62+ pending . push ( candidate ) ;
63+ break ;
64+ }
65+ }
66+ }
67+ return roots . sort ( ) ;
68+ }
69+
70+ /** Sorted list of build-input paths, relative to the repo root. */
3571export function webInputFiles ( ) {
3672 const seen = new Set ( ) ;
37- for ( const pattern of INPUT_GLOBS ) {
38- for ( const file of globSync ( pattern , { cwd : webRoot } ) ) {
39- const normalized = file . split ( '\\' ) . join ( '/' ) ;
40- if ( statSync ( resolve ( webRoot , normalized ) ) . isFile ( ) ) seen . add ( normalized ) ;
73+ for ( const root of [ webRoot , ...workspaceDependencyRoots ( ) ] ) {
74+ for ( const pattern of INPUT_GLOBS ) {
75+ for ( const file of globSync ( pattern , { cwd : root } ) ) {
76+ const full = resolve ( root , file ) ;
77+ if ( ! statSync ( full ) . isFile ( ) ) continue ;
78+ seen . add ( relative ( repoRoot , full ) . split ( '\\' ) . join ( '/' ) ) ;
79+ }
4180 }
4281 }
82+ for ( const file of EXTRA_INPUTS ) {
83+ if ( existsSync ( resolve ( repoRoot , file ) ) ) seen . add ( file ) ;
84+ }
4385 return [ ...seen ] . sort ( ) ;
4486}
4587
@@ -53,7 +95,7 @@ export async function computeWebInputHash() {
5395 for ( const file of files ) {
5496 digest . update ( file ) ;
5597 digest . update ( '\0' ) ;
56- digest . update ( await readFile ( resolve ( webRoot , file ) ) ) ;
98+ digest . update ( await readFile ( resolve ( repoRoot , file ) ) ) ;
5799 digest . update ( '\0' ) ;
58100 }
59101 return { hash : digest . digest ( 'hex' ) , fileCount : files . length } ;
0 commit comments