Skip to content

Commit 99c485d

Browse files
authored
Fail upon encountering unrecognized fields in the config (#1272)
1 parent e79c98b commit 99c485d

File tree

8 files changed

+377
-162
lines changed

8 files changed

+377
-162
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ Other improvements:
3838
current workspace.
3939
- #1110: `spago publish` will now install packages returned by the registry solver
4040
before trying to build with them.
41+
- Spago no longer ignores config fields that it doesn't recognize. This should
42+
help catch typos in field names.
4143

4244
## [0.21.0] - 2023-05-04
4345

core/src/Config.purs

Lines changed: 125 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,32 @@ import Data.Array.NonEmpty as NonEmptyArray
4343
import Data.Codec as Codec
4444
import Data.Codec.JSON as CJ
4545
import Data.Codec.JSON.Record as CJ.Record
46+
import Data.Codec.JSON.Strict as CJS
4647
import Data.Codec.JSON.Sum as CJ.Sum
4748
import Data.Either as Either
4849
import Data.List as List
4950
import Data.Map as Map
5051
import Data.Profunctor as Profunctor
5152
import Partial.Unsafe (unsafeCrashWith)
52-
import Registry.Internal.Codec as Internal.Codec
53+
import Registry.Internal.Codec as Reg.Internal.Codec
54+
import Registry.Internal.Parsing as Reg.Internal.Parsing
5355
import Registry.License as License
5456
import Registry.Location as Location
5557
import Registry.PackageName as PackageName
5658
import Registry.Range as Range
5759
import Registry.Sha256 as Sha256
5860
import Registry.Version as Version
59-
import Type.Proxy (Proxy(..))
6061

6162
type Config =
6263
{ package :: Maybe PackageConfig
6364
, workspace :: Maybe WorkspaceConfig
6465
}
6566

6667
configCodec :: CJ.Codec Config
67-
configCodec = CJ.object
68-
$ CJ.recordPropOptional (Proxy @"package") packageConfigCodec
69-
$ CJ.recordPropOptional (Proxy @"workspace") workspaceConfigCodec
70-
$ CJ.record
68+
configCodec = CJS.objectStrict
69+
$ CJS.recordPropOptional @"package" packageConfigCodec
70+
$ CJS.recordPropOptional @"workspace" workspaceConfigCodec
71+
$ CJS.record
7172

7273
type PackageConfig =
7374
{ name :: PackageName
@@ -81,16 +82,16 @@ type PackageConfig =
8182
}
8283

8384
packageConfigCodec :: CJ.Codec PackageConfig
84-
packageConfigCodec = CJ.named "PackageConfig" $ CJ.object
85-
$ CJ.recordProp (Proxy @"name") PackageName.codec
86-
$ CJ.recordPropOptional (Proxy @"description") CJ.string
87-
$ CJ.recordProp (Proxy @"dependencies") dependenciesCodec
88-
$ CJ.recordPropOptional (Proxy @"build") packageBuildOptionsCodec
89-
$ CJ.recordPropOptional (Proxy @"bundle") bundleConfigCodec
90-
$ CJ.recordPropOptional (Proxy @"run") runConfigCodec
91-
$ CJ.recordPropOptional (Proxy @"test") testConfigCodec
92-
$ CJ.recordPropOptional (Proxy @"publish") publishConfigCodec
93-
$ CJ.record
85+
packageConfigCodec = CJ.named "PackageConfig" $ CJS.objectStrict
86+
$ CJS.recordProp @"name" PackageName.codec
87+
$ CJS.recordPropOptional @"description" CJ.string
88+
$ CJS.recordProp @"dependencies" dependenciesCodec
89+
$ CJS.recordPropOptional @"build" packageBuildOptionsCodec
90+
$ CJS.recordPropOptional @"bundle" bundleConfigCodec
91+
$ CJS.recordPropOptional @"run" runConfigCodec
92+
$ CJS.recordPropOptional @"test" testConfigCodec
93+
$ CJS.recordPropOptional @"publish" publishConfigCodec
94+
$ CJS.record
9495

9596
type PublishConfig =
9697
{ version :: Version
@@ -101,24 +102,62 @@ type PublishConfig =
101102
}
102103

103104
publishConfigCodec :: CJ.Codec PublishConfig
104-
publishConfigCodec = CJ.named "PublishConfig" $ CJ.object
105-
$ CJ.recordProp (Proxy @"version") Version.codec
106-
$ CJ.recordProp (Proxy @"license") License.codec
107-
$ CJ.recordPropOptional (Proxy @"location") Location.codec
108-
$ CJ.recordPropOptional (Proxy @"include") (CJ.array CJ.string)
109-
$ CJ.recordPropOptional (Proxy @"exclude") (CJ.array CJ.string)
110-
$ CJ.record
105+
publishConfigCodec = CJ.named "PublishConfig" $ CJS.objectStrict
106+
$ CJS.recordProp @"version" Version.codec
107+
$ CJS.recordProp @"license" License.codec
108+
$ CJS.recordPropOptional @"location" publishLocationCodec
109+
$ CJS.recordPropOptional @"include" (CJ.array CJ.string)
110+
$ CJS.recordPropOptional @"exclude" (CJ.array CJ.string)
111+
$ CJS.record
112+
113+
-- This codec duplicates `Location.codec` from the Registry library, but with
114+
-- strict parsing of fields, so that we would error out on unknown fields, thus
115+
-- catching typos in field names. We do not want to modify the original codec in
116+
-- the Registry library, because it's used for network communication, not for
117+
-- reading user input, and therefore it's more important there to ignore unknown
118+
-- fields for backwards compatibiility.
119+
publishLocationCodec :: CJ.Codec Location
120+
publishLocationCodec = CJ.named "Publish Location" $ Codec.codec' decode encode
121+
where
122+
decode json =
123+
(Location.Git <$> Codec.decode gitCodec json)
124+
<|> (Location.GitHub <$> Codec.decode githubCodec json)
125+
126+
encode = case _ of
127+
Location.Git git -> CJ.encode gitCodec git
128+
Location.GitHub github -> CJ.encode githubCodec github
129+
130+
githubCodec :: CJ.Codec Location.GitHubData
131+
githubCodec = Profunctor.dimap toJsonRep fromJsonRep $ CJ.named "GitHub" $ CJ.Record.objectStrict
132+
{ githubOwner: CJ.string
133+
, githubRepo: CJ.string
134+
, subdir: CJ.Record.optional CJ.string
135+
}
136+
where
137+
toJsonRep { owner, repo, subdir } = { githubOwner: owner, githubRepo: repo, subdir }
138+
fromJsonRep { githubOwner, githubRepo, subdir } = { owner: githubOwner, repo: githubRepo, subdir }
139+
140+
gitCodec :: CJ.Codec Location.GitData
141+
gitCodec = Profunctor.dimap toJsonRep fromJsonRep $ CJ.named "Git" $ CJ.Record.objectStrict
142+
{ gitUrl: Reg.Internal.Codec.parsedString Reg.Internal.Parsing.gitUrl
143+
, subdir: CJ.Record.optional CJ.string
144+
}
145+
where
146+
-- The JSON representation of the GitHub type uses 'gitUrl', but in PureScript
147+
-- we use 'url' for convenience.
148+
toJsonRep { url, subdir } = { gitUrl: url, subdir }
149+
fromJsonRep { gitUrl, subdir } = { url: gitUrl, subdir }
111150

112151
type RunConfig =
113152
{ main :: Maybe String
114153
, execArgs :: Maybe (Array String)
115154
}
116155

117156
runConfigCodec :: CJ.Codec RunConfig
118-
runConfigCodec = CJ.named "RunConfig" $ CJ.object
119-
$ CJ.recordPropOptional (Proxy @"main") CJ.string
120-
$ CJ.recordPropOptional (Proxy @"execArgs") (CJ.array CJ.string)
121-
$ CJ.record
157+
runConfigCodec = CJ.named "RunConfig" $ CJS.objectStrict
158+
$ CJS.recordPropOptional @"main" CJ.string
159+
$ CJS.recordPropOptional @"execArgs" (CJ.array CJ.string)
160+
$ CJS.record
122161

123162
type TestConfig =
124163
{ main :: String
@@ -130,25 +169,25 @@ type TestConfig =
130169
}
131170

132171
testConfigCodec :: CJ.Codec TestConfig
133-
testConfigCodec = CJ.named "TestConfig" $ CJ.object
134-
$ CJ.recordProp (Proxy @"main") CJ.string
135-
$ CJ.recordPropOptional (Proxy @"execArgs") (CJ.array CJ.string)
136-
$ CJ.recordPropOptional (Proxy @"censorTestWarnings") censorBuildWarningsCodec
137-
$ CJ.recordPropOptional (Proxy @"strict") CJ.boolean
138-
$ CJ.recordPropOptional (Proxy @"pedanticPackages") CJ.boolean
139-
$ CJ.recordProp (Proxy @"dependencies") dependenciesCodec
140-
$ CJ.record
172+
testConfigCodec = CJ.named "TestConfig" $ CJS.objectStrict
173+
$ CJS.recordProp @"main" CJ.string
174+
$ CJS.recordPropOptional @"execArgs" (CJ.array CJ.string)
175+
$ CJS.recordPropOptional @"censorTestWarnings" censorBuildWarningsCodec
176+
$ CJS.recordPropOptional @"strict" CJ.boolean
177+
$ CJS.recordPropOptional @"pedanticPackages" CJ.boolean
178+
$ CJS.recordProp @"dependencies" dependenciesCodec
179+
$ CJS.record
141180

142181
type BackendConfig =
143182
{ cmd :: String
144183
, args :: Maybe (Array String)
145184
}
146185

147186
backendConfigCodec :: CJ.Codec BackendConfig
148-
backendConfigCodec = CJ.named "BackendConfig" $ CJ.object
149-
$ CJ.recordProp (Proxy @"cmd") CJ.string
150-
$ CJ.recordPropOptional (Proxy @"args") (CJ.array CJ.string)
151-
$ CJ.record
187+
backendConfigCodec = CJ.named "BackendConfig" $ CJS.objectStrict
188+
$ CJS.recordProp @"cmd" CJ.string
189+
$ CJS.recordPropOptional @"args" (CJ.array CJ.string)
190+
$ CJS.record
152191

153192
type PackageBuildOptionsInput =
154193
{ censorProjectWarnings :: Maybe CensorBuildWarnings
@@ -157,11 +196,11 @@ type PackageBuildOptionsInput =
157196
}
158197

159198
packageBuildOptionsCodec :: CJ.Codec PackageBuildOptionsInput
160-
packageBuildOptionsCodec = CJ.named "PackageBuildOptionsInput" $ CJ.object
161-
$ CJ.recordPropOptional (Proxy @"censorProjectWarnings") censorBuildWarningsCodec
162-
$ CJ.recordPropOptional (Proxy @"strict") CJ.boolean
163-
$ CJ.recordPropOptional (Proxy @"pedanticPackages") CJ.boolean
164-
$ CJ.record
199+
packageBuildOptionsCodec = CJ.named "PackageBuildOptionsInput" $ CJS.objectStrict
200+
$ CJS.recordPropOptional @"censorProjectWarnings" censorBuildWarningsCodec
201+
$ CJS.recordPropOptional @"strict" CJ.boolean
202+
$ CJS.recordPropOptional @"pedanticPackages" CJ.boolean
203+
$ CJS.record
165204

166205
type BundleConfig =
167206
{ minify :: Maybe Boolean
@@ -173,17 +212,19 @@ type BundleConfig =
173212
}
174213

175214
bundleConfigCodec :: CJ.Codec BundleConfig
176-
bundleConfigCodec = CJ.named "BundleConfig" $ CJ.object
177-
$ CJ.recordPropOptional (Proxy @"minify") CJ.boolean
178-
$ CJ.recordPropOptional (Proxy @"module") CJ.string
179-
$ CJ.recordPropOptional (Proxy @"outfile") CJ.string
180-
$ CJ.recordPropOptional (Proxy @"platform") bundlePlatformCodec
181-
$ CJ.recordPropOptional (Proxy @"type") bundleTypeCodec
182-
$ CJ.recordPropOptional (Proxy @"extraArgs") (CJ.array CJ.string)
183-
$ CJ.record
215+
bundleConfigCodec = CJ.named "BundleConfig" $ CJS.objectStrict
216+
$ CJS.recordPropOptional @"minify" CJ.boolean
217+
$ CJS.recordPropOptional @"module" CJ.string
218+
$ CJS.recordPropOptional @"outfile" CJ.string
219+
$ CJS.recordPropOptional @"platform" bundlePlatformCodec
220+
$ CJS.recordPropOptional @"type" bundleTypeCodec
221+
$ CJS.recordPropOptional @"extraArgs" (CJ.array CJ.string)
222+
$ CJS.record
184223

185224
data BundlePlatform = BundleNode | BundleBrowser
186225

226+
derive instance Eq BundlePlatform
227+
187228
instance Show BundlePlatform where
188229
show = case _ of
189230
BundleNode -> "node"
@@ -202,6 +243,8 @@ bundlePlatformCodec = CJ.Sum.enumSum show (parsePlatform)
202243
-- App bundles with a main fn, while Module does not include a main.
203244
data BundleType = BundleApp | BundleModule
204245

246+
derive instance Eq BundleType
247+
205248
instance Show BundleType where
206249
show = case _ of
207250
BundleApp -> "app"
@@ -238,7 +281,7 @@ instance Monoid Dependencies where
238281
dependenciesCodec :: CJ.Codec Dependencies
239282
dependenciesCodec = Profunctor.dimap to from $ CJ.array dependencyCodec
240283
where
241-
packageSingletonCodec = Internal.Codec.packageMap spagoRangeCodec
284+
packageSingletonCodec = Reg.Internal.Codec.packageMap spagoRangeCodec
242285

243286
to :: Dependencies -> Array (Either PackageName (Map PackageName Range))
244287
to (Dependencies deps) =
@@ -291,12 +334,12 @@ type WorkspaceConfig =
291334
}
292335

