@@ -6,49 +6,72 @@ import {
66 windowsSigningArgs ,
77} from '../scripts/package-win'
88
9- interface SchemaNode {
10- readonly $ref ?: string
11- readonly anyOf ?: readonly SchemaNode [ ]
12- readonly properties ?: Readonly < Record < string , SchemaNode > >
13- }
9+ type JsonObject = Record < string , unknown >
1410
1511const requireFromTests = createRequire ( import . meta. url )
16- const schema = requireFromTests (
17- requireFromTests . resolve ( 'app-builder-lib/scheme.json' , {
18- paths : [ requireFromTests . resolve ( 'electron-builder' ) ] ,
19- } ) ,
20- ) as { readonly definitions : Readonly < Record < string , SchemaNode > > }
21-
22- function referencedDefinition ( node : SchemaNode ) : SchemaNode | undefined {
23- const reference = node . $ref ?? node . anyOf ?. find ( branch => branch . $ref !== undefined ) ?. $ref
24- return reference === undefined ? undefined : schema . definitions [ reference . replace ( '#/definitions/' , '' ) ]
12+ const electronBuilderPath = requireFromTests . resolve ( 'electron-builder' )
13+ const schema : unknown = requireFromTests (
14+ requireFromTests . resolve ( 'app-builder-lib/scheme.json' , { paths : [ electronBuilderPath ] } ) ,
15+ )
16+ const Ajv = requireFromTests ( requireFromTests . resolve ( 'ajv' , { paths : [ electronBuilderPath ] } ) ) as {
17+ readonly default : new ( options : JsonObject ) => {
18+ compile : ( schema : unknown ) => ( ( data : unknown ) => boolean ) & { errors ?: readonly { instancePath : string , keyword : string , message ?: string , params : JsonObject } [ ] }
19+ }
2520}
21+ // The same Ajv settings app-builder-lib validates a release configuration with,
22+ // so a configuration this accepts is one Electron Builder accepts.
23+ const validateConfiguration = new Ajv . default ( {
24+ allErrors : true ,
25+ verbose : true ,
26+ coerceTypes : true ,
27+ strict : false ,
28+ } ) . compile ( schema )
29+
30+ const baseConfiguration = ( requireFromTests ( '../package.json' ) as { readonly build : JsonObject } ) . build
2631
2732/**
28- * Assert that a `--config.<path>` option exists in the installed Electron Builder schema.
29- *
30- * `WindowsConfiguration` sets `additionalProperties: false`, so an option that
31- * the schema does not declare fails validation before any build work and takes
32- * the whole `win` object down with it. Comparing against the real schema keeps
33- * these arguments honest across Electron Builder upgrades, which is what an
34- * expected-array assertion cannot do.
35- * @param path - Dotted option path with the `--config.` prefix removed.
33+ * Apply generated `--config.<path> <value>` arguments to the packaged build configuration.
34+ * @param args - Arguments as they reach Electron Builder.
35+ * @returns The configuration Electron Builder would validate.
3636 */
37- function assertSchemaOption ( path : string ) : void {
38- const [ root , ...rest ] = path . split ( '.' )
39- expect ( root ) . toBe ( 'win' )
40- let definition = schema . definitions [ 'WindowsConfiguration' ] !
41- rest . forEach ( ( segment , index ) => {
42- const property = definition . properties ?. [ segment ]
43- if ( property === undefined ) {
44- throw new Error ( `Electron Builder has no option '${ rest . slice ( 0 , index + 1 ) . join ( '.' ) } ' under win` )
37+ function configurationFrom ( args : readonly string [ ] ) : JsonObject {
38+ const configuration = structuredClone ( baseConfiguration )
39+ for ( let index = 0 ; index < args . length ; index += 1 ) {
40+ const argument = args [ index ] !
41+ if ( ! argument . startsWith ( '--config.' ) ) continue
42+ const path = argument . slice ( '--config.' . length ) . split ( '.' )
43+ let node = configuration
44+ for ( const key of path . slice ( 0 , - 1 ) ) {
45+ node [ key ] ??= { }
46+ node = node [ key ] as JsonObject
4547 }
46- const next = referencedDefinition ( property )
47- if ( next !== undefined ) definition = next
48- else if ( index !== rest . length - 1 ) {
49- throw new Error ( `Electron Builder option 'win.${ rest . slice ( 0 , index + 1 ) . join ( '.' ) } ' has no nested options` )
50- }
51- } )
48+ node [ path . at ( - 1 ) ! ] = args [ index + 1 ]
49+ index += 1
50+ }
51+ return configuration
52+ }
53+
54+ /**
55+ * Assert Electron Builder would accept the configuration these arguments produce.
56+ *
57+ * Comparing generated arguments against a hand-written array cannot tell that
58+ * the arguments are invalid — both copies carry the same mistake. Electron
59+ * Builder validates the merged configuration before it packages anything, so
60+ * running that same validation here fails in the suite instead of at the tag.
61+ * @param args - Arguments as they reach Electron Builder.
62+ */
63+ function assertConfigurationAccepted ( args : readonly string [ ] ) : void {
64+ if ( validateConfiguration ( configurationFrom ( args ) ) ) return
65+ const errors = validateConfiguration . errors ?? [ ]
66+ const unknown = errors
67+ . filter ( error => error . keyword === 'additionalProperties' )
68+ . map ( error => `${ error . instancePath } .${ String ( error . params [ 'additionalProperty' ] ) } ` )
69+ if ( unknown . length > 0 ) throw new Error ( `Electron Builder rejects unknown options: ${ unknown . join ( ', ' ) } ` )
70+ // anyOf/type noise follows every real error; the specific keywords name the cause.
71+ const specific = errors . filter ( error => error . keyword !== 'anyOf' && error . keyword !== 'type' )
72+ const reported = ( specific . length > 0 ? specific : errors )
73+ . map ( error => `${ error . instancePath === '' ? 'configuration' : error . instancePath } ${ error . message ?? 'is invalid' } ` )
74+ throw new Error ( `Electron Builder rejects the configuration: ${ [ ...new Set ( reported ) ] . join ( '; ' ) } ` )
5275}
5376
5477const signingEnvironment : NodeJS . ProcessEnv = {
@@ -101,24 +124,28 @@ describe('Windows Azure signing configuration', () => {
101124 } )
102125} )
103126
104- describe ( 'Electron Builder option names ' , ( ) => {
127+ describe ( 'Electron Builder configuration ' , ( ) => {
105128 const certificateEnvironment : NodeJS . ProcessEnv = {
106129 WIN_CSC_LINK : 'certificate.p12' ,
107130 WIN_CSC_KEY_PASSWORD : 'password' ,
108131 WINDOWS_SIGNING_PUBLISHER_NAME : 'CN=Example Publisher, O=Example Publisher' ,
109132 }
110133
111- it ( 'emits only options the installed Electron Builder schema declares' , ( ) => {
134+ it ( 'accepts the packaged configuration on its own' , ( ) => {
135+ expect ( ( ) => { assertConfigurationAccepted ( [ ] ) } ) . not . toThrow ( )
136+ } )
137+
138+ it ( 'accepts the configuration every signing method produces' , ( ) => {
112139 for ( const environment of [ signingEnvironment , certificateEnvironment ] ) {
113- const options = windowsSigningArgs ( environment ) . filter ( argument => argument . startsWith ( '--config.' ) )
114- expect ( options . length ) . toBeGreaterThan ( 0 )
115- for ( const option of options ) assertSchemaOption ( option . slice ( '--config.' . length ) )
140+ const args = windowsSigningArgs ( environment )
141+ expect ( args . length ) . toBeGreaterThan ( 0 )
142+ expect ( ( ) => { assertConfigurationAccepted ( args ) } ) . not . toThrow ( )
116143 }
117144 } )
118145
119- it ( 'rejects an option the schema does not declare ' , ( ) => {
120- expect ( ( ) => { assertSchemaOption ( ' win.publisherName') } )
121- . toThrow ( " Electron Builder has no option ' publisherName' under win" )
146+ it ( 'rejects an option Electron Builder has removed ' , ( ) => {
147+ expect ( ( ) => { assertConfigurationAccepted ( [ '--config. win.publisherName', 'CN=Example Publisher' ] ) } )
148+ . toThrow ( ' Electron Builder rejects unknown options: /win. publisherName')
122149 } )
123150} )
124151
0 commit comments