11import { execFile , spawn } from 'node:child_process' ;
2- import { mkdtemp , mkdir , readFile , rm , writeFile } from 'node:fs/promises' ;
2+ import {
3+ chmod ,
4+ copyFile ,
5+ mkdtemp ,
6+ mkdir ,
7+ readFile ,
8+ rm ,
9+ writeFile ,
10+ } from 'node:fs/promises' ;
311import { tmpdir } from 'node:os' ;
4- import { dirname , join , resolve } from 'node:path' ;
12+ import { delimiter , dirname , join , resolve } from 'node:path' ;
513import { fileURLToPath } from 'node:url' ;
614import { promisify } from 'node:util' ;
715
@@ -44,6 +52,56 @@ const executeStage = proofMode
4452 if ( proofFailure === id ) throw new Error ( 'proof failure' ) ;
4553 }
4654 : ( _id , run ) => run ( ) ;
55+ const packageOnly = process . argv . includes ( '--package-only' ) ;
56+ const releaseTarballIndex = process . argv . indexOf ( '--release-tarball' ) ;
57+ const releaseTarball =
58+ releaseTarballIndex === - 1
59+ ? undefined
60+ : resolve ( root , process . argv [ releaseTarballIndex + 1 ] ) ;
61+ const requiredPackageFiles = [
62+ 'README.md' ,
63+ 'package.json' ,
64+ 'dist/index.html' ,
65+ 'scripts/build-diff-data.mjs' ,
66+ 'scripts/cli-args.mjs' ,
67+ 'scripts/coding-agents.mjs' ,
68+ 'scripts/doctor.mjs' ,
69+ 'scripts/generate-summaries.mjs' ,
70+ 'scripts/present.mjs' ,
71+ 'scripts/serve-built.mjs' ,
72+ 'scripts/summary-path.mjs' ,
73+ ] ;
74+ const allowedPackageFile = / ^ ( R E A D M E (?: \. m d ) ? | L I C E N S E (?: \. m d ) ? | N O T I C E (?: \. m d ) ? | p a c k a g e \. j s o n | d i s t \/ .+ | s c r i p t s \/ (?: b u i l d - d i f f - d a t a | c l i - a r g s | c o d i n g - a g e n t s | d o c t o r | g e n e r a t e - s u m m a r i e s | p r e s e n t | s e r v e - b u i l t | s u m m a r y - p a t h ) \. m j s ) $ / ;
75+ const privatePackageFile = / ( ^ | \/ ) (?: \. e n v | \. n p m r c | \. g i t | \. g i t h u b | \. a g e n t s | \. c o d e x ) (?: \/ | $ ) | \. (?: p e m | k e y ) $ / i;
76+
77+ export function validatePackageManifest ( pack ) {
78+ const files = pack . files ?? [ ] ;
79+ const paths = new Set ( files . map ( ( file ) => file . path ) ) ;
80+ const missing = requiredPackageFiles . filter ( ( path ) => ! paths . has ( path ) ) ;
81+ const unexpected = files . filter ( ( file ) => ! allowedPackageFile . test ( file . path ) ) ;
82+ const privateFiles = files . filter ( ( file ) => privatePackageFile . test ( file . path ) ) ;
83+ const oversizedPackage = pack . unpackedSize > 12_000_000 ;
84+ const oversizedFile = files . some ( ( file ) => file . size > 1_000_000 ) ;
85+ const problems = [
86+ { present : missing . length > 0 , text : `missing ${ missing . join ( ', ' ) } ` } ,
87+ {
88+ present : unexpected . length > 0 ,
89+ text : `unexpected ${ unexpected . map ( ( file ) => file . path ) . join ( ', ' ) } ` ,
90+ } ,
91+ {
92+ present : privateFiles . length > 0 ,
93+ text : `private ${ privateFiles . map ( ( file ) => file . path ) . join ( ', ' ) } ` ,
94+ } ,
95+ { present : oversizedPackage , text : 'package exceeds 12 MB' } ,
96+ { present : oversizedFile , text : 'file exceeds 1 MB' } ,
97+ ]
98+ . filter ( ( problem ) => problem . present )
99+ . map ( ( problem ) => problem . text ) ;
100+
101+ if ( problems . length ) {
102+ throw new Error ( `Package manifest failed: ${ problems . join ( '; ' ) } ` ) ;
103+ }
104+ }
47105
48106async function runStage ( id , name , run ) {
49107 console . log ( `\n==> ${ name } ` ) ;
@@ -57,6 +115,67 @@ async function runStage(id, name, run) {
57115 console . log ( `✓ ${ name } ` ) ;
58116}
59117
118+ async function makeSmokeCommandFixtures ( consumerRoot ) {
119+ const bin = join ( consumerRoot , 'bin' ) ;
120+ const windows = process . platform === 'win32' ;
121+ const extension = windows ? '.cmd' : '' ;
122+ const contents = windows
123+ ? '@echo off\r\necho test version\r\n'
124+ : '#!/bin/sh\nprintf "%s\\n" "test version"\n' ;
125+ await mkdir ( bin ) ;
126+ for ( const command of [ 'git' , 'gh' , 'codex' ] ) {
127+ const path = join ( bin , `${ command } ${ extension } ` ) ;
128+ await writeFile ( path , contents ) ;
129+ await chmod ( path , 0o755 ) ;
130+ }
131+ return bin ;
132+ }
133+
134+ async function makeSmokeRuntimeFixture ( consumerRoot ) {
135+ const fixture = join ( consumerRoot , 'fixture' ) ;
136+ await mkdir ( fixture ) ;
137+ await execFileAsync ( 'git' , [ 'init' , '-q' ] , { cwd : fixture } ) ;
138+ await execFileAsync ( 'git' , [ 'config' , 'user.email' , 'release@example.test' ] , {
139+ cwd : fixture ,
140+ } ) ;
141+ await execFileAsync ( 'git' , [ 'config' , 'user.name' , 'Release test' ] , {
142+ cwd : fixture ,
143+ } ) ;
144+ await writeFile ( join ( fixture , 'changed.txt' ) , 'before\n' ) ;
145+ await execFileAsync ( 'git' , [ 'add' , 'changed.txt' ] , { cwd : fixture } ) ;
146+ await execFileAsync ( 'git' , [ 'commit' , '-qm' , 'base' ] , { cwd : fixture } ) ;
147+ await writeFile ( join ( fixture , 'changed.txt' ) , 'after\n' ) ;
148+ const runtimeOutput = join ( consumerRoot , 'runtime.json' ) ;
149+ await execFileAsync (
150+ process . execPath ,
151+ [
152+ resolve (
153+ consumerRoot ,
154+ 'node_modules/diffsplain/scripts/build-diff-data.mjs' ,
155+ ) ,
156+ '--repo' ,
157+ fixture ,
158+ '--output' ,
159+ runtimeOutput ,
160+ ] ,
161+ { cwd : consumerRoot } ,
162+ ) ;
163+ return JSON . parse ( await readFile ( runtimeOutput , 'utf8' ) ) ;
164+ }
165+
166+ function verifySmokeResults ( { packageJson, version, help, doctor, runtime } ) {
167+ const checks = [
168+ packageJson . name === 'diffsplain' ,
169+ version . stdout . includes ( packageJson . version ) ,
170+ help . stdout . includes ( 'Usage:' ) ,
171+ doctor . stdout . includes ( 'Diffsplain doctor' ) ,
172+ runtime . files ?. [ 0 ] ?. path === 'changed.txt' ,
173+ ] ;
174+ if ( checks . includes ( false ) ) {
175+ throw new Error ( 'packed package has the wrong name' ) ;
176+ }
177+ }
178+
60179async function smokeTestPackage ( ) {
61180 const packageRoot = await mkdtemp ( join ( tmpdir ( ) , 'diffsplain-package-' ) ) ;
62181 const consumerRoot = join ( packageRoot , 'consumer' ) ;
@@ -68,6 +187,11 @@ async function smokeTestPackage() {
68187 ) ;
69188 const [ pack ] = JSON . parse ( stdout ) ;
70189 const tarball = join ( packageRoot , pack . filename ) ;
190+ validatePackageManifest ( pack ) ;
191+ if ( releaseTarball ) {
192+ await mkdir ( dirname ( releaseTarball ) , { recursive : true } ) ;
193+ await copyFile ( tarball , releaseTarball ) ;
194+ }
71195
72196 await mkdir ( consumerRoot ) ;
73197 await writeFile (
@@ -87,13 +211,20 @@ async function smokeTestPackage() {
87211 'node_modules/diffsplain' ,
88212 packageJson . bin . diffsplain ,
89213 ) ;
90- await execFileAsync ( process . execPath , [ executable , '--version' ] , {
214+ const version = await execFileAsync ( process . execPath , [ executable , '--version' ] , {
215+ cwd : consumerRoot ,
216+ } ) ;
217+ const help = await execFileAsync ( process . execPath , [ executable , '--help' ] , {
91218 cwd : consumerRoot ,
92219 } ) ;
93220
94- if ( packageJson . name !== 'diffsplain' ) {
95- throw new Error ( 'packed package has the wrong name' ) ;
96- }
221+ const bin = await makeSmokeCommandFixtures ( consumerRoot ) ;
222+ const doctor = await execFileAsync ( process . execPath , [ executable , 'doctor' ] , {
223+ cwd : consumerRoot ,
224+ env : { ...process . env , PATH : `${ bin } ${ delimiter } ${ process . env . PATH } ` } ,
225+ } ) ;
226+ const runtime = await makeSmokeRuntimeFixture ( consumerRoot ) ;
227+ verifySmokeResults ( { packageJson, version, help, doctor, runtime } ) ;
97228 } finally {
98229 await rm ( packageRoot , { force : true , recursive : true } ) ;
99230 }
@@ -107,12 +238,21 @@ const stages = [
107238 [ 'docs' , 'Production docs build' , ( ) => runNpm ( [ 'run' , 'docs:build' ] ) ] ,
108239] ;
109240
110- try {
111- for ( const [ id , name , run ] of stages ) {
241+ export async function runCheck ( ) {
242+ const selectedStages = packageOnly
243+ ? stages . filter ( ( [ id ] ) => id === 'build' )
244+ : stages ;
245+ for ( const [ id , name , run ] of selectedStages ) {
112246 await runStage ( id , name , run ) ;
113247 }
114248 await runStage ( 'package' , 'Packed-package smoke test' , smokeTestPackage ) ;
115- } catch ( error ) {
116- console . error ( `\nCheck stopped: ${ error . message } ` ) ;
117- process . exitCode = 1 ;
249+ }
250+
251+ if ( resolve ( process . argv [ 1 ] ) === fileURLToPath ( import . meta. url ) ) {
252+ try {
253+ await runCheck ( ) ;
254+ } catch ( error ) {
255+ console . error ( `\nCheck stopped: ${ error . message } ` ) ;
256+ process . exitCode = 1 ;
257+ }
118258}
0 commit comments