293336
workspaceConfigCodec :: CJ.Codec WorkspaceConfig
294-
workspaceConfigCodec = CJ.named "WorkspaceConfig" $ CJ.object
295-
$ CJ.recordPropOptional (Proxy @"packageSet") setAddressCodec
296-
$ CJ.recordPropOptional (Proxy @"backend") backendConfigCodec
297-
$ CJ.recordPropOptional (Proxy @"buildOpts") buildOptionsCodec
298-
$ CJ.recordPropOptional (Proxy @"extraPackages") (Internal.Codec.packageMap extraPackageCodec)
299-
$ CJ.record
337+
workspaceConfigCodec = CJ.named "WorkspaceConfig" $ CJS.objectStrict
338+
$ CJS.recordPropOptional @"packageSet" setAddressCodec
339+
$ CJS.recordPropOptional @"backend" backendConfigCodec
340+
$ CJS.recordPropOptional @"buildOpts" buildOptionsCodec
341+
$ CJS.recordPropOptional @"extraPackages" (Reg.Internal.Codec.packageMap extraPackageCodec)
342+
$ CJS.record
300343

301344
type WorkspaceBuildOptionsInput =
302345
{ output :: Maybe FilePath
@@ -305,11 +348,11 @@ type WorkspaceBuildOptionsInput =
305348
}
306349

