44 */
55
66import { describe , expect , test } from "bun:test" ;
7- import { readFileSync } from "node:fs" ;
7+ import { mkdirSync , readFileSync , writeFileSync } from "node:fs" ;
88import { dirname , join , resolve } from "node:path" ;
99import { parseSync } from "oxc-parser" ;
10+ import { publint } from "publint" ;
11+ import { formatMessage } from "publint/utils" ;
1012import { parse as parseYaml } from "yaml" ;
1113import manifest from "../../.release-please-manifest.json" ;
1214import pkg from "../../package.json" ;
1315import tsdown from "../../tsdown.config.js" ;
1416import { ROOT } from "../root.js" ;
1517import { readSettingsSchema } from "../settings-schema.js" ;
18+ import { withTempDir } from "../temp-dir.js" ;
1619
1720/** The built lib/settings.schema.json, whose $id names the repository the package must point at. */
1821const schema = readSettingsSchema ( ) ;
1922
2023/** tsdown types its options loosely (a glob, a list, or a map); this config is the map form. */
2124const build = tsdown as { entry : Record < string , string > ; outDir : string } ;
2225
23- /** The export targets and the bin paths package.json names, each as a ./-relative path. */
24- function manifestTargets ( ) : string [ ] {
26+ /** The two fields the publish checks read, so a hand-made variant of `pkg` is a manifest too. */
27+ interface Manifest {
28+ readonly exports : Readonly < Record < string , string | { types : string ; default : string } > > ;
29+ readonly bin : Readonly < Record < string , string > > ;
30+ }
31+
32+ /** The export targets and the bin paths `manifest` names, each as a ./-relative path. */
33+ function manifestTargets ( manifest : Manifest ) : string [ ] {
2534 return [
26- ...Object . values ( pkg . exports ) . map ( ( entry ) =>
35+ ...Object . values ( manifest . exports ) . map ( ( entry ) =>
2736 typeof entry === "string" ? entry : entry . default ,
2837 ) ,
29- ...Object . values ( pkg . bin ) . map ( ( bin ) => `./${ bin } ` ) ,
38+ ...Object . values ( manifest . bin ) . map ( ( bin ) => `./${ bin } ` ) ,
3039 ] ;
3140}
3241
42+ function manifestDeclarations ( manifest : Manifest ) : string [ ] {
43+ return Object . values ( manifest . exports ) . flatMap ( ( entry ) =>
44+ typeof entry === "string" ? [ ] : [ entry . types ] ,
45+ ) ;
46+ }
47+
3348/** The import paths the library page's entry table names, plus the two its trailing sentence says ride along, sorted. */
3449function documentedExportPaths ( page : string ) : string [ ] {
3550 const entries = page . match ( / ^ # # T h e t w o e n t r i e s \n ( [ \s \S ] * ?) ^ # # / m) ?. [ 1 ] ?? "" ;
@@ -49,11 +64,31 @@ function documentedBinNames(page: string): string[] {
4964 return [ ...sentence . matchAll ( / ` ( [ ^ ` ] + ) ` / g) ] . map ( ( match ) => match [ 1 ] ?? "" ) . sort ( ) ;
5065}
5166
52- /** Whether `files` packs `target`: an entry names a file or a directory, with or without a trailing slash. */
53- function isPacked ( target : string ) : boolean {
54- return pkg . files . some ( ( entry ) => {
55- const path = `./${ entry . replace ( / \/ $ / , "" ) } ` ;
56- return target === path || target . startsWith ( `${ path } /` ) ;
67+ /**
68+ * What publint reports for `manifest` once npm, the publisher, has packed it; warnings count as errors, as lint:package
69+ * runs it. A scratch package holds the manifest and an empty stub at every file it points a consumer at (a bin stub
70+ * carries the shebang publint requires), so the judgment is the manifest's shape and npm's pack list, not the build.
71+ */
72+ async function manifestMessages ( manifest : Manifest ) : Promise < string [ ] > {
73+ return withTempDir ( "gsac-manifest-" , async ( dir ) => {
74+ const bins = Object . values ( manifest . bin ) . map ( ( bin ) => `./${ bin } ` ) ;
75+ for ( const file of [ ...manifestTargets ( manifest ) , ...manifestDeclarations ( manifest ) ] ) {
76+ if ( file === "./package.json" ) {
77+ continue ;
78+ }
79+ mkdirSync ( join ( dir , dirname ( file ) ) , { recursive : true } ) ;
80+ writeFileSync ( join ( dir , file ) , bins . includes ( file ) ? "#!/usr/bin/env node\n" : "" ) ;
81+ }
82+ writeFileSync ( join ( dir , "package.json" ) , JSON . stringify ( manifest ) ) ;
83+ const { messages } = await publint ( {
84+ pkgDir : dir ,
85+ pack : "npm" ,
86+ level : "warning" ,
87+ strict : true ,
88+ } ) ;
89+ return messages . map (
90+ ( message ) => formatMessage ( message , manifest , { color : false } ) ?? message . code ,
91+ ) ;
5792 } ) ;
5893}
5994
@@ -104,7 +139,7 @@ describe("package.json as the npm manifest", () => {
104139
105140 test ( "the exports map and the bins name exactly the library build, the schema, and the manifest" , ( ) => {
106141 const built = Object . keys ( build . entry ) . map ( ( name ) => `./${ build . outDir } /${ name } .js` ) ;
107- const targets = manifestTargets ( ) ;
142+ const targets = manifestTargets ( pkg ) ;
108143 expect ( targets . filter ( ( target ) => ! built . includes ( target ) ) . sort ( ) ) . toEqual ( [
109144 "./lib/settings.schema.json" ,
110145 "./package.json" ,
@@ -131,20 +166,26 @@ describe("package.json as the npm manifest", () => {
131166 ) ;
132167 }
133168 }
134- const declarations : string [ ] = [ ] ;
135169 for ( const entry of Object . values ( pkg . exports ) ) {
136170 if ( typeof entry === "string" ) {
137171 continue ;
138172 }
139173 expect ( entry . types ) . toBe ( entry . default . replace ( / \. j s $ / , ".d.ts" ) ) ;
140- declarations . push ( entry . types ) ;
141- }
142- // npm packs `files` (and the manifest, always), so a target outside them installs as a dangling path.
143- for ( const target of [ ...targets , ...declarations ] . filter ( ( t ) => t !== "./package.json" ) ) {
144- expect ( isPacked ( target ) , target ) . toBe ( true ) ;
145174 }
146175 } ) ;
147176
177+ test ( "npm's pack list holds every file the manifest points at, and publint has nothing else to say" , async ( ) => {
178+ // npm packs `files`, the manifest, README, LICENSE, and every bin, so a target outside them installs as a dangling path.
179+ expect ( await manifestMessages ( pkg ) ) . toEqual ( [ ] ) ;
180+ } ) ;
181+
182+ test ( "an export target outside `files` is reported as unpublished (negative control)" , async ( ) => {
183+ const manifest = { ...pkg , exports : { ...pkg . exports , "./extra" : "./dist/extra.js" } } ;
184+ expect ( await manifestMessages ( manifest ) ) . toEqual ( [
185+ 'pkg.exports["./extra"] is ./dist/extra.js but the file is not published. Is it specified in pkg.files?' ,
186+ ] ) ;
187+ } ) ;
188+
148189 test ( "the runtime dependencies are exactly the packages the library entries import" , ( ) => {
149190 // tsdown externalizes `dependencies`, so the consumer's package manager installs them beside lib/pkg/; the action bundle inlines everything,
150191 // so a package only src/action/ or src/main.ts uses (@actions/core, @actions/artifact) stays a devDependency.
0 commit comments