@@ -2,10 +2,15 @@ module Spago.Command.Bundle where
22
33import Spago.Prelude
44
5+ import Data.Array (all , fold , take )
6+ import Data.String as Str
7+ import Data.String.Utils (startsWith )
58import Node.Path as Path
69import Spago.Cmd as Cmd
710import Spago.Config (BundlePlatform (..), BundleType (..), Workspace , WorkspacePackage )
811import Spago.Esbuild (Esbuild )
12+ import Spago.FS as FS
13+ import Spago.Generated.BuildInfo as BuildInfo
914
1015type 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
3945run = 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+ ]
0 commit comments