307350
buildOptionsCodec :: CJ.Codec WorkspaceBuildOptionsInput
308-
buildOptionsCodec = CJ.named "WorkspaceBuildOptionsInput" $ CJ.object
309-
$ CJ.recordPropOptional (Proxy @"output") CJ.string
310-
$ CJ.recordPropOptional (Proxy @"censorLibraryWarnings") censorBuildWarningsCodec
311-
$ CJ.recordPropOptional (Proxy @"statVerbosity") statVerbosityCodec
312-
$ CJ.record
351+
buildOptionsCodec = CJ.named "WorkspaceBuildOptionsInput" $ CJS.objectStrict
352+
$ CJS.recordPropOptional @"output" CJ.string
353+
$ CJS.recordPropOptional @"censorLibraryWarnings" censorBuildWarningsCodec
354+
$ CJS.recordPropOptional @"statVerbosity" statVerbosityCodec
355+
$ CJS.record
313356

314357
data CensorBuildWarnings
315358
= CensorAllWarnings
@@ -361,13 +404,15 @@ warningCensorTestCodec = Codec.codec' decode encode
361404
byCode = ByCode <$> Codec.decode CJ.string json
362405
byPrefix = (ByMessagePrefix <<< _.byPrefix) <$> Codec.decode byMessagePrefixCodec json
363406

