Skip to content

Commit e06d664

Browse files
authored
Refuse to overwrite existing target bundle file unless it was generated by Spago in the first place (#1260)
1 parent 413d3cf commit e06d664

17 files changed

+216
-45
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Other improvements:
1919
- Added typo suggestions upon failing to find a package by name.
2020
- `spago publish` now checks that the publish location matches one of the remotes in the current Git repository.
2121
- Emoji ✅ ❌ ‼️ replaced with ✓ ✘ ‼ respectively, and are not printed at all with `--no-color`.
22+
- `spago bundle` now writes a special marker into the bundle and will refuse to
23+
overwrite the file if the marker isn't present, assuming that the file was
24+
manually created or edited, not generated by Spago itself.
2225

2326
## [0.21.0] - 2023-05-04
2427

bin/src/Flags.purs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,16 @@ outfile =
6969
OT.optional
7070
$ O.strOption
7171
( O.long "outfile"
72-
<> O.help "Destination path for the bundle"
72+
<> O.help "Destination path for the bundle. Defaults to ./index.js"
7373
)
7474

75+
forceBundle :: Parser Boolean
76+
forceBundle =
77+
O.switch
78+
( O.long "force"
79+
<> O.help "Overwrite the target bundle file even if it was manually created or generated by a tool other than Spago."
80+
)
81+
7582
-- TODO make an ADT for node and browser
7683
platform :: Parser (Maybe String)
7784
platform =

bin/src/Main.purs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ type BundleArgs =
172172
, backendArgs :: List String
173173
, bundlerArgs :: List String
174174
, output :: Maybe String
175+
, force :: Boolean
175176
, pedanticPackages :: Boolean
176177
, type :: Maybe String
177178
, ensureRanges :: Boolean
@@ -406,6 +407,7 @@ bundleArgsParser =
406407
, backendArgs: Flags.backendArgs
407408
, bundlerArgs: Flags.bundlerArgs
408409
, output: Flags.output
410+
, force: Flags.forceBundle
409411
, pedanticPackages: Flags.pedanticPackages
410412
, ensureRanges: Flags.ensureRanges
411413
, strict: Flags.strict
@@ -757,8 +759,17 @@ mkBundleEnv bundleArgs = do
757759
let cliArgs = Array.fromFoldable bundleArgs.bundlerArgs
758760
(Alternative.guard (Array.length cliArgs > 0) *> pure cliArgs) <|> (bundleConf _.extraArgs)
759761

760-
let bundleOptions = { minify, module: entrypoint, outfile, platform, type: bundleType, sourceMaps: bundleArgs.sourceMaps, extraArgs }
761762
let
763+
bundleOptions =
764+
{ minify
765+
, module: entrypoint
766+
, outfile
767+
, force: bundleArgs.force
768+
, platform
769+
, type: bundleType
770+
, sourceMaps: bundleArgs.sourceMaps
771+
, extraArgs
772+
}
762773
newWorkspace = workspace
763774
{ buildOptions
764775
{ output = bundleArgs.output <|> workspace.buildOptions.output

src/Spago/Command/Bundle.purs

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ module Spago.Command.Bundle where
22

33
import Spago.Prelude
44

5+
import Data.Array (all, fold, take)
6+
import Data.String as Str
7+
import Data.String.Utils (startsWith)
58
import Node.Path as Path
69
import Spago.Cmd as Cmd
710
import Spago.Config (BundlePlatform(..), BundleType(..), Workspace, WorkspacePackage)
811
import Spago.Esbuild (Esbuild)
12+
import Spago.FS as FS
13+
import Spago.Generated.BuildInfo as BuildInfo
914

1015
type BundleEnv a =
1116
{ esbuild :: Esbuild
@@ -21,6 +26,7 @@ type BundleOptions =
2126
, sourceMaps :: Boolean
2227
, module :: String
2328
, outfile :: FilePath
29+
, force :: Boolean
2430
, platform :: BundlePlatform
2531
, type :: BundleType
2632
, extraArgs :: Array String
@@ -35,7 +41,7 @@ type RawBundleOptions =
3541
, extraArgs :: Array String
3642
}
3743

38-
run :: forall a. Spago (BundleEnv a) Unit
44+
run :: a. Spago (BundleEnv a) Unit
3945
run = do
4046
{ esbuild, selected, workspace, bundleOptions: opts } <- ask
4147
logDebug $ "Bundle options: " <> show opts
@@ -47,38 +53,104 @@ run = do
4753
BundleBrowser, BundleApp -> "--format=iife"
4854
_, _ -> "--format=esm"
4955

50-
-- See https://github.com/evanw/esbuild/issues/1921
51-
nodePatch = case opts.platform of
52-
BundleNode -> [ "--banner:js=import __module from \'module\';import __path from \'path\';import __url from \'url\';const require = __module.createRequire(import.meta.url);const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url));const __filename=new URL(import.meta.url).pathname" ]
53-
_ -> []
56+
onlyForNode s = case opts.platform of
57+
BundleNode -> s
58+
BundleBrowser -> ""
5459

55-
output = case workspace.buildOptions.output of
56-
Nothing -> "output"
57-
Just o -> o
60+
output = workspace.buildOptions.output # fromMaybe "output"
5861
-- TODO: we might need to use `Path.relative selected.path output` instead of just output there
5962
mainPath = withForwardSlashes $ Path.concat [ output, opts.module, "index.js" ]
6063

61-
shebang = case opts.platform of
62-
BundleNode -> "#!/usr/bin/env node\n\n"
63-
_ -> ""
64-
6564
{ input, entrypoint } = case opts.type of
66-
BundleApp -> { entrypoint: [], input: Cmd.StdinWrite (shebang <> "import { main } from './" <> mainPath <> "'; main();") }
67-
BundleModule -> { entrypoint: [ mainPath ], input: Cmd.StdinNewPipe }
65+
BundleApp ->
66+
{ entrypoint: []
67+
, input: Cmd.StdinWrite $ fold [ onlyForNode "#!/usr/bin/env node\n\n", "import { main } from './", mainPath, "';main();" ]
68+
}
69+
BundleModule ->
70+
{ entrypoint: [ mainPath ]
71+
, input: Cmd.StdinNewPipe
72+
}
73+
6874
execOptions = Cmd.defaultExecOptions { pipeStdin = input }
6975

70-
args =
71-
[ "--bundle"
72-
, "--outfile=" <> outfile
73-
, "--platform=" <> show opts.platform
74-
-- See https://github.com/evanw/esbuild/issues/1051
75-
, "--loader:.node=file"
76-
, format
77-
] <> opts.extraArgs <> minify <> sourceMap <> entrypoint <> nodePatch
76+
banner = fold
77+
[ bundleWatermarkPrefix
78+
, BuildInfo.packages."spago-bin"
79+
, " */"
80+
, onlyForNode nodeTargetPolyfill
81+
]
82+
83+
args = fold
84+
[ [ "--bundle"
85+
, "--outfile=" <> outfile
86+
, "--platform=" <> show opts.platform
87+
, "--banner:js=" <> banner
88+
, "--loader:.node=file" -- See https://github.com/evanw/esbuild/issues/1051
89+
, format
90+
]
91+
, opts.extraArgs
92+
, minify
93+
, sourceMap
94+
, entrypoint
95+
]
96+
97+
-- FIXME: remove this after 2024-12-01
98+
whenM (FS.exists checkWatermarkMarkerFileName)
99+
$ unless opts.force
100+
$ whenM (isNotSpagoGeneratedFile outfile)
101+
$ die [ "Target file " <> opts.outfile <> " was not previously generated by Spago. Use --force to overwrite anyway." ]
102+
78103
logInfo "Bundling..."
79104
logDebug $ "Running esbuild: " <> show args
80105
Cmd.exec esbuild.cmd args execOptions >>= case _ of
81106
Right _ -> logSuccess "Bundle succeeded."
82107
Left r -> do
83108
logDebug $ Cmd.printExecResult r
84109
die [ "Failed to bundle." ]
110+
111+
isNotSpagoGeneratedFile :: a. String -> Spago (BundleEnv a) Boolean
112+
isNotSpagoGeneratedFile path = do
113+
exists <- FS.exists path
114+
if not exists then
115+
pure false
116+
else
117+
-- The first line of the file could be the marker, or it could the shebang
118+
-- if the bundle was compiled for Node, in which case the marker will be the
119+
-- second line. So we check the first two lines.
120+
FS.readTextFile path
121+
<#> Str.split (Str.Pattern "\n")
122+
>>> take 2
123+
>>> all (not startsWith bundleWatermarkPrefix)
124+
125+
bundleWatermarkPrefix :: String
126+
bundleWatermarkPrefix = "/* Generated by Spago v"
127+
128+
-- Presence of this file gates the watermark check.
129+
--
130+
-- If this file exists in the current directory, the Bundle command will check
131+
-- if the target bundle file already exists and has the watermark in it, and if
132+
-- it doesn't have the watermark, will refuse to overwrite it for fear of
133+
-- overwriting a user-generated file.
134+
--
135+
-- We gate this check on the presence of this file so that the check is only
136+
-- performed in a controlled context (such as integration tests), but doesn't
137+
-- work for normal users. The idea is that the users who upgrade their Spago
138+
-- aren't immediately met with a refusal to overwrite the bundle. Instead, Spago
139+
-- will overwrite the bundle just fine, but now the bundle will have the
140+
-- watermark in it. Then, after some time, after enough users have upgraded and
141+
-- acquired the watermark in their bundles, we will remove this gating
142+
-- mechanism, and watermark checking will start working for normal users.
143+
checkWatermarkMarkerFileName :: String
144+
checkWatermarkMarkerFileName = ".check-bundle-watermark"
145+
146+
-- A polyfill inserted when building for Node to work around this esbuild issue:
147+
-- https://github.com/evanw/esbuild/issues/1921
148+
nodeTargetPolyfill :: String
149+
nodeTargetPolyfill = Str.joinWith ";"
150+
[ "import __module from 'module'"
151+
, "import __path from 'path'"
152+
, "import __url from 'url'"
153+
, "const require = __module.createRequire(import.meta.url)"
154+
, "const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url))"
155+
, "const __filename=new URL(import.meta.url).pathname"
156+
]

test-fixtures/bundle-app-browser-map.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-fixtures/bundle-app-browser-map.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-fixtures/bundle-app-browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* Generated by Spago v0 */
12
(() => {
23
// output/Effect.Console/foreign.js
34
var log = function(s) {

test-fixtures/bundle-app-node-map.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-fixtures/bundle-app-node-map.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test-fixtures/bundle-app-node.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env node
2-
import __module from 'module';import __path from 'path';import __url from 'url';const require = __module.createRequire(import.meta.url);const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url));const __filename=new URL(import.meta.url).pathname
2+
/* Generated by Spago v0 */import __module from 'module';import __path from 'path';import __url from 'url';const require = __module.createRequire(import.meta.url);const __dirname = __path.dirname(__url.fileURLToPath(import.meta.url));const __filename=new URL(import.meta.url).pathname
33

44
// output/Effect.Console/foreign.js
55
var log = function(s) {

0 commit comments

Comments
 (0)