11import { resolve } from 'node:path'
22import { beforeEach , describe , expect , it , vi } from 'vitest'
3- import type { FeatureName } from '../../constants/config.js'
3+ import { type FeatureName , getStackConfig } from '../../constants/config.js'
44
55vi . mock ( 'node:fs/promises' , ( ) => ( {
66 rm : vi . fn ( ) . mockResolvedValue ( undefined ) ,
@@ -58,6 +58,30 @@ function getWrittenPackageJson(): Record<string, unknown> {
5858 return JSON . parse ( lastCall [ 1 ] as string )
5959}
6060
61+ // Reads the workspaces field in either form (string[] or { packages }).
62+ function getWorkspacePackages ( pkg : Record < string , unknown > ) : string [ ] {
63+ const workspaces = pkg . workspaces
64+
65+ if ( Array . isArray ( workspaces ) ) {
66+ return workspaces as string [ ]
67+ }
68+
69+ const packages = ( workspaces as { packages ?: unknown } | undefined ) ?. packages
70+ return Array . isArray ( packages ) ? ( packages as string [ ] ) : [ ]
71+ }
72+
73+ // Dirs removed for a selection, derived from config — keeps the invariant config-driven instead of
74+ // hardcoding carpincho-wallet.
75+ function removedCantonDirs ( selected : FeatureName [ ] ) : string [ ] {
76+ return Object . entries ( getStackConfig ( 'canton' ) . features )
77+ . filter ( ( [ name , definition ] ) => ! selected . includes ( name ) && ( definition . paths ?. length ?? 0 ) > 0 )
78+ . flatMap ( ( [ , definition ] ) => definition . paths as string [ ] )
79+ }
80+
81+ function entryTargetsRemovedDir ( entry : string , removedDirs : string [ ] ) : boolean {
82+ return removedDirs . some ( ( dir ) => entry === dir || entry . startsWith ( `${ dir } /` ) )
83+ }
84+
6185function mockEvmPackageJson ( ) {
6286 vi . mocked ( readFileSync ) . mockReturnValue (
6387 JSON . stringify ( {
@@ -75,31 +99,53 @@ function mockEvmPackageJson() {
7599 )
76100}
77101
78- // Mirrors the real root package.json on BootNodeDev/cn-dappbooster@main.
79- function mockCantonPackageJson ( ) {
102+ // Mirrors cn-dappbooster@main's root package.json, including the workspaces array — carpincho-wallet
103+ // is a workspace, so deselecting carpincho must prune it.
104+ const CANTON_WORKSPACES = [
105+ 'canton-connect-kit' ,
106+ 'carpincho-wallet' ,
107+ 'canton-barebones' ,
108+ 'canton-barebones/wallet-service' ,
109+ 'dapp/daml' ,
110+ 'dapp/e2e' ,
111+ 'dapp/frontend' ,
112+ ]
113+
114+ const CANTON_SCRIPTS = {
115+ 'canton:up' : 'npm --prefix canton-barebones run up' ,
116+ 'canton:down' : 'npm --prefix canton-barebones run down' ,
117+ 'canton:health' : 'npm --prefix canton-barebones run health' ,
118+ 'canton:token' : 'npm --prefix canton-barebones run token' ,
119+ 'build-dar' : 'bash scripts/build-dar.sh' ,
120+ 'deploy-dar' : 'bash canton-barebones/scripts/deploy-dar.sh' ,
121+ 'wallet:dev' : 'npm --prefix carpincho-wallet run dev' ,
122+ 'wallet-service:dev' : 'npm --prefix canton-barebones/wallet-service run dev' ,
123+ 'wallet-service:health' : 'curl -fsS http://localhost:3010/health' ,
124+ 'carpincho:build:extension' : 'npm --prefix carpincho-wallet run build:extension' ,
125+ 'app:dev' : 'npm --prefix dapp/frontend run dev -- --host localhost --port 3012 --strictPort' ,
126+ lint : 'biome check' ,
127+ 'lint:fix' : 'biome check --write' ,
128+ format : 'biome format --write' ,
129+ e2e : 'npm --prefix dapp/e2e test' ,
130+ 'e2e:headed' : 'npm --prefix dapp/e2e run test:headed' ,
131+ 'e2e:ui' : 'npm --prefix dapp/e2e run test:ui' ,
132+ prepare : 'husky' ,
133+ }
134+
135+ const CANTON_DEV_DEPS = {
136+ husky : '^9.1.7' ,
137+ 'lint-staged' : '^17.0.4' ,
138+ '@commitlint/cli' : '^21.0.1' ,
139+ '@commitlint/config-conventional' : '^21.0.1' ,
140+ }
141+
142+ // Pass { packages } to exercise the object form; defaults to the string[] form.
143+ function mockCantonPackageJson ( workspaces : unknown = CANTON_WORKSPACES ) {
80144 vi . mocked ( readFileSync ) . mockReturnValue (
81145 JSON . stringify ( {
82- scripts : {
83- 'canton:up' : 'npm --prefix canton-barebones run up' ,
84- 'canton:down' : 'npm --prefix canton-barebones run down' ,
85- 'build-dar' : 'bash scripts/build-dar.sh' ,
86- 'deploy-dar' : 'bash canton-barebones/scripts/deploy-dar.sh' ,
87- 'wallet:dev' : 'npm --prefix carpincho-wallet run dev' ,
88- 'wallet-service:dev' : 'npm --prefix canton-barebones/wallet-service run dev' ,
89- 'carpincho:build:extension' : 'npm --prefix carpincho-wallet run build:extension' ,
90- 'app:dev' :
91- 'npm --prefix dapp/frontend run dev -- --host localhost --port 3012 --strictPort' ,
92- lint : 'biome check' ,
93- e2e : 'npm --prefix dapp/e2e test' ,
94- 'e2e:headed' : 'npm --prefix dapp/e2e run test:headed' ,
95- prepare : 'husky' ,
96- } ,
97- devDependencies : {
98- husky : '^9.1.7' ,
99- 'lint-staged' : '^17.0.4' ,
100- '@commitlint/cli' : '^21.0.1' ,
101- '@commitlint/config-conventional' : '^21.0.1' ,
102- } ,
146+ workspaces,
147+ scripts : CANTON_SCRIPTS ,
148+ devDependencies : CANTON_DEV_DEPS ,
103149 } ) ,
104150 )
105151}
@@ -411,6 +457,12 @@ describe('cleanupFiles — canton', () => {
411457 expect ( execFile ) . toHaveBeenCalledWith ( 'git' , [ 'add' , '.' ] , { cwd : '/project/my_app' } )
412458 } )
413459
460+ it ( 'keeps the full workspaces array (nothing removed)' , async ( ) => {
461+ await cleanupFiles ( 'canton' , '/project/my_app' , 'full' )
462+
463+ expect ( getWorkspacePackages ( getWrittenPackageJson ( ) ) ) . toEqual ( CANTON_WORKSPACES )
464+ } )
465+
414466 it ( 'makes the initial commit with --no-verify so kept project hooks cannot block it' , async ( ) => {
415467 await cleanupFiles ( 'canton' , '/project/my_app' , 'full' )
416468
@@ -496,6 +548,41 @@ describe('cleanupFiles — canton', () => {
496548 expect ( scripts [ 'wallet:dev' ] ) . toBeUndefined ( )
497549 expect ( scripts [ 'carpincho:build:extension' ] ) . toBeUndefined ( )
498550 } )
551+
552+ it ( 'prunes carpincho-wallet from the workspaces array but keeps the rest' , async ( ) => {
553+ await cleanupFiles ( 'canton' , '/project/my_app' , 'custom' , [ 'github' , 'precommit' , 'llm' ] )
554+
555+ const workspaces = getWorkspacePackages ( getWrittenPackageJson ( ) )
556+ expect ( workspaces ) . not . toContain ( 'carpincho-wallet' )
557+ expect ( workspaces ) . toContain ( 'canton-barebones' )
558+ expect ( workspaces ) . toContain ( 'dapp/frontend' )
559+ } )
560+
561+ // The invariant the bug violated — asserted generally, not just for carpincho-wallet.
562+ it ( 'leaves no workspace entry pointing at a removed directory' , async ( ) => {
563+ const selected : FeatureName [ ] = [ 'github' , 'precommit' , 'llm' ]
564+ await cleanupFiles ( 'canton' , '/project/my_app' , 'custom' , selected )
565+
566+ const removedDirs = removedCantonDirs ( selected )
567+ const workspaces = getWorkspacePackages ( getWrittenPackageJson ( ) )
568+ for ( const entry of workspaces ) {
569+ expect ( entryTargetsRemovedDir ( entry , removedDirs ) ) . toBe ( false )
570+ }
571+ // Guard against the array being emptied wholesale.
572+ expect ( workspaces . length ) . toBeGreaterThan ( 0 )
573+ } )
574+
575+ it ( 'prunes the { packages } object form and preserves sibling keys' , async ( ) => {
576+ mockCantonPackageJson ( { packages : CANTON_WORKSPACES , nohoist : [ '**/react' ] } )
577+ await cleanupFiles ( 'canton' , '/project/my_app' , 'custom' , [ 'github' , 'precommit' , 'llm' ] )
578+
579+ const written = getWrittenPackageJson ( )
580+ expect ( Array . isArray ( written . workspaces ) ) . toBe ( false )
581+ const workspaces = written . workspaces as { packages : string [ ] ; nohoist : string [ ] }
582+ expect ( workspaces . packages ) . not . toContain ( 'carpincho-wallet' )
583+ expect ( workspaces . packages ) . toContain ( 'dapp/frontend' )
584+ expect ( workspaces . nohoist ) . toEqual ( [ '**/react' ] )
585+ } )
499586 } )
500587
501588 describe ( 'custom mode — llm deselected' , ( ) => {
@@ -512,6 +599,17 @@ describe('cleanupFiles — canton', () => {
512599 expect ( paths ) . toContain ( resolve ( '/project/my_app' , 'architecture.md' ) )
513600 expect ( paths ) . toContain ( resolve ( '/project/my_app' , 'llms.txt' ) )
514601 } )
602+
603+ // llm's paths aren't workspaces — removing it must not touch the array.
604+ it ( 'leaves the workspaces array intact (its paths are not workspaces)' , async ( ) => {
605+ await cleanupFiles ( 'canton' , '/project/my_app' , 'custom' , [
606+ 'github' ,
607+ 'precommit' ,
608+ 'carpincho' ,
609+ ] )
610+
611+ expect ( getWorkspacePackages ( getWrittenPackageJson ( ) ) ) . toEqual ( CANTON_WORKSPACES )
612+ } )
515613 } )
516614
517615 describe ( 'onProgress callback' , ( ) => {
0 commit comments