364-
byMessagePrefixCodec = CJ.named "ByMessagePrefix" $ CJ.Record.object { byPrefix: CJ.string }
407+
byMessagePrefixCodec = CJ.named "ByMessagePrefix" $ CJ.Record.objectStrict { byPrefix: CJ.string }
365408

366409
data StatVerbosity
367410
= NoStats
368411
| CompactStats
369412
| VerboseStats
370413

414+
derive instance Eq StatVerbosity
415+
371416
instance Show StatVerbosity where
372417
show = case _ of
373418
NoStats -> "NoStats"
@@ -397,9 +442,9 @@ derive instance Eq SetAddress
397442
setAddressCodec :: CJ.Codec SetAddress
398443
setAddressCodec = Codec.codec' decode encode
399444
where
400-
setFromRegistryCodec = CJ.named "SetFromRegistry" $ CJ.Record.object { registry: Version.codec }
401-
setFromUrlCodec = CJ.named "SetFromUrl" $ CJ.Record.object { url: CJ.string, hash: CJ.Record.optional Sha256.codec }
402-
setFromPathCodec = CJ.named "SetFromPath" $ CJ.Record.object { path: CJ.string }
445+
setFromRegistryCodec = CJ.named "SetFromRegistry" $ CJ.Record.objectStrict { registry: Version.codec }
446+
setFromUrlCodec = CJ.named "SetFromUrl" $ CJ.Record.objectStrict { url: CJ.string, hash: CJ.Record.optional Sha256.codec }
447+
setFromPathCodec = CJ.named "SetFromPath" $ CJ.Record.objectStrict { path: CJ.string }
403448

