-
Notifications
You must be signed in to change notification settings - Fork 134
Expand file tree
/
Copy pathBundle.purs
More file actions
208 lines (187 loc) Β· 7.6 KB
/
Copy pathBundle.purs
File metadata and controls
208 lines (187 loc) Β· 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
module Spago.Command.Bundle where
import Spago.Prelude
import Data.Array (all, fold, take)
import Data.Array.NonEmpty as NEA
import Data.Map as Map
import Data.String as Str
import Data.String.Utils (startsWith)
import Spago.Cmd as Cmd
import Spago.Command.Build as Build
import Spago.Command.Fetch as Fetch
import Spago.Config (BundlePlatform(..), BundleType(..), Workspace, WorkspacePackage)
import Spago.Esbuild (Esbuild)
import Spago.FS as FS
import Spago.Generated.BuildInfo as BuildInfo
import Spago.Path as Path
import Spago.Purs (Purs, ModuleGraph(..))
import Spago.Purs as Purs
import Spago.Purs.EntryPoint as EntryPoint
type BundleEnv a =
{ esbuild :: Esbuild
, logOptions :: LogOptions
, rootPath :: RootPath
, bundleOptions :: BundleOptions
, workspace :: Workspace
, selected :: WorkspacePackage
, purs :: Purs
, dependencies :: Fetch.PackageTransitiveDeps
| a
}
type BundleOptions =
{ minify :: Boolean
, sourceMaps :: Boolean
, module :: String
, outfile :: RawFilePath
, force :: Boolean
, platform :: BundlePlatform
, type :: BundleType
, extraArgs :: Array String
}
run :: β a. Spago (BundleEnv a) Unit
run = do
{ rootPath, esbuild, selected, workspace, bundleOptions: opts } <- ask
logDebug $ "Bundle options: " <> show opts
let
minify = if opts.minify then [ "--minify" ] else []
sourceMap = if opts.sourceMaps then [ "--sourcemap" ] else []
outfile = selected.path </> opts.outfile
format = case opts.platform, opts.type of
BundleBrowser, BundleApp -> "--format=iife"
_, _ -> "--format=esm"
onlyForNode s = case opts.platform of
BundleNode -> s
BundleBrowser -> ""
output = workspace.buildOptions.output # fromMaybe (rootPath </> "output")
-- TODO: we might need to use `Path.relative selected.path output` instead of just output there
mainPath = Path.localPart $ Path.withForwardSlashes $ output </> opts.module </> "index.js"
{ input, entrypoint } = case opts.type of
BundleApp ->
{ entrypoint: []
, input: Cmd.StdinWrite $ fold [ onlyForNode "#!/usr/bin/env node\n\n", "import { main } from './", mainPath, "';main();" ]
}
BundleModule ->
{ entrypoint: [ mainPath ]
, input: Cmd.StdinNewPipe
}
execOptions = Cmd.defaultExecOptions { pipeStdin = input, cwd = Just (Path.toGlobal rootPath) }
banner = fold
[ bundleWatermarkPrefix
, BuildInfo.packages."spago-bin"
, " */"
, onlyForNode nodeTargetPolyfill
]
args = fold
[ [ "--bundle"
, "--outfile=" <> (Path.toRaw outfile)
, "--platform=" <> show opts.platform
, "--banner:js=" <> banner
, "--loader:.node=file" -- See https://github.com/evanw/esbuild/issues/1051
, format
]
, opts.extraArgs
, minify
, sourceMap
, entrypoint
]
-- Check that the entry module exports a `main` function when bundling an app
when (opts.type == BundleApp) do
validateMainExport opts.module
-- FIXME: remove this after 2024-12-01
whenM (FS.exists $ rootPath </> checkWatermarkMarkerFileName)
$ unless opts.force
$ whenM (isNotSpagoGeneratedFile outfile)
$ die [ "Target file " <> Path.quote outfile <> " was not previously generated by Spago. Use --force to overwrite anyway." ]
logInfo "Bundling..."
logDebug $ "Running esbuild: " <> show args
Cmd.exec esbuild.cmd args execOptions >>= case _ of
Right _ -> logSuccess "Bundle succeeded."
Left r -> do
logDebug $ Cmd.printExecResult r
die [ "Failed to bundle." ]
isNotSpagoGeneratedFile :: β a. LocalPath -> Spago (BundleEnv a) Boolean
isNotSpagoGeneratedFile path = do
exists <- FS.exists path
if not exists then
pure false
else
-- The first line of the file could be the marker, or it could the shebang
-- if the bundle was compiled for Node, in which case the marker will be the
-- second line. So we check the first two lines.
FS.readTextFile path
<#> Str.split (Str.Pattern "\n")
>>> take 2
>>> all (not startsWith bundleWatermarkPrefix)
bundleWatermarkPrefix :: String
bundleWatermarkPrefix = "/* Generated by Spago v"
-- Presence of this file gates the watermark check.
--
-- If this file exists in the current directory, the Bundle command will check
-- if the target bundle file already exists and has the watermark in it, and if
-- it doesn't have the watermark, will refuse to overwrite it for fear of
-- overwriting a user-generated file.
--
-- We gate this check on the presence of this file so that the check is only
-- performed in a controlled context (such as integration tests), but doesn't
-- work for normal users. The idea is that the users who upgrade their Spago
-- aren't immediately met with a refusal to overwrite the bundle. Instead, Spago
-- will overwrite the bundle just fine, but now the bundle will have the
-- watermark in it. Then, after some time, after enough users have upgraded and
-- acquired the watermark in their bundles, we will remove this gating
-- mechanism, and watermark checking will start working for normal users.
checkWatermarkMarkerFileName :: String
checkWatermarkMarkerFileName = ".check-bundle-watermark"
-- A polyfill inserted when building for Node to work around this esbuild issue:
-- https://github.com/evanw/esbuild/issues/1921
nodeTargetPolyfill :: String
nodeTargetPolyfill = Str.joinWith ";"
[ "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"
]
-- | Validate that the entry module declares and exports a `main` function
validateMainExport :: forall a. String -> Spago (BundleEnv a) Unit
validateMainExport moduleName = do
{ rootPath, selected, dependencies } <- ask
let
globs = Build.getBuildGlobs
{ rootPath
, dependencies:
let
{ core, test } = unsafeFromJust $ Map.lookup selected.package.name dependencies
in
Map.union core test
, depsOnly: false
, withTests: false
, selected: NEA.singleton selected
}
Purs.graph rootPath globs [] >>= case _ of
Left err -> logWarn $ "Could not verify main export: " <> show err
Right (ModuleGraph graph) ->
case Map.lookup moduleName graph of
Nothing ->
die
[ "Cannot bundle app: module " <> moduleName <> " was not found in the build."
, ""
, "Make sure the module exists and is included in your build."
]
Just { path } -> do
sourceCode <- FS.readTextFile (rootPath </> path)
case EntryPoint.hasMainExport sourceCode of
EntryPoint.MainExported -> pure unit
EntryPoint.MainWrongType ->
logWarn "The `main` function does not have the expected type `Effect Unit`. The bundle may not work correctly."
EntryPoint.MainNotDeclared ->
die
[ "Cannot bundle app: module " <> moduleName <> " does not declare a `main` function."
, "If you want to create a bundle without an entry point, use --bundle-type=module instead."
]
EntryPoint.MainNotExported ->
die
[ "Cannot bundle app: module " <> moduleName <> " does not export `main`."
, "Add `main` to the module's export list, remove the explicit export list, or use --bundle-type=module."
]
EntryPoint.ParseError err ->
logDebug $ "Could not verify main export: " <> err