@@ -3,6 +3,7 @@ jest.mock('fs-extra', () => ({
33 default : {
44 pathExists : jest . fn ( ) ,
55 readJson : jest . fn ( ) ,
6+ copy : jest . fn ( ) ,
67 } ,
78} ) ) ;
89
@@ -26,10 +27,15 @@ import path from 'path';
2627import fs from 'fs-extra' ;
2728import { execa } from 'execa' ;
2829import ghPages from 'gh-pages' ;
29- import { runDeployToGitHubPages } from '../gh-pages.js' ;
30+ import {
31+ getGitHubPagesPublicPath ,
32+ normalizeDeployBasePath ,
33+ runDeployToGitHubPages ,
34+ } from '../gh-pages.js' ;
3035
3136const mockPathExists = fs . pathExists as jest . MockedFunction < typeof fs . pathExists > ;
3237const mockReadJson = fs . readJson as jest . MockedFunction < typeof fs . readJson > ;
38+ const mockCopy = fs . copy as jest . MockedFunction < typeof fs . copy > ;
3339const mockExeca = execa as jest . MockedFunction < typeof execa > ;
3440const mockGhPagesPublish = ghPages . publish as jest . MockedFunction < typeof ghPages . publish > ;
3541
@@ -38,7 +44,11 @@ const cwd = '/tmp/my-app';
3844/** Setup pathExists to return values based on path (avoids brittle call-order chains) */
3945function setupPathExists ( checks : Record < string , boolean > ) {
4046 mockPathExists . mockImplementation ( ( p : string ) => {
47+ if ( p . endsWith ( `${ path . sep } 404.html` ) || p . endsWith ( '/404.html' ) ) {
48+ return Promise . resolve ( checks [ '404.html' ] ?? false ) ;
49+ }
4150 for ( const [ key , value ] of Object . entries ( checks ) ) {
51+ if ( key === '404.html' ) continue ;
4252 if ( p . includes ( key ) || p === path . join ( cwd , key ) ) return Promise . resolve ( value ) ;
4353 }
4454 return Promise . resolve ( checks [ '*' ] ?? false ) ;
@@ -56,6 +66,22 @@ describe('runDeployToGitHubPages', () => {
5666 consoleLogSpy . mockRestore ( ) ;
5767 } ) ;
5868
69+ describe ( 'getGitHubPagesPublicPath / normalizeDeployBasePath' , ( ) => {
70+ it ( 'uses root for user GitHub Pages repo' , ( ) => {
71+ expect ( getGitHubPagesPublicPath ( 'octocat' , 'octocat.github.io' ) ) . toBe ( '/' ) ;
72+ } ) ;
73+
74+ it ( 'uses /repo/ for project pages' , ( ) => {
75+ expect ( getGitHubPagesPublicPath ( 'octocat' , 'Hello-World' ) ) . toBe ( '/Hello-World/' ) ;
76+ } ) ;
77+
78+ it ( 'normalizes base path overrides' , ( ) => {
79+ expect ( normalizeDeployBasePath ( '/' ) ) . toBe ( '/' ) ;
80+ expect ( normalizeDeployBasePath ( 'my-app' ) ) . toBe ( '/my-app/' ) ;
81+ expect ( normalizeDeployBasePath ( '/my-app' ) ) . toBe ( '/my-app/' ) ;
82+ } ) ;
83+ } ) ;
84+
5985 it ( 'throws when package.json does not exist' , async ( ) => {
6086 mockPathExists . mockImplementation ( ( p : string ) =>
6187 Promise . resolve ( path . join ( cwd , 'package.json' ) !== p )
@@ -108,14 +134,20 @@ describe('runDeployToGitHubPages', () => {
108134 exitCode : 0 ,
109135 } as Awaited < ReturnType < typeof execa > > ) ;
110136 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
137+ mockCopy . mockResolvedValue ( undefined ) ;
111138
112139 await runDeployToGitHubPages ( cwd , { skipBuild : false } ) ;
113140
114141 expect ( mockExeca ) . toHaveBeenCalledTimes ( 2 ) ; // git, npm run build
115142 expect ( mockExeca ) . toHaveBeenNthCalledWith ( 2 , 'npm' , [ 'run' , 'build' ] , {
116143 cwd,
117144 stdio : 'inherit' ,
145+ env : expect . objectContaining ( { ASSET_PATH : '/repo/' } ) ,
118146 } ) ;
147+ expect ( mockCopy ) . toHaveBeenCalledWith (
148+ path . join ( cwd , 'dist' , 'index.html' ) ,
149+ path . join ( cwd , 'dist' , '404.html' )
150+ ) ;
119151 expect ( mockGhPagesPublish ) . toHaveBeenCalledWith (
120152 path . join ( cwd , 'dist' ) ,
121153 { branch : 'gh-pages' , repo : 'https://github.com/user/repo.git' } ,
@@ -140,15 +172,36 @@ describe('runDeployToGitHubPages', () => {
140172 exitCode : 0 ,
141173 } as Awaited < ReturnType < typeof execa > > ) ;
142174 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
175+ mockCopy . mockResolvedValue ( undefined ) ;
143176
144177 await runDeployToGitHubPages ( cwd , { skipBuild : false } ) ;
145178
146- expect ( mockExeca ) . toHaveBeenNthCalledWith ( 2 , 'yarn' , [ 'build' ] , {
179+ expect ( mockExeca ) . toHaveBeenNthCalledWith ( 2 , 'yarn' , [ 'run' , ' build'] , {
147180 cwd,
148181 stdio : 'inherit' ,
182+ env : expect . objectContaining ( { ASSET_PATH : '/repo/' } ) ,
149183 } ) ;
150184 } ) ;
151185
186+ it ( 'does not set ASSET_PATH for <user>.github.io repository (site root)' , async ( ) => {
187+ setupPathExists ( { 'package.json' : true , 'dist' : true } ) ;
188+ mockReadJson . mockResolvedValueOnce ( {
189+ scripts : { build : 'webpack --config webpack.prod.js' } ,
190+ } ) ;
191+ mockExeca . mockResolvedValue ( {
192+ stdout : 'https://github.com/octocat/octocat.github.io.git' ,
193+ stderr : '' ,
194+ exitCode : 0 ,
195+ } as Awaited < ReturnType < typeof execa > > ) ;
196+ mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
197+ mockCopy . mockResolvedValue ( undefined ) ;
198+
199+ await runDeployToGitHubPages ( cwd , { skipBuild : false } ) ;
200+
201+ const buildOpts = mockExeca . mock . calls [ 1 ] [ 2 ] as { env ?: NodeJS . ProcessEnv } ;
202+ expect ( buildOpts . env ?. ASSET_PATH ) . toBeUndefined ( ) ;
203+ } ) ;
204+
152205 it ( 'uses pnpm when pnpm-lock.yaml exists (and no yarn.lock)' , async ( ) => {
153206 setupPathExists ( { 'package.json' : true , 'pnpm-lock.yaml' : true , 'dist' : true } ) ;
154207 mockReadJson . mockResolvedValueOnce ( {
@@ -160,12 +213,14 @@ describe('runDeployToGitHubPages', () => {
160213 exitCode : 0 ,
161214 } as Awaited < ReturnType < typeof execa > > ) ;
162215 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
216+ mockCopy . mockResolvedValue ( undefined ) ;
163217
164218 await runDeployToGitHubPages ( cwd , { skipBuild : false } ) ;
165219
166- expect ( mockExeca ) . toHaveBeenNthCalledWith ( 2 , 'pnpm' , [ 'build' ] , {
220+ expect ( mockExeca ) . toHaveBeenNthCalledWith ( 2 , 'pnpm' , [ 'run' , ' build' , '--' , '--base' , '/repo/ '] , {
167221 cwd,
168222 stdio : 'inherit' ,
223+ env : expect . objectContaining ( { ASSET_PATH : '/repo/' } ) ,
169224 } ) ;
170225 } ) ;
171226
@@ -177,6 +232,7 @@ describe('runDeployToGitHubPages', () => {
177232 exitCode : 0 ,
178233 } as Awaited < ReturnType < typeof execa > > ) ;
179234 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
235+ mockCopy . mockResolvedValue ( undefined ) ;
180236
181237 await runDeployToGitHubPages ( cwd , { skipBuild : true } ) ;
182238
@@ -228,6 +284,7 @@ describe('runDeployToGitHubPages', () => {
228284 exitCode : 0 ,
229285 } as Awaited < ReturnType < typeof execa > > ) ;
230286 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) => cb ( null ) ) ;
287+ mockCopy . mockResolvedValue ( undefined ) ;
231288
232289 await runDeployToGitHubPages ( cwd , {
233290 skipBuild : true ,
@@ -271,6 +328,7 @@ describe('runDeployToGitHubPages', () => {
271328 stderr : '' ,
272329 exitCode : 0 ,
273330 } as Awaited < ReturnType < typeof execa > > ) ;
331+ mockCopy . mockResolvedValue ( undefined ) ;
274332 mockGhPagesPublish . mockImplementation ( ( _dir , _opts , cb ) =>
275333 cb ( new Error ( 'Deploy failed' ) )
276334 ) ;
0 commit comments