404449
encode (SetFromRegistry r) = CJ.encode setFromRegistryCodec r
405450
encode (SetFromUrl u) = CJ.encode setFromUrlCodec u
@@ -427,7 +472,7 @@ extraPackageCodec = Codec.codec' decode encode
427472
type LocalPackage = { path :: FilePath }
428473

429474
localPackageCodec :: CJ.Codec LocalPackage
430-
localPackageCodec = CJ.named "LocalPackage" $ CJ.Record.object { path: CJ.string }
475+
localPackageCodec = CJ.named "LocalPackage" $ CJ.Record.objectStrict { path: CJ.string }
431476

432477
data RemotePackage
433478
= RemoteGitPackage GitPackage
@@ -455,12 +500,12 @@ type GitPackage =
455500
}
456501

457502
gitPackageCodec :: CJ.Codec GitPackage
458-
gitPackageCodec = CJ.named "GitPackage" $ CJ.object
459-
$ CJ.recordProp (Proxy @"git") CJ.string
460-
$ CJ.recordProp (Proxy @"ref") CJ.string
461-
$ CJ.recordPropOptional (Proxy @"subdir") CJ.string
462-
$ CJ.recordPropOptional (Proxy @"dependencies") dependenciesCodec
463-
$ CJ.record
503+
gitPackageCodec = CJ.named "GitPackage" $ CJS.objectStrict
504+
$ CJS.recordProp @"git" CJ.string
505+
$ CJS.recordProp @"ref" CJ.string
506+
$ CJS.recordPropOptional @"subdir" CJ.string
507+
$ CJS.recordPropOptional @"dependencies" dependenciesCodec
508+
$ CJS.record
464509

465510
-- | The format of a legacy packages.json package set entry for an individual
466511
-- | package.
@@ -471,8 +516,8 @@ type LegacyPackageSetEntry =
471516
}
472517

473518
legacyPackageSetEntryCodec :: CJ.Codec LegacyPackageSetEntry
474-
legacyPackageSetEntryCodec = CJ.named "LegacyPackageSetEntry" $ CJ.object
475-
$ CJ.recordProp (Proxy @"repo") CJ.string
476-
$ CJ.recordProp (Proxy @"version") CJ.string
477-
$ CJ.recordProp (Proxy @"dependencies") (CJ.array PackageName.codec)
478-
$ CJ.record
519+
legacyPackageSetEntryCodec = CJ.named "LegacyPackageSetEntry" $ CJS.objectStrict
520+
$ CJS.recordProp @"repo" CJ.string
521+
$ CJS.recordProp @"version" CJ.string
522+
$ CJS.recordProp @"dependencies" (CJ.array PackageName.codec)
523+
$ CJS.record

docs-search/client-halogen/spago.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ package:
3838
bundle:
3939
type: "app"
4040
minify: true
41-
sourceMaps: true
4241
module: Docs.Search.App
4342
outfile: "../../bin/docs-search-app.js"
4443
platform: browser
@@ -47,3 +46,4 @@ package:
4746
# The node module is also considered deprecated and recommends using the upstream npm package punycode. So its an easy swap-in.
4847
# The extra / at the end is how you tell node and esbuild to override a builtin node package with a user-space package.
4948
- "--alias:punycode=punycode/"
49+
- "--sourcemap"

0 commit comments

Comments
 (0)