Skip to content

Commit 678e004

Browse files
sigma-andexf-fJordanMartinezsd-yip
authored
Reimplement spago run for 0.15 (ES modules) (#866)
Co-authored-by: Fabrizio Ferrai <fabrizio.ferrai@gmail.com> Co-authored-by: Jordan Martinez <jordanalex.martinez@protonmail.com> Co-authored-by: Nicholas Yip <nicholasyip@obceit.cc>
1 parent 7465075 commit 678e004

15 files changed

+290
-83
lines changed

.github/workflows/build.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ jobs:
9595
$HOME\AppData\Local\Programs\stack\x86_64-windows
9696
key: ${{ runner.os }}-${{ hashFiles('stack.yaml') }}-1
9797

98-
- run: npm install -g purescript@0.14.0 psc-package@3.0.1 bower@1.8.8
98+
- run: npm install -g purescript@0.14.0 psc-package@3.0.1 bower@1.8.8 esbuild@0.14.28
9999

100100
- name: Install dependencies
101101
run: |
@@ -112,6 +112,13 @@ jobs:
112112
run: ./scripts/fix-home stack install
113113
shell: bash
114114

115-
- name: Run tests
115+
- name: Run tests (PureScript < 0.15.0)
116116
run: ./scripts/fix-home stack test
117117
shell: bash
118+
119+
- name: Install PureScript 0.15.0
120+
run: npm install -g purescript@next
121+
122+
- name: Run tests (PureScript >= 0.15.0)
123+
shell: bash
124+
run: ./scripts/fix-home stack test --ta "--match purs-0.15"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Features:
1313
- Support Glibc versions >= `2.24`
1414
- Retry git clone up to two times (#834, #873)
1515

16+
Bugfixes
17+
- Fix `spago run` and `spago test` to accept command line arguments correctly, by writing a JS file to run (#865, #866)
18+
- Remove support for node versions older than 12.0.0 as they do not work with es modules (#866)
19+
1620
## [0.20.7] - 2022-02-12
1721

1822
Bugfixes

spago.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ library
8787
Spago.Bower
8888
Spago.Build
8989
Spago.CLI
90+
Spago.Cmd
9091
Spago.Command.Init
9192
Spago.Command.Ls
9293
Spago.Command.Path

src/Spago/Build.hs

Lines changed: 71 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import qualified Data.List.NonEmpty as NonEmpty
1717
import qualified Data.Map as Map
1818
import qualified Data.Set as Set
1919
import qualified Data.Text as Text
20+
import qualified Data.Versions as Version
2021
import System.Directory (getCurrentDirectory)
2122
import System.FilePath (splitDirectories)
2223
import qualified System.FilePath.Glob as Glob
@@ -36,6 +37,7 @@ import qualified Spago.Packages as Packages
3637
import qualified Spago.Purs as Purs
3738
import qualified Spago.Templates as Templates
3839
import qualified Spago.Watch as Watch
40+
import qualified Spago.Cmd as Cmd
3941

4042

4143
prepareBundleDefaults
@@ -50,7 +52,7 @@ prepareBundleDefaults maybeModuleName maybeTargetPath maybePlatform = (moduleNam
5052
platform = fromMaybe Browser maybePlatform
5153

5254
-- eventually running some other action after the build
53-
build :: HasBuildEnv env => Maybe (RIO Env ()) -> RIO env ()
55+
build :: HasBuildEnv env => Maybe (RIO env ()) -> RIO env ()
5456
build maybePostBuild = do
5557
logDebug "Running `spago build`"
5658
BuildOptions{..} <- view (the @BuildOptions)
@@ -143,8 +145,7 @@ build maybePostBuild = do
143145
checkImports
144146

145147
buildAction globs = do
146-
env <- Run.getEnv
147-
let action = buildBackend globs >> (runRIO env $ fromMaybe (pure ()) maybePostBuild)
148+
let action = buildBackend globs >> (fromMaybe (pure ()) maybePostBuild)
148149
runCommands "Before" beforeCommands
149150
action `onException` (runCommands "Else" elseCommands)
150151
runCommands "Then" thenCommands
@@ -309,6 +310,22 @@ script modulePath tag packageDeps opts = do
309310

310311
data RunDirectories = RunDirectories { sourceDir :: FilePath, executeDir :: FilePath }
311312

313+
data NodeEsSupport = Unsupported Version.SemVer | Experimental | Supported
314+
315+
hasNodeEsSupport :: (HasLogFunc env) => RIO env NodeEsSupport
316+
hasNodeEsSupport = do
317+
nodeVersion <- Cmd.getCmdVersion "node"
318+
case nodeVersion of
319+
Left err -> do
320+
logDebug $ display $ "Unable to get Node.js version: " <> displayShow err
321+
pure Supported
322+
Right nv@Version.SemVer{} | Version._svMajor nv < 12 ->
323+
pure $ Unsupported nv
324+
Right nv@Version.SemVer{} | Version._svMajor nv >= 12 && Version._svMajor nv < 13 ->
325+
pure Experimental
326+
_ -> pure Supported
327+
328+
312329
-- | Run the project with node (or the chosen alternate backend):
313330
-- compile and run the provided ModuleName
314331
runBackend
@@ -323,15 +340,17 @@ runBackend
323340
runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybeSuccessMessage failureMessage extraArgs = do
324341
logDebug $ display $ "Running with backend: " <> fromMaybe "nodejs" maybeBackend
325342
BuildOptions{ pursArgs } <- view (the @BuildOptions)
326-
isES <- Purs.hasMinPursVersion "0.15.0"
327-
let postBuild = maybe (nodeAction isES $ Path.getOutputPath pursArgs) backendAction maybeBackend
343+
let
344+
postBuild = maybe (nodeAction $ Path.getOutputPath pursArgs) backendAction maybeBackend
328345
build (Just postBuild)
329346
where
330347
fromFilePath = Text.pack . Turtle.encodeString
348+
runJsSource = fromFilePath (sourceDir Turtle.</> ".spago/run.js")
349+
packageJson = fromFilePath (sourceDir Turtle.</> ".spago/package.json")
331350
nodeArgs = Text.intercalate " " $ map unBackendArg extraArgs
332351
esContents outputPath' =
333352
fold
334-
[ "import { main } from '"
353+
[ "import { main } from 'file://"
335354
, Text.replace "\\" "/" (fromFilePath sourceDir)
336355
, "/"
337356
, Text.pack outputPath'
@@ -352,19 +371,36 @@ runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybe
352371
, unModuleName moduleName
353372
, "').main()"
354373
]
355-
nodeCmd isES outputPath'=
356-
if isES then
357-
"node --input-type=module -e \"" <> esContents outputPath' <> "\" -- " <> nodeArgs
358-
else
359-
"node -e \"" <> cjsContents outputPath' <> "\" -- " <> nodeArgs
360-
361-
nodeAction isES outputPath' = do
374+
nodeContents isES outputPath' =
375+
if isES then esContents outputPath'
376+
else cjsContents outputPath'
377+
378+
packageJsonContents = "{\"type\":\"module\" }"
379+
380+
nodeCmd isES Experimental | isES = "node --experimental-modules \"" <> runJsSource <> "\" " <> nodeArgs
381+
nodeCmd _ _ = "node \"" <> runJsSource <> "\" " <> nodeArgs
382+
383+
nodeAction outputPath' = do
384+
isES <- Purs.hasMinPursVersion "0.15.0-alpha-01"
385+
nodeVersion <- hasNodeEsSupport
386+
case (isES, nodeVersion) of
387+
(True, Unsupported nv) ->
388+
die [ "Unsupported Node.js version: " <> display (Version.prettySemVer nv), "Required Node.js version >=12." ]
389+
_ ->
390+
pure ()
391+
logDebug $ "Writing " <> displayShow @Text runJsSource
392+
writeTextFile runJsSource (nodeContents isES outputPath')
393+
void $ chmod executable $ pathFromText runJsSource
394+
if isES then do
395+
logDebug $ "Writing " <> displayShow @Text packageJson
396+
writeTextFile packageJson packageJsonContents
397+
else pure ()
362398
-- cd to executeDir in case it isn't the same as sourceDir
363399
logDebug $ "Executing from: " <> displayShow @FilePath executeDir
364400
Turtle.cd executeDir
365401
-- We build a process by hand here because we need to forward the stdin to the backend process
366-
logDebug $ "Running node command: `" <> display (nodeCmd isES outputPath') <> "`"
367-
let processWithStdin = (Process.shell (Text.unpack $ nodeCmd isES outputPath')) { Process.std_in = Process.Inherit }
402+
logDebug $ "Running node command: `" <> display (nodeCmd isES nodeVersion) <> "`"
403+
let processWithStdin = (Process.shell (Text.unpack $ nodeCmd isES nodeVersion)) { Process.std_in = Process.Inherit }
368404
Turtle.system processWithStdin empty >>= \case
369405
ExitSuccess -> maybe (pure ()) (logInfo . display) maybeSuccessMessage
370406
ExitFailure n -> die [ display failureMessage <> "exit code: " <> repr n ]
@@ -376,26 +412,28 @@ runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybe
376412
ExitFailure n -> die [ display failureMessage <> "Backend " <> displayShow backend <> " exited with error:" <> repr n ]
377413

378414

379-
bundleWithEsbuild :: HasLogFunc env => WithMain -> ModuleName -> TargetPath -> Platform -> Minify -> RIO env ()
380-
bundleWithEsbuild withMain (ModuleName moduleName) (TargetPath targetPath) platform minify = do
415+
bundleWithEsbuild :: HasLogFunc env => WithMain -> WithSrcMap -> ModuleName -> TargetPath -> Platform -> Minify -> RIO env ()
416+
bundleWithEsbuild withMain srcMap (ModuleName moduleName) (TargetPath targetPath) platform minify = do
381417
esbuild <- getESBuild
382418
let
383419
platformOpt = case platform of
384-
Browser -> "browser"
385-
Node -> "node"
420+
Browser -> ["--platform=browser"]
421+
Node -> ["--platform=node"]
386422
minifyOpt = case minify of
387-
NoMinify -> ""
388-
Minify -> " --minify"
389-
cmd = case withMain of
390-
WithMain ->
391-
"echo \"import { main } from './output/" <> moduleName <> "/index.js'\nmain()\" | "
392-
<> esbuild <> " --platform=" <> platformOpt <> minifyOpt <> " --bundle "
393-
<> " --outfile=" <> targetPath
423+
NoMinify -> []
424+
Minify -> ["--minify"]
425+
srcMapOpt = case srcMap of
426+
WithSrcMap -> ["--sourcemap"]
427+
WithoutSrcMap -> []
428+
esbuildBase = platformOpt <> minifyOpt <> srcMapOpt <> ["--format=esm", "--bundle", "--outfile=" <> targetPath]
429+
(input, cmd) = case withMain of
430+
WithMain -> do
431+
let
432+
echoLine = "import { main } from './output/" <> moduleName <> "/index.js'; main();"
433+
(Turtle.textToLine echoLine, esbuild :| esbuildBase)
394434
WithoutMain ->
395-
esbuild <> " --platform=" <> platformOpt <> minifyOpt <> " --bundle "
396-
<> "output/" <> moduleName <> "/index.js"
397-
<> " --outfile=" <> targetPath
398-
runWithOutput cmd
435+
(Nothing, esbuild :| esbuildBase <> ["output/" <> moduleName <> "/index.js"])
436+
runProcessWithOutput cmd input
399437
("Bundle succeeded and output file to " <> targetPath)
400438
"Bundle failed."
401439
where
@@ -414,12 +452,12 @@ bundleModule
414452
-> UsePsa
415453
-> RIO env ()
416454
bundleModule withMain BundleOptions { maybeModuleName, maybeTargetPath, maybePlatform, minify, noBuild } buildOpts usePsa = do
417-
isES <- Purs.hasMinPursVersion "0.15.0"
455+
isES <- Purs.hasMinPursVersion "0.15.0-alpha-01"
418456
let
419457
(moduleName, targetPath, platform) = prepareBundleDefaults maybeModuleName maybeTargetPath maybePlatform
420458
bundleAction =
421459
if isES then
422-
bundleWithEsbuild withMain moduleName targetPath platform minify
460+
bundleWithEsbuild withMain (withSourceMap buildOpts) moduleName targetPath platform minify
423461
else case withMain of
424462
WithMain -> Purs.bundle WithMain (withSourceMap buildOpts) moduleName targetPath
425463
WithoutMain ->
@@ -437,7 +475,7 @@ bundleModule withMain BundleOptions { maybeModuleName, maybeTargetPath, maybePla
437475
Left (n :: SomeException) -> die [ "Make module failed: " <> repr n ]
438476
case noBuild of
439477
DoBuild -> Run.withBuildEnv usePsa buildOpts $ build (Just bundleAction)
440-
NoBuild -> Run.getEnv >>= (flip runRIO) bundleAction
478+
NoBuild -> Run.withBuildEnv usePsa buildOpts bundleAction
441479

442480
docsSearchTemplate :: (HasType LogFunc env, HasType PursCmd env) => RIO env Text
443481
docsSearchTemplate = ifM (Purs.hasMinPursVersion "0.14.0")

src/Spago/Cmd.hs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Spago.Cmd (getCmdVersion) where
2+
3+
import qualified Spago.Messages as Messages
4+
import qualified Turtle.Bytes
5+
import Spago.Prelude
6+
import qualified Data.Text as Text
7+
import qualified Data.Text.Encoding as Text.Encoding
8+
import qualified Data.Text.Encoding.Error as Text.Encoding
9+
import qualified Data.Versions as Version
10+
11+
-- | Get the semantic version of a command, e.g. purs --version
12+
getCmdVersion :: forall io. MonadIO io => Text -> io (Either Text Version.SemVer)
13+
getCmdVersion cmd =
14+
Turtle.Bytes.shellStrictWithErr (cmd <> " --version") empty >>= \case
15+
(ExitSuccess, out, _err) -> do
16+
let versionText = headMay $ Text.split (== ' ') (Text.strip $ Text.Encoding.decodeUtf8With lenientDecode out)
17+
parsed = versionText >>= (\vt -> Text.stripPrefix "v" vt <|> Just vt) >>= (hush . Version.semver)
18+
19+
pure $ case parsed of
20+
Nothing ->
21+
Left $
22+
Messages.failedToParseCommandOutput
23+
(cmd <> " --version")
24+
(Text.Encoding.decodeUtf8With Text.Encoding.lenientDecode out)
25+
Just p -> Right p
26+
(_, _out, _err) -> pure $ Left $ "Failed to run '" <> cmd <> " --version'"

src/Spago/Prelude.hs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ module Spago.Prelude
6767
, findExecutableOrDie
6868
, findExecutable
6969
, runWithOutput
70+
, runProcessWithOutput
7071
-- * Other
7172
, Dhall.Core.throws
7273
, repr
@@ -108,7 +109,7 @@ import RIO as X hiding (FilePath, fi
108109
import RIO.Orphans as X
109110
import Safe (headMay, lastMay)
110111
import System.FilePath (isAbsolute, pathSeparator, (</>))
111-
import Turtle (FilePath, appendonly, chmod,
112+
import Turtle (FilePath, Line, appendonly, chmod,
112113
executable, mktree, repr, shell,
113114
shellStrict, shellStrictWithErr,
114115
systemStrictWithErr, testdir)
@@ -271,6 +272,14 @@ runWithOutput command success failure = do
271272
ExitSuccess -> logInfo $ display success
272273
ExitFailure _ -> die [ display failure ]
273274

275+
-- | Run the given command.
276+
runProcessWithOutput :: HasLogFunc env => NonEmpty Text -> Maybe Line -> Text -> Text -> RIO env ()
277+
runProcessWithOutput (command :| arguments) input success failure = do
278+
logDebug $ "Running command: `" <> display (Text.intercalate " " $ command : arguments) <> "`"
279+
Turtle.proc command arguments (Turtle.select input) >>= \case
280+
ExitSuccess -> logInfo $ display success
281+
ExitFailure _ -> die [ display failure ]
282+
274283
-- | Return the full path of the executable we're trying to call,
275284
-- or die trying
276285
findExecutableOrDie :: HasLogFunc env => String -> RIO env Text

src/Spago/Purs.hs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@ import Spago.Env
1919
import qualified Data.ByteString.Lazy as BSL
2020
import qualified Data.Text as Text
2121
import qualified Data.Text.Encoding as Text.Encoding
22-
import qualified Data.Text.Encoding.Error as Text.Encoding
2322
import qualified Data.Versions as Version
2423

2524
import qualified Spago.Messages as Messages
2625
import qualified Turtle.Bytes
27-
26+
import qualified Spago.Cmd as Cmd
2827

2928
compile
3029
:: (HasPurs env, HasLogFunc env)
@@ -130,19 +129,7 @@ docs format sourcePaths = do
130129
"Docs generation failed."
131130

132131
pursVersion :: RIO env (Either Text Version.SemVer)
133-
pursVersion = Turtle.Bytes.shellStrictWithErr (purs <> " --version") empty >>= \case
134-
(ExitSuccess, out, _err) -> do
135-
let versionText = headMay $ Text.split (== ' ') (Text.strip $ Text.Encoding.decodeUtf8With lenientDecode out)
136-
parsed = versionText >>= (hush . Version.semver)
137-
138-
pure $ case parsed of
139-
Nothing -> Left $ Messages.failedToParseCommandOutput
140-
(purs <> " --version")
141-
(Text.Encoding.decodeUtf8With Text.Encoding.lenientDecode out)
142-
Just p -> Right p
143-
(_, _out, _err) -> pure $ Left $ "Failed to run '" <> purs <> " --version'"
144-
where
145-
purs = "purs"
132+
pursVersion = Cmd.getCmdVersion "purs"
146133

147134
hasMinPursVersion :: (HasLogFunc env, HasPurs env) => Text -> RIO env Bool
148135
hasMinPursVersion maybeMinVersion = do

0 commit comments

Comments
 (0)