├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .jshintrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bower.json ├── index.js ├── package-lock.json ├── package.json ├── src ├── Main.js ├── Main.purs └── Pulp │ ├── Args.purs │ ├── Args │ ├── Get.purs │ ├── Help.js │ ├── Help.purs │ ├── Parser.purs │ └── Types.purs │ ├── Browserify.js │ ├── Browserify.purs │ ├── Build.purs │ ├── BumpVersion.purs │ ├── Docs.purs │ ├── Exec.purs │ ├── Files.js │ ├── Files.purs │ ├── Git.purs │ ├── Init.js │ ├── Init.purs │ ├── Login.purs │ ├── Outputter.purs │ ├── PackageManager.purs │ ├── Project.purs │ ├── Publish.purs │ ├── Repl.purs │ ├── Run.purs │ ├── Server.purs │ ├── Shell.purs │ ├── Sorcery.js │ ├── Sorcery.purs │ ├── System │ ├── FFI.js │ ├── FFI.purs │ ├── Files.js │ ├── Files.purs │ ├── HTTP.purs │ ├── Read.js │ ├── Read.purs │ ├── Require.js │ ├── Require.purs │ ├── StaticServer.js │ ├── StaticServer.purs │ ├── Stream.js │ ├── Stream.purs │ ├── SupportsColor.js │ ├── SupportsColor.purs │ ├── TreeKill.js │ ├── TreeKill.purs │ ├── Which.js │ └── Which.purs │ ├── Test.purs │ ├── Utils.purs │ ├── Validate.purs │ ├── Version.purs │ ├── VersionBump.purs │ ├── Versions │ └── PureScript.purs │ ├── Watch.js │ └── Watch.purs ├── test-js ├── cjs │ ├── Main.js │ └── Main.purs ├── es │ ├── Main.js │ └── Main.purs ├── integration.js └── sh.js └── test └── Main.purs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | env: 9 | PURS_BUILD_VERSION: v0.14.5 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | purs_test_version: [v0.12.0, v0.12.4, v0.12.5, v0.13.0, v0.14.0, v0.14.5, v0.15.0-alpha-01] 17 | fail-fast: false 18 | runs-on: ${{ matrix.os }} 19 | env: 20 | PURS_TEST_VERSION: ${{ matrix.purs_test_version }} 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: "12" 27 | 28 | - name: "non-Windows - Download PureScript (Build version)" 29 | shell: bash 30 | if: runner.os != 'Windows' 31 | run: | 32 | DIR=$HOME/bin/purescript-$PURS_BUILD_VERSION 33 | mkdir -p $DIR 34 | wget -O $DIR/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$PURS_BUILD_VERSION/linux64.tar.gz 35 | tar -xvf $DIR/purescript.tar.gz -C $DIR --strip-components 1 purescript/purs 36 | chmod a+x $DIR/purs 37 | 38 | - name: "non-Windows - Download PureScript (Test version)" 39 | if: runner.os != 'Windows' && env.PURS_BUILD_VERSION != env.PURS_TEST_VERSION 40 | shell: bash 41 | run: | 42 | DIR=$HOME/bin/purescript-$PURS_TEST_VERSION 43 | mkdir -p $DIR 44 | wget -O $DIR/purescript.tar.gz https://github.com/purescript/purescript/releases/download/$PURS_TEST_VERSION/linux64.tar.gz 45 | tar -xvf $DIR/purescript.tar.gz -C $DIR --strip-components 1 purescript/purs 46 | chmod a+x $DIR/purs 47 | 48 | - name: "non-Windows - Run NPM install" 49 | if: runner.os != 'Windows' 50 | run: | 51 | echo "Using Purs with version:" 52 | PATH="$HOME/bin/purescript-$PURS_BUILD_VERSION:$PATH" 53 | purs --version 54 | npm install 55 | 56 | - name: "non-Windows - Run NPM test (unit)" 57 | if: runner.os != 'Windows' 58 | run: | 59 | echo "Using Purs with version:" 60 | PATH="$HOME/bin/purescript-$PURS_BUILD_VERSION:$PATH" 61 | purs --version 62 | npm run test:unit 63 | 64 | - name: "non-Windows - Run NPM test (integration)" 65 | if: runner.os != 'Windows' 66 | run: | 67 | echo "Using Purs with version:" 68 | PATH="$HOME/bin/purescript-$PURS_TEST_VERSION:$PATH" 69 | purs --version 70 | npm run test:integration 71 | 72 | 73 | - name: "Windows - Download PureScript (Build version)" 74 | shell: bash 75 | if: runner.os == 'Windows' 76 | run: | 77 | pushd C:\\tools 78 | DIR=purescript-$PURS_BUILD_VERSION 79 | mkdir -p $DIR 80 | curl -opurescript.tar.gz -L https://github.com/purescript/purescript/releases/download/$PURS_BUILD_VERSION/win64.tar.gz 81 | tar -xvzf purescript.tar.gz -C $DIR --strip-components 1 purescript/purs.exe 82 | ls . 83 | ls purescript-$PURS_BUILD_VERSION 84 | popd 85 | 86 | - name: "Windows - Download PureScript (Test version)" 87 | if: runner.os == 'Windows' && env.PURS_BUILD_VERSION != env.PURS_TEST_VERSION 88 | shell: bash 89 | run: | 90 | pushd C:\\tools 91 | DIR=purescript-$PURS_TEST_VERSION 92 | mkdir -p $DIR 93 | curl -opurescript.tar.gz -L https://github.com/purescript/purescript/releases/download/$PURS_TEST_VERSION/win64.tar.gz 94 | tar -xvzf purescript.tar.gz -C $DIR --strip-components 1 purescript/purs.exe 95 | popd 96 | 97 | - name: "Windows - Run NPM install" 98 | if: runner.os == 'Windows' 99 | run: | 100 | echo "Using Purs with version:" 101 | $env:Path="C:\tools\purescript-$env:PURS_BUILD_VERSION;$env:Path" 102 | purs.exe --version 103 | npm install 104 | 105 | - name: "Windows - Run NPM test (unit)" 106 | if: runner.os == 'Windows' 107 | run: | 108 | echo "Using Purs with version:" 109 | $env:Path="C:\tools\purescript-$env:PURS_BUILD_VERSION;$env:Path" 110 | purs.exe --version 111 | npm run test:unit 112 | 113 | - name: "Windows - Run NPM test (integration)" 114 | if: runner.os == 'Windows' 115 | run: | 116 | echo "Using Purs with version:" 117 | $env:Path="C:\tools\purescript-$env:PURS_TEST_VERSION;$env:Path" 118 | purs.exe --version 119 | npm run test:integration 120 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | .tern-port 3 | /TAGS 4 | /bower_components/ 5 | /output/ 6 | .psci 7 | .psci_modules/ 8 | /pulp.js 9 | .pulp-cache/ 10 | .browserify-cache.json 11 | /.psvm/ 12 | /unit-tests.js 13 | .psc* 14 | .purs-repl 15 | 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "eqeqeq": true, 4 | "forin": true, 5 | "freeze": true, 6 | "funcscope": true, 7 | "futurehostile": true, 8 | "globalstrict": true, 9 | "latedef": true, 10 | "noarg": true, 11 | "nocomma": true, 12 | "nonew": true, 13 | "notypeof": true, 14 | "singleGroups": true, 15 | "undef": true, 16 | "unused": true, 17 | "eqnull": true, 18 | "globals": { 19 | "exports": true, 20 | "process": true, 21 | "console": false, 22 | "require": false, 23 | "__dirname": false, 24 | "__filename": false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Pulp Release History 2 | 3 | ## 16.0.0 4 | 5 | Breaking: 6 | * Increased minimum `purs` version to `v0.12.0` and dropped support for all 7 | compiler versions earlier than that. (#399 and #405 by @JordanMartinez) 8 | * Increased minimum `psa` version to `v0.7.0` and dropped support for all 9 | versions earlier than that. (#399 by @JordanMartinez) 10 | * Increased minimum `node` version to `v12.0.0` and dropped support for all 11 | versions earlier than that. (#401 by @JordanMartinez) 12 | * Include `bower.json`'s `devDependencies` field when publishing (#405 by @JordanMartinez) 13 | 14 | Note: `pulp publish` will fail if a dependency (e.g. `purescript-numbers`) 15 | exists in both the `bower.json` file's `dependencies` and `devDependencies` field. 16 | When this occurs, you will see a message like the following: 17 | 18 | ``` 19 | There is a problem with your package, which meant that it could not be published. 20 | Details: 21 | The following dependency does not appear to be installed: 22 | * purescript-numbers 23 | ``` 24 | 25 | Other improvements: 26 | - Added a `debug` flag that enables debugger output for the `pulp publish` command (#405 by @JordanMartinez) 27 | - Improve error message due to `pulp publish` needing to be run twice (#406 by @JordanMartinez) 28 | 29 | Whenever one runs `pulp publish` for the first time, the command will almost always fail with 30 | an HTTP 400 error due to invalid JSON. Running the command a second time will succeed and finish 31 | what was started in the first one. However, this fact is never communicated to users, so 32 | one can only handle this situation if they already know about it (e.g. aren't new users). 33 | 34 | The error message now suggests the user try running it a second time to get around this issue. 35 | 36 | Internal: 37 | * Added support for the `v0.15.0` compiler version (#401 by @JordanMartinez) 38 | * Update project and its dependencies to use PureScript `v0.14.5` and 39 | `v0.14.0`-compatible libraries. (#399 by @JordanMartinez) 40 | * Migrated from Travis CI to GitHub Actions. (#399 by @JordanMartinez) 41 | * Updated CI integration tests to verify `pulp` works when 42 | the compiler version used is `v0.12.x`, `v0.13.x`, and `v0.14.x`. 43 | (#399 by @JordanMartinez) 44 | 45 | ## 15.0.0 46 | 47 | * Remove the check for `main` being an appropriate entry point when generating 48 | a call to `main`. This previously relied on a compiler-internal API (the 49 | externs.json files) which the compiler no longer produces as of v0.13.8. Note 50 | that previous versions of Pulp can work around this bug by passing the 51 | `--no-check-main` flag. (#392, @garyb). 52 | 53 | ## 14.0.0 54 | 55 | * Stop attempting to register on the Bower registry on publish, since the Bower 56 | registry no longer accepts new registrations. Instead, require the user to 57 | register their package in the purescript/registry repo (#388). 58 | 59 | ## 13.0.0 60 | 61 | * Changes to support `purs >= 0.13.0` in `pulp docs`. Because of idiosyncrasies 62 | in the previous `purs docs` CLI, it has not been possible to support both, 63 | so support for `purs < 0.13.0` in `pulp docs` has been dropped. 64 | * Remove the `--with-dependencies` option in `pulp docs`; now that html is the 65 | default output format of `purs docs`, it makes less sense to only produce 66 | local modules by default (since if we don't produce dependency modules then 67 | links will be broken). To restore the previous behaviour, you will need to 68 | run `pulp docs -- --format markdown`, and then manually delete non-local 69 | modules from the `generated-docs/md` directory. 70 | * Add a --build-path argument for `pulp docs`; to be passed to `purs docs` for 71 | `purs >= 0.13.0`, since `purs docs` now uses a compiler output directory. 72 | * Avoid using string-stream to fix `pulp browserify` on node 12.x (#380, 73 | @rnons). 74 | * Pass `follow: true` to `gaze` to follow symlinked directories (#378, @rnons). 75 | 76 | ## 12.4.2 77 | 78 | * Fix `pulp version` and `pulp publish`, which were both completely broken as 79 | of changes in v12.4.1 (@hdgarrood) 80 | 81 | ## 12.4.1 82 | 83 | * Switch to gaze instead of watchpack for `--watch` and `pulp server`; this 84 | fixes an issue where if you have source files which are symbolic links which 85 | point outside your source directory, changes to those files would not be 86 | picked up (@rnons, #371) 87 | * Fix an issue where `pulp version` and `pulp publish` would fail for packages 88 | which do not have any dependencies (@hdgarrood) 89 | 90 | ## 12.4.0 91 | 92 | * When running against a sufficiently new version of the PureScript compiler 93 | (specifically, v0.12.4 or later), when publishing, generate the new JSON 94 | format for resolutions files. This fixes an issue where Bower would produce 95 | out of memory errors when attempting to publish. (@hdgarrood, #351) 96 | 97 | ## 12.3.1 98 | 99 | * Bug fix: the compiler command line interface changed in 0.12 to replace the 100 | `--source-maps` option with a new `--codegen` option. `pulp` has now been 101 | made aware of this and so generating source maps should now work (with 102 | compilers from both before and after this change). (@nwolverson, #343) 103 | 104 | ## 12.3.0 105 | 106 | * Have `pulp init` generate projects based on compiler version: the `pulp init` 107 | command now has the ability to produce a project skeleton using either 108 | `Effect` or `Eff`. By default, `Effect` is chosen if the compiler version is 109 | at least 0.12.0, otherwise `Eff` is chosen. However, this behaviour can be 110 | overridden with the `--with-eff` or `--with-effect` flags to `pulp init`. 111 | (#340, @vladciobanu) 112 | 113 | ## 12.2.0 114 | 115 | * The type `Effect.Effect` is now considered by Pulp to be acceptable for the 116 | type of your program's entry point (usually `Main.main`). 117 | `Control.Monad.Eff.Eff` also continues to be acceptable. (#338) 118 | * Allow specifying a list of allowable types for your program's entry point, by 119 | separating them with commas. (#338) 120 | * Bug fix: allow specifying a specific version when running `pulp version` for 121 | the first time. (@plippe, #328) 122 | * Bug fix: Passthrough arguments with `pulp run` now go to your program, as the 123 | documentation claims, rather than to `purs`. (@kika, #309) 124 | * Bug fix: Pulp will no longer check that your program has an acceptable entry 125 | point when using `pulp browserify --standalone`, since there is no reason to 126 | do so. (#339) 127 | 128 | ## 12.1.0 129 | 130 | * Add source map support (@nwolverson, #305). 131 | 132 | ## 12.0.1 133 | 134 | * Fix a bug where running commands with `--watch` would sometimes produce 135 | an internal error (@thoradam, #300). 136 | 137 | ## 12.0.0 138 | 139 | * Add support for psc-package (@thoradam, #243). See the README for details. 140 | * Check that a program's entry point is of a suitable type when bundling (see https://github.com/purescript/purescript/issues/2086). By default `main` is required to be of type `Eff`, but this can be controlled using the `--check-main-type` flag. Alternatively this check can be turned off entirely using the `--no-check-main` flag. 141 | * Fix a bug where pulp would crash on uncommon operating systems (#299) 142 | * Fix an error in the help message for the `--else` option (@tkawachi, #294) 143 | 144 | ## 11.0.2 145 | 146 | * Fix a bug where running `pulp version` in a repo which didn't yet have any git tags would cause pulp to crash 147 | 148 | ## 11.0.1 149 | 150 | * Allow empty paths for the `--include` option (@anilanar, #263) 151 | * Various fixes to pulp's docs and `--help` output (@anttih) 152 | * If psa is being used, check that it is not too old (#272) 153 | * Use an exitcode of 1 on invalid options/commands (#285) 154 | * Set Cache-Control: no-cache in `pulp server` (@geigerzaehler, #288) 155 | 156 | ## 11.0.0 157 | 158 | * Compatibility with PureScript 0.11.x. Compatibility with 0.10.x and previous versions of the PureScript compiler has been dropped (@natefaubion). 159 | * Create a .purs-repl file during pulp init, to automatically import Prelude in new projects (@chexxor). 160 | 161 | ## 10.0.4 162 | 163 | * Fix an issue causing "EXDEV: cross-device link not permitted" errors in some configurations (#252). 164 | 165 | ## 10.0.3 166 | 167 | Nothing changed this release, I just messed up the 10.0.2 release so published another patch-level update. 168 | 169 | ## 10.0.2 170 | 171 | * Allow pulp to work with recent development builds of the PureScript compiler (#255, @sloosch). 172 | * Fix a missing space character in a message during 'pulp run' (#256, @bionicbrian). 173 | 174 | ## 10.0.1 175 | 176 | * Fix an issue where extra command line arguments were not being passed to test programs properly (#239, @mcoffin). 177 | 178 | ## 10.0.0 179 | 180 | ### Breaking changes 181 | 182 | * Explicit separation of passthrough arguments (#220). The original behaviour 183 | that unrecognised arguments were passed through to `psc` (or whichever 184 | underlying program `pulp` calls) has turned out to be confusing. Instead, 185 | passthrough arguments are now separated from pulp's arguments with a `--`. 186 | Any unrecognised arguments before the `--` will now cause pulp to complain. 187 | * `pulp server` has been broken since 9.0.0 and is now fixed! It also no longer 188 | uses webpack and purs-loader, instead it builds with the same mechanisms that 189 | `pulp build` uses, which should make things more reliable and easier (#151). 190 | This is a breaking change because some command line options for `pulp server` 191 | were removed as they were webpack-specific and therefore no longer 192 | applicable. 193 | * Remove options from `pulp run` which were not applicable and should never 194 | have been there: `--skip-entry-point`, `--to`, and `--optimise`. 195 | * Remove `pulp dep` (#234). `pulp dep` has been deprecated for quite a long 196 | time now. 197 | 198 | ### Other changes 199 | 200 | * Add `--jobs` for specifying parallelism in `psc` (#93). 201 | * Fix swallowing of "Compiling \" messages from `psc`. 202 | * Stop hardcoding version ranges in the initial bower.json created by `pulp 203 | init` (#231). Now, pulp delegates to `bower install --save` to select an 204 | appropriate version range. 205 | * Fix a bug where some arguments were mistakenly dropped instead of being 206 | passed to `psc-bundle` (#188). 207 | 208 | ## 9.1.0 209 | 210 | * Ignore .psc-ide-port, .psa-stash, and other dotfiles beginning with .psc or .psa in the default .gitignore file created by `pulp init` (@texastoland, #223, 225). 211 | * Bump version ranges in the default bower.json file created by `pulp init` to pick up the newest versions of core libraries (@Risto-Stevcev, #230). 212 | * Updated some npm dependencies to reduce the number of warnings you get when you run `npm install pulp`. 213 | 214 | ## 9.0.1 215 | 216 | * Improved error messages in the case where submitting a package to Pursuit as 217 | part of `pulp publish` fails. 218 | * Update README to use `bower uninstall` instead of the undocumented and 219 | presumably deprecated `bower rm` (@menelaos, #215). 220 | 221 | ## 9.0.0 222 | 223 | * Compatibility with version 0.9 of the PureScript compiler. Pulp no longer 224 | works with earlier versions of the PureScript compiler; to use earlier 225 | versions, you will need to downgrade to a previous version of Pulp. 226 | * Fix a bug where the version of psc being used was not being printed properly 227 | (#210). 228 | 229 | ## 8.2.1 230 | 231 | * Remove unused npm dependencies (`xhr2`, `ansi`, and `supports-color`). 232 | * Fix Pulp's description in package.json; Pulp is not a package manager. 233 | 234 | ## 8.2.0 235 | 236 | * Update the dependency on `watchpack` to fix deprecation warnings (#196). 237 | * Add a `--no-push` flag to `pulp publish`, allowing you to skip pushing 238 | tags to a Git remote as part of publishing (#201). 239 | * Add a `--push-to` option to `pulp publish`, allowing you to push to a 240 | specific Git remote (#201). 241 | 242 | ## 8.2.0-rc.3 243 | 244 | * Actually fix `pulp login` (which was *still* broken). 245 | * Include the response body for any errors from the GitHub API in `pulp login`. 246 | 247 | ## 8.2.0-rc.2 248 | 249 | * Remove the `moduleType` field from the bower.json file generated by `pulp 250 | init`. 251 | * Fix `pulp login` using the wrong environment variable for the home directory 252 | on Windows (#197). 253 | * Fix `pulp login` failing to check the auth token with GitHub (#199). 254 | * Don't require being inside a project to run `pulp init` (#200). 255 | 256 | ## 8.2.0-rc.1 257 | 258 | * Added `pulp version` for bumping versions and creating git tags for releases 259 | of PureScript packages, as well as `pulp publish` for sending those releases 260 | out into the world. See `pulp version --help` and `pulp publish --help` for 261 | more info. 262 | 263 | ## 8.1.1 264 | 265 | * Revert to an open Prelude import in the default code generated by `pulp 266 | init`. 267 | 268 | ## 8.1.0 269 | 270 | * Fix `pulp browserify` hanging on Windows. 271 | * Fix `--dependency-path` and `--monochrome` options not being honoured 272 | when using psa. 273 | * Add `pulp login`, which will be useful later along with the upcoming 274 | `pulp release`. 275 | * Fix compiler warnings in PureScript source files generated by `pulp init`. 276 | 277 | ## 8.0.0 278 | 279 | * Pulp's rebuild logic has been removed, as it was causing more 280 | trouble than it was worth. This means the `--force` flag is now once 281 | again only available on `pulp browserify`, to force a 282 | non-incremental build. 283 | * Pulp will now use the 284 | [`psa`](https://github.com/natefaubion/purescript-psa) tool instead 285 | of `psc` if available on your path. You can disable this behaviour 286 | by passing the `--no-psa` flag. 287 | 288 | Bugs fixed: #140. 289 | 290 | ## 7.0.0 291 | 292 | * Remove the `--engine` option, since the `--runtime` option fulfils the same 293 | need. 294 | * Fix the `--runtime` option, which was previously broken. (#143) 295 | * Fix a bug where Pulp was sometimes using terminal ANSI codes for colours 296 | when it shouldn't have been, and not using them when it should. (#147) 297 | * Relay interrupts (Ctrl+C) to `psci` when running `pulp psci`, so that 298 | long-running commands can be interrupted, and to stop the "hGetChar: 299 | hardware fault (Input/output error)" message from being shown. (#88) 300 | 301 | ## 6.2.1 302 | 303 | * Fix the `--watch` option, which was broken in 6.2.0. 304 | * Remove the `--optimise` option for `pulp test` and `pulp server`, since it 305 | doesn't really make sense with these commands. 306 | 307 | ## 6.2.0 308 | 309 | * `pulp dep` is now deprecated. It continues to work as before, but 310 | you will have to install Bower yourself in order to use it. It’s 311 | recommended that you use Bower directly instead. 312 | * New global options `--before` and `--else` to complement `--then`. 313 | * `--skip-entry-point` now works when using `pulp build --to`. 314 | 315 | ## 6.1.0 316 | 317 | * You can now use `pulp browserify --standalone ` to 318 | produce a browserified bundle wrapped in a UMD header, which can be 319 | `require()`d, and which re-exports the main module of your 320 | PureScript project. It works by invoking `browserify --standalone`; 321 | see 322 | [the Browserify documentation](https://github.com/substack/node-browserify#usage). 323 | 324 | ## 6.0.1 325 | 326 | * Remove unnecessary `postinstall` script. 327 | 328 | ## 6.0.0 329 | 330 | * Pulp has been ported to PureScript :) 331 | 332 | * The `--with-deps` flag for `pulp docs` has been renamed to 333 | `--with-dependencies`. 334 | 335 | Bugs fixed: #123, #122, #121, #111, #108, #92. 336 | 337 | ## 5.0.2 338 | 339 | Bugs fixed: #109. 340 | 341 | ## 5.0.1 342 | 343 | Bugs fixed: #105, #106, #107, #113. 344 | 345 | ## 5.0.0 346 | 347 | * Pulp will now skip the build step if your project hasn't changed 348 | since your last rebuild. 349 | 350 | * The `--force` flag is now avaliable on all commands that trigger a 351 | build, and will force a rebuild regardless of whether your project 352 | has changed. It still also forces a non-incremental build when 353 | used with `pulp browserify`. 354 | 355 | * You can now use the flags `--src-path`, `--test-path` and 356 | `--dependency-path` to override the normal locations for these 357 | directories. 358 | 359 | * The format for passing multiple directories to `--include` has 360 | changed: you now separate directories using the standard system path 361 | delimiter, as opposed to spaces. 362 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU Lesser General Public License version 3 2 | =========================================== 3 | 4 | Version 3, 29 June 2007 5 | 6 | Copyright © 2007 Free Software Foundation, Inc. 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | This version of the GNU Lesser General Public License incorporates the 12 | terms and conditions of version 3 of the GNU General Public License, 13 | supplemented by the additional permissions listed below. 14 | 15 | **0. Additional Definitions.** 16 | 17 | As used herein, “this License” refers to version 3 of the GNU Lesser 18 | General Public License, and the “GNU GPL” refers to version 3 of the GNU 19 | General Public License. 20 | 21 | “The Library” refers to a covered work governed by this License, other 22 | than an Application or a Combined Work as defined below. 23 | 24 | An “Application” is any work that makes use of an interface provided by 25 | the Library, but which is not otherwise based on the Library. Defining a 26 | subclass of a class defined by the Library is deemed a mode of using an 27 | interface provided by the Library. 28 | 29 | A “Combined Work” is a work produced by combining or linking an 30 | Application with the Library. The particular version of the Library with 31 | which the Combined Work was made is also called the “Linked Version”. 32 | 33 | The “Minimal Corresponding Source” for a Combined Work means the 34 | Corresponding Source for the Combined Work, excluding any source code 35 | for portions of the Combined Work that, considered in isolation, are 36 | based on the Application, and not on the Linked Version. 37 | 38 | The “Corresponding Application Code” for a Combined Work means the 39 | object code and/or source code for the Application, including any data 40 | and utility programs needed for reproducing the Combined Work from the 41 | Application, but excluding the System Libraries of the Combined Work. 42 | 43 | **1. Exception to Section 3 of the GNU GPL.** 44 | 45 | You may convey a covered work under sections 3 and 4 of this License 46 | without being bound by section 3 of the GNU GPL. 47 | 48 | **2. Conveying Modified Versions.** 49 | 50 | If you modify a copy of the Library, and, in your modifications, a 51 | facility refers to a function or data to be supplied by an Application 52 | that uses the facility (other than as an argument passed when the 53 | facility is invoked), then you may convey a copy of the modified 54 | version: 55 | 56 | a. under this License, provided that you make a good faith effort to 57 | ensure that, in the event an Application does not supply the 58 | function or data, the facility still operates, and performs whatever 59 | part of its purpose remains meaningful, or 60 | 61 | b. under the GNU GPL, with none of the additional permissions of this 62 | License applicable to that copy. 63 | 64 | **3. Object Code Incorporating Material from Library Header Files.** 65 | 66 | The object code form of an Application may incorporate material from a 67 | header file that is part of the Library. You may convey such object code 68 | under terms of your choice, provided that, if the incorporated material 69 | is not limited to numerical parameters, data structure layouts and 70 | accessors, or small macros, inline functions and templates (ten or fewer 71 | lines in length), you do both of the following: 72 | 73 | a. Give prominent notice with each copy of the object code that the 74 | Library is used in it and that the Library and its use are covered 75 | by this License. 76 | 77 | b. Accompany the object code with a copy of the GNU GPL and this 78 | license document. 79 | 80 | **4. Combined Works.** 81 | 82 | You may convey a Combined Work under terms of your choice that, taken 83 | together, effectively do not restrict modification of the portions of 84 | the Library contained in the Combined Work and reverse engineering for 85 | debugging such modifications, if you also do each of the following: 86 | 87 | a. Give prominent notice with each copy of the Combined Work that the 88 | Library is used in it and that the Library and its use are covered 89 | by this License. 90 | 91 | b. Accompany the Combined Work with a copy of the GNU GPL and this 92 | license document. 93 | 94 | c. For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among these 96 | notices, as well as a reference directing the user to the copies of 97 | the GNU GPL and this license document. 98 | 99 | d. Do one of the following: 100 | 101 | 1. Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to recombine 104 | or relink the Application with a modified version of the Linked 105 | Version to produce a modified Combined Work, in the manner 106 | specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 2. Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time a 111 | copy of the Library already present on the user’s computer 112 | system, and (b) will operate properly with a modified version of 113 | the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e. Provide Installation Information, but only if you would otherwise be 117 | required to provide such information under section 6 of the GNU GPL, 118 | and only to the extent that such information is necessary to install 119 | and execute a modified version of the Combined Work produced by 120 | recombining or relinking the Application with a modified version of 121 | the Linked Version. (If you use option 4d0, the Installation 122 | Information must accompany the Minimal Corresponding Source and 123 | Corresponding Application Code. If you use option 4d1, you must 124 | provide the Installation Information in the manner specified by 125 | section 6 of the GNU GPL for conveying Corresponding Source.) 126 | 127 | **5. Combined Libraries.** 128 | 129 | You may place library facilities that are a work based on the Library 130 | side by side in a single library together with other library facilities 131 | that are not Applications and are not covered by this License, and 132 | convey such a combined library under terms of your choice, if you do 133 | both of the following: 134 | 135 | a. Accompany the combined library with a copy of the same work based on 136 | the Library, uncombined with any other library facilities, conveyed 137 | under the terms of this License. 138 | 139 | b. Give prominent notice with the combined library that part of it is a 140 | work based on the Library, and explaining where to find the 141 | accompanying uncombined form of the same work. 142 | 143 | **6. Revised Versions of the GNU Lesser General Public License.** 144 | 145 | The Free Software Foundation may publish revised and/or new versions of 146 | the GNU Lesser General Public License from time to time. Such new 147 | versions will be similar in spirit to the present version, but may 148 | differ in detail to address new problems or concerns. 149 | 150 | Each version is given a distinguishing version number. If the Library as 151 | you received it specifies that a certain numbered version of the GNU 152 | Lesser General Public License “or any later version” applies to it, you 153 | have the option of following the terms and conditions either of that 154 | published version or of any later version published by the Free Software 155 | Foundation. If the Library as you received it does not specify a version 156 | number of the GNU Lesser General Public License, you may choose any 157 | version of the GNU Lesser General Public License ever published by the 158 | Free Software Foundation. 159 | 160 | If the Library as you received it specifies that a proxy can decide 161 | whether future versions of the GNU Lesser General Public License shall 162 | apply, that proxy’s public statement of acceptance of any version is 163 | permanent authorization for you to choose that version for the Library. 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Travis CI status AppVeyor CI status Pulp 2 | 3 | [![Join the chat at https://gitter.im/bodil/pulp](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/bodil/pulp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | A build tool for PureScript. 6 | 7 | ![Jarvis Cocker dancing](http://24.media.tumblr.com/77b76c557515a801a7e99ca5507b6548/tumblr_n5cx52oT831r4ba6to1_400.gif) 8 | 9 | 10 | 11 | 12 | 13 | - [Installation](#installation) 14 | - [Getting Started with a Pulp Project](#getting-started-with-a-pulp-project) 15 | - [What if I need something a bit more complicated?](#what-if-i-need-something-a-bit-more-complicated) 16 | - [Pulp Commands](#pulp-commands) 17 | - [Global, Command Specific and Pass-Through Options](#global-command-specific-and-pass-through-options) 18 | - [Pass-Through Options](#pass-through-options) 19 | - [Building Projects](#building-projects) 20 | - [Making a JavaScript Bundle](#making-a-javascript-bundle) 21 | - [Running Your PureScript Project](#running-your-purescript-project) 22 | - [Running Test Suites](#running-test-suites) 23 | - [Running Commands Before and After an Action](#running-commands-before-and-after-an-action) 24 | - [CommonJS Aware Builds](#commonjs-aware-builds) 25 | - [Optimising Code Size](#optimising-code-size) 26 | - [Reimporting Browserified Bundles](#reimporting-browserified-bundles) 27 | - [Building Documentation](#building-documentation) 28 | - [Launching a REPL](#launching-a-repl) 29 | - [Launching a Development Server](#launching-a-development-server) 30 | - [A Quick Example](#a-quick-example) 31 | - [I Need More](#i-need-more) 32 | - [Dependency Management](#dependency-management) 33 | - [Dependency Management Cheat Sheet](#dependency-management-cheat-sheet) 34 | - [Installing Dependencies](#installing-dependencies) 35 | - [Housekeeping](#housekeeping) 36 | - [Releasing Packages](#releasing-packages) 37 | - [Publishing Packages](#publishing-packages) 38 | - [Development](#development) 39 | - [Licence](#licence) 40 | 41 | 42 | 43 | ## Installation 44 | 45 | Assuming you already have [Node](https://nodejs.org/en/download/) set 46 | up (and we recommend you also set up NPM to 47 | [keep your global packages in your home directory](https://github.com/sindresorhus/guides/blob/master/npm-global-without-sudo.md)), 48 | all you need to do to get a working PureScript environment is: 49 | 50 | ```sh 51 | $ npm install -g purescript pulp bower 52 | ``` 53 | 54 | This installs the PureScript compiler, the Pulp build tool, and the 55 | [Bower](http://bower.io/) package manager. 56 | 57 | *Aside: if you're familiar with the JavaScript ecosystem and you're wondering 58 | why PureScript uses Bower and not npm, you might be interested to read [Why the 59 | PureScript community uses Bower](http://harry.garrood.me/blog/purescript-why-bower/). 60 | Otherwise, please ignore this and read on.* 61 | 62 | ## Getting Started with a Pulp Project 63 | 64 | The short version: 65 | 66 | ```sh 67 | $ mkdir purescript-hello 68 | $ cd purescript-hello 69 | $ pulp init 70 | $ pulp run 71 | ``` 72 | 73 | The structure of your project folder, after running `pulp init`, will 74 | look like this: 75 | 76 | ``` 77 | purescript-hello 78 | - bower.json 79 | - src/ 80 | - test/ 81 | ``` 82 | 83 | `pulp` works by convention. It expects all projects to contain a manifest file 84 | for package management (usually `bower.json`, since package management in 85 | PureScript is usually handled by [Bower](http://bower.io/)). 86 | 87 | Your project source files go in the `src` folder. Your test files go in the 88 | `test` folder. Project dependencies will be installed under the Bower standard 89 | `bower_components` folder, and are expected to have the same basic `src`/`test` 90 | structure. That's all there is to a `pulp` project. 91 | 92 | We employ the `purescript-` prefix as a convention to identify PureScript 93 | projects when they're used as dependencies. You're welcome to call your project 94 | anything you like, but without the `purescript-` prefix it won't be picked up 95 | by `pulp` as a dependency. 96 | 97 | ### What if I need something a bit more complicated? 98 | 99 | If you want to change any of these defaults, you can—`pulp` offers a 100 | number of command line flags to alter its behaviour—but try to avoid using 101 | them unless you have a good reason to. 102 | 103 | If you get fed up with having to remember long `pulp` invocations, try 104 | [using `npm` as your build tool](http://substack.net/task_automation_with_npm_run). 105 | `pulp`'s numerous command line flags make it well suited for this. 106 | 107 | If that's still not enough, you might try using a more generic build tool, 108 | such as [webpack](https://webpack.github.io/) with 109 | [purs-loader](https://github.com/ethul/purs-loader), or 110 | [gulp](http://gulpjs.com) with 111 | [gulp-purescript](https://github.com/purescript-contrib/gulp-purescript). 112 | 113 | ## Pulp Commands 114 | 115 | To get a quick overview of the things `pulp` can do, you can ask it to 116 | give you a list of its available commands: 117 | 118 | ```sh 119 | $ pulp --help 120 | ``` 121 | 122 | This will print a list of `pulp`'s global command line options, and a 123 | list of commands it will accept. 124 | 125 | To see the available options for a specific command, you can invoke 126 | the command with the `--help` flag, like this: 127 | 128 | ```sh 129 | $ pulp build --help 130 | ``` 131 | 132 | This will give you an exhaustive list of ways you can modify the basic 133 | behaviour of the command. 134 | 135 | ### Global, Command Specific and Pass-Through Options 136 | 137 | Notice that there's a distinction between _global_ command line 138 | options and command specific options. Global options must appear 139 | _before_ the name of the command, and command specific options must 140 | appear _after_ it. 141 | 142 | Thus, if you want to run the `build` command in watch mode (where it 143 | will run the command once, then wait and re-run the command whenever 144 | you change a source file) you need to put the `--watch` flag _before_ 145 | the command itself, like so: 146 | 147 | ```sh 148 | $ pulp --watch build 149 | ``` 150 | 151 | On the other hand, if you want to tell the build command to produce 152 | optimised code (performing dead code elimination), using the command 153 | specific option `--optimise`, the flag needs to come _after_ the 154 | command name: 155 | 156 | ```sh 157 | $ pulp build --optimise 158 | ``` 159 | 160 | #### Pass-Through Options 161 | 162 | Finally, `pulp` commands sometimes allows you to pass flags through to 163 | the `purs` compiler. Any options appearing after `--` will be passed through to 164 | the compiler, or whichever process a `pulp` command spawns. For instance, if 165 | you want to tell `purs` to skip applying tail call optimisations, you would 166 | invoke `pulp build` like this: 167 | 168 | ```sh 169 | $ pulp build -- --no-tco 170 | ``` 171 | 172 | ## Building Projects 173 | 174 | At heart, `pulp` is just a frontend for the PureScript compiler, 175 | `purs`. Its basic function is to compile your project, which you can do 176 | by running `pulp build`. This will simply run `purs compile` with all your 177 | source files, leaving the compiled JavaScript files in the `output` 178 | folder. These files will all be CommonJS modules, which you can 179 | `require()` using anything which supports CommonJS, such as `node`. 180 | 181 | However, you will usually want to do more with your project than just 182 | compile your PureScript code into a jumble of CommonJS modules. `pulp` 183 | provides a number of commands and options for the most common use 184 | cases. 185 | 186 | ### Making a JavaScript Bundle 187 | 188 | `pulp build` can also call `purs bundle` for you, which is a compiler 189 | tool whose job it is to take the output from `purs compile`, remove the code 190 | which isn't actually being used by your program, and bundle it all up 191 | into a single compact JavaScript file. 192 | 193 | There are two command line options you can give `pulp build` to 194 | accomplish this, depending on where you want the resulting code. You 195 | can use the `--optimise` flag (or its shorthand alias, `-O`), which 196 | will send the bundled result to standard output, or you can use the 197 | `--to` (or `-t`) option, passing it a file name, and `pulp` will store 198 | the bundle in a file of that name. 199 | 200 | So, you can use either of these methods, which in this example will 201 | both have the same effect: 202 | 203 | ```sh 204 | $ pulp build --optimise > hello.js 205 | $ pulp build --to hello.js 206 | ``` 207 | 208 | Note that using both options (`pulp build --optimise --to hello.js`) 209 | is superfluous. The presence of `--to` implies the presence of 210 | `--optimise`. 211 | 212 | ### Running Your PureScript Project 213 | 214 | If you're developing a Node project using PureScript, you can tell 215 | `pulp` to run it after compiling using the `pulp run` command. This 216 | command will first run `pulp build` for you, if necessary, then launch 217 | your compiled code using `node`. If you have used any pass-through 218 | command line options, these will be passed to the `node` process. 219 | 220 | So, to run the hello world project you get from `pulp init`, you would 221 | simply: 222 | 223 | ```sh 224 | $ pulp run 225 | ``` 226 | 227 | If you want to pass command line arguments to your application, `pulp` 228 | lets you do that too: 229 | 230 | ```sh 231 | $ pulp run -- file1.txt file2.txt file3.txt 232 | ``` 233 | 234 | If you want to run your application using something other than `node`, 235 | `pulp` lets you do that too, with the `--runtime` option. For instance, 236 | if you've written an application which runs on PhantomJS, you might 237 | launch it like this: 238 | 239 | ```sh 240 | $ pulp run --runtime phantomjs 241 | ``` 242 | 243 | ### Running Test Suites 244 | 245 | `pulp` has a command `pulp test`, which works much like `pulp run`, 246 | except it will also compile the code you've placed in your `test` 247 | folder, and instead of running the `main` function in your `Main` 248 | module, it will use `Test.Main`. This module should be located in your 249 | `test` folder. 250 | 251 | `pulp` doesn't care what test framework you've chosen, as long as 252 | there's a `main` function in your `Test.Main` module to be run. If the 253 | process exits with a non-zero return code, that means your test suite 254 | failed, as far as `pulp` is concerned, and it will itself exit with an 255 | error. 256 | 257 | In short, to run your tests: 258 | 259 | ```sh 260 | $ pulp test 261 | ``` 262 | 263 | To continuously run your tests when you change the source code: 264 | 265 | ```sh 266 | $ pulp --watch test 267 | ``` 268 | 269 | ### Running Commands Before and After an Action 270 | 271 | It's sometimes useful to kick off a command before or after an action, 272 | particularly in combination with the `--watch` option above. To do 273 | this, you can use `--before`, or `--then` and `--else` for successful 274 | or failing actions respectively: 275 | 276 | ```sh 277 | $ pulp --watch --before clear build # Clears the screen before builds. 278 | $ pulp --watch --then 'say Done' build # On OS X, announces 'Done' after a successful build. 279 | $ pulp --watch --else 'say Failed' build # Announces 'Failed' if a build failed. 280 | 281 | # A more long-winded example combining the three: 282 | $ pulp --watch --before clear --then "say $(basename `pwd`) succeeded." --else 'say $(basename `pwd`) failed.' build 283 | ``` 284 | 285 | 286 | ### CommonJS Aware Builds 287 | 288 | Often, you'll want to go outside PureScript and leverage some of the 289 | enormous body of JavaScript code available on 290 | [NPM](https://www.npmjs.com/). This is such a common use case that 291 | `pulp` provides a command for it: `pulp browserify`. As the name 292 | suggests, this uses [Browserify](http://browserify.org/) to bundle up 293 | your PureScript code with Node style CommonJS dependencies. 294 | 295 | For instance, the majority of web UI libraries for PureScript these 296 | days depend on either 297 | [virtual-dom](https://github.com/Matt-Esch/virtual-dom) or 298 | [React](https://facebook.github.io/react/) as a CommonJS dependency. 299 | Here is how you would add React to your project and build a JS bundle 300 | with React included (assuming your PureScript code `require`s it): 301 | 302 | ```sh 303 | $ npm install react 304 | $ pulp browserify --to hello.js 305 | ``` 306 | 307 | Essentially, `pulp browserify --to` works exactly like `pulp build 308 | --to`, except it also resolves CommonJS dependencies and includes them 309 | in the bundle. The resulting JS file can now be loaded directly into 310 | the browser, and everything you need to run your application should be 311 | included. 312 | 313 | If you omit the `--to` option, the bundle is piped to standard output. 314 | This would thus have the same effect as the example above: 315 | 316 | ```sh 317 | $ pulp browserify > hello.js 318 | ``` 319 | 320 | #### Optimising Code Size 321 | 322 | `pulp browserify` will pull code in at the module level by default, so 323 | every file `require`d from your entry point will appear in the bundle. 324 | The PureScript compiler, as we know, is able to perform dead code 325 | elimination on your compiled PureScript code, and we can leverage this 326 | in `pulp browserify` using the `--optimise` flag. 327 | 328 | ```sh 329 | $ pulp browserify --optimise --to hello.js 330 | ``` 331 | 332 | Note that, unlike `pulp build`, `--to` doesn't automatically imply 333 | `--optimise`. In fact, if you omit `--optimise`, `pulp browserify` 334 | will not only omit the dead code elimination step, it will also run 335 | Browserify as an incremental build, which means it will run 336 | considerably faster. You should use `--optimise` only when you're 337 | building production code—when you're developing, you'll probably 338 | prefer the much faster compile times provided by Browserify's 339 | incremental mode. 340 | 341 | #### Reimporting Browserified Bundles 342 | 343 | While browserified bundles are intended to be consumed directly by 344 | browsers, you may sometimes prefer to access the bundle from some 345 | external code. While it's generally preferable to consume CommonJS 346 | modules directly, there are use cases where you might want to provide 347 | a single JS file ready to be `require`d by a consumer without needing 348 | to deal with installing and resolving dependencies. Browserify 349 | provides the `--standalone` mechanism for that, and `pulp browserify` 350 | supports it: 351 | 352 | ```sh 353 | $ pulp browserify --standalone myBundle --to myBundle.js 354 | ``` 355 | 356 | This makes a bundle which comes wrapped in a UMD header (meaning it 357 | supports both CommonJS and AMD, and will install itself in the global 358 | namespace under the name you provided if neither is present), and the 359 | exports it provides will be the same as those you export in your 360 | `Main` module. 361 | 362 | So, given the example above produces a bundle where a PureScript 363 | function `Main.main` exists, you can access it from JavaScript via 364 | CommonJS like this: 365 | 366 | ```javascript 367 | var myBundle = require("./myBundle"); 368 | myBundle.main(); 369 | ``` 370 | 371 | ### Building Documentation 372 | 373 | PureScript has an inline syntax for documentation, which can be 374 | extracted into Markdown or HTML files using the `purs docs` command. `pulp` 375 | provides the `pulp docs` command to make this process easy: 376 | 377 | ```sh 378 | $ pulp docs [--with-dependencies] 379 | ``` 380 | 381 | This extracts the documentation from your source files, and places it 382 | in the `generated-docs` folder under your project's root folder. By 383 | default, dependencies are not included, but this can be enabled 384 | with the `--with-dependencies` flag. 385 | 386 | You can also extract documentation from your tests, if you like: 387 | 388 | ```sh 389 | $ pulp docs --with-tests 390 | ``` 391 | 392 | The `purs docs` command itself also accepts some options to modify its 393 | behaviour, which can be specified by using pass-through options. The `--format` 394 | option is particularly useful, as it allows you to specify the desired output 395 | format. In particular, you can generate nice hyperlinked Pursuit-style HTML 396 | docs with the following command: 397 | 398 | ```sh 399 | $ pulp docs -- --format html 400 | ``` 401 | 402 | It is a good idea to run this command and browse the generated HTML 403 | documentation before publishing a library to Pursuit, as doing so will allow 404 | you to spot any formatting issues or any declarations which are missing 405 | documentation. 406 | 407 | ### Launching a REPL 408 | 409 | The `purs repl` interactive shell for PureScript is fantastically useful, 410 | but setting it up can be a bit of a chore, especially with a large 411 | number of dependencies. That's where `pulp repl` comes in. 412 | 413 | `pulp repl` will generate a `.purs-repl` file for your project 414 | automatically whenever you invoke it, and launch `purs repl` for you 415 | directly. It's as simple as: 416 | 417 | ```sh 418 | $ pulp repl 419 | ``` 420 | 421 | ### Launching a Development Server 422 | 423 | A common need when developing client side web apps is a tightly integrated 424 | development web server, which takes care of compilation for you on the fly. 425 | This is what `pulp server` is for: whenever you make a change to your source 426 | files, you just switch to your browser and hit the refresh button, and the 427 | server will compile and deliver your assets on the fly. No need to wait for the 428 | PureScript compiler to finish before switching to the browser. 429 | 430 | `pulp server` only provides the most basic functionality: it will serve static 431 | assets from your project root, and it will serve your compiled JS bundle from 432 | `/app.js`. 433 | 434 | #### A Quick Example 435 | 436 | To see how this works, let's set up a project for serving the default 437 | hello world app through `pulp server`. 438 | 439 | ```sh 440 | $ mkdir hello-server 441 | $ cd hello-server 442 | $ pulp init 443 | ``` 444 | 445 | We need an `index.html` file to load our compiled PureScript code. 446 | Place this in your new `hello-server` folder: 447 | 448 | ```html 449 | 450 | 451 | 452 |

Hello sailor!

453 | 454 | 455 | 456 | ``` 457 | 458 | Now, start the server: 459 | 460 | ```sh 461 | $ pulp server 462 | ``` 463 | 464 | It will tell you that it's launched a web server at 465 | [http://localhost:1337/](http://localhost:1337/), and after a little while it 466 | will tell you that it's finished compiling: 467 | 468 | ``` 469 | * Server listening on http://localhost:1337/ 470 | * Building project in /home/harry/code/hello-serve 471 | Compiling Data.Symbol 472 | Compiling Type.Data.RowList 473 | Compiling Record.Unsafe 474 | 475 | * Build successful. 476 | * Bundling JavaScript... 477 | * Bundled. 478 | ``` 479 | 480 | If you browse to 481 | [http://localhost:1337/](http://localhost:1337/), you should, in 482 | addition to the "Hello sailor!" header on the webpage, see that your 483 | PureScript code has printed the text "Hello sailor!" to the console. 484 | 485 | #### I Need More 486 | 487 | As mentioned, this is a very bare bones development server, since `pulp server` 488 | is intended as a starting point only. You're likely to quickly need more 489 | features if you plan on doing any kind of serious web development. At this 490 | point, you'll need to look further afield; one option is to use 491 | [Webpack](https://webpack.github.io/) together with 492 | [purs-loader](https://github.com/ethul/purs-loader). 493 | 494 | ## Dependency Management 495 | 496 | `pulp` is not a package manager, only a build tool. The PureScript community 497 | has standardised on [Bower](http://bower.io/) as the default package manager, 498 | but there are alternatives such as 499 | [psc-package](https://github.com/purescript/psc-package). Currently, `pulp` 500 | supports both Bower and psc-package. 501 | 502 | Pulp expects the presence of a project manifest file in your project root, in 503 | which your project’s dependencies and other metadata are recorded. If you're 504 | using Bower, that file will be `bower.json`; if you're using psc-package, it 505 | will be `psc-package.json`. 506 | 507 | When you run commands like `pulp build`, Pulp will locate PureScript source 508 | files from installed dependencies based on which of these two files it finds in 509 | your project, and pass these files on to the relevant program (e.g. `purs 510 | compile`). If your project has both `bower.json` and `psc-package.json` files, 511 | Pulp uses the dependencies installed via Bower by default; if you want to use 512 | dependencies installed via psc-package, you can use the `--psc-package` flag, 513 | e.g. 514 | 515 | ```sh 516 | $ pulp --psc-package build 517 | ``` 518 | 519 | You can also run `pulp --psc-package init` to initialize a project with a 520 | `psc-package.json` file instead of a `bower.json` file. 521 | 522 | ### Dependency Management Cheat Sheet 523 | 524 | This document isn't going to explain how Bower works, or go into 525 | details about PureScript dependency management. However, a tl;dr is 526 | often enough to get you started and productive without having to dive 527 | into yet another package management system. It's going to be 528 | especially easy if you're already used to `npm`. So, here we go. 529 | 530 | #### Installing Dependencies 531 | 532 | To install the `purescript-profunctor` package into your project: 533 | 534 | ```sh 535 | $ bower install purescript-profunctor 536 | ``` 537 | 538 | To also record this as a dependency in the `bower.json` file: 539 | 540 | ```sh 541 | $ bower install --save purescript-profunctor 542 | ``` 543 | 544 | To install every dependency which has been recorded in `bower.json` as 545 | needed by your project: 546 | 547 | ```sh 548 | $ bower install 549 | ``` 550 | 551 | #### Housekeeping 552 | 553 | To remove an installed package: 554 | 555 | ```sh 556 | $ bower uninstall purescript-profunctor 557 | ``` 558 | 559 | To remove it from `bower.json` as well: 560 | 561 | ```sh 562 | $ bower uninstall --save purescript-profunctor 563 | ``` 564 | 565 | To list all packages installed in your project: 566 | 567 | ```sh 568 | $ bower ls 569 | ``` 570 | 571 | To update all installed packages to the most recent version allowed by 572 | `bower.json`: 573 | 574 | ```sh 575 | $ bower update 576 | ``` 577 | 578 | ### Releasing Packages 579 | 580 | Imagine you've created a new PureScript library for working with 581 | zygohistomorphic prepromorphisms (because who doesn't need zygohistomorphic 582 | prepromorphisms), called `purescript-zygo`. 583 | 584 | `pulp init` will have installed a basic `bower.json` file for you along with 585 | the project skeleton, but before you continue, you should read the [Bower 586 | documentation on the file 587 | format](https://github.com/bower/spec/blob/master/json.md) and make sure you’ve 588 | configured it to your satisfaction before you publish your package. In 589 | particular, mind that you’ve added a `license` field. 590 | 591 | Note that there is a convention of prefixing PureScript package names with 592 | `purescript-`. Please stick with that unless you have an especially good reason 593 | not to, as `pulp` and many other tools expect installed dependencies to follow 594 | this convention. 595 | 596 | You would start by tagging an initial version: 597 | 598 | ```sh 599 | $ cd /path/to/purescript-zygo 600 | $ pulp version 0.1.0 601 | ``` 602 | 603 | This runs a few checks to ensure that your package is properly set up for 604 | publishing, and if they pass, creates a Git tag `v0.1.0`. 605 | 606 | #### Publishing Packages 607 | 608 | Bower packages are installed directly from Git repositories, and versioning 609 | follows Git tags. This means that once you've tagged a version, all you need to 610 | do to make a new release is push that tag to GitHub, register your package 611 | and upload your package's documentation to Pursuit. 612 | 613 | Originally, `pulp` was designed to work exclusively with the Bower registry but 614 | things became complicated after it no longer accepted new PureScript 615 | package submissions. Older packages are still registered in Bower but new packages need to be 616 | registered in the [PureScript Registry](https://github.com/purescript/registry). 617 | The upshot is that you will usually use a `spago` workflow to 618 | prepare the ground for publication and then use `pulp` for the actual publication step itself. 619 | For this reason you should read [spago: Publish my library](https://github.com/purescript/spago#publish-my-library) before proceding. You may also find it useful to read the notes on 620 | [How to submit packages](https://pursuit.purescript.org/help/authors#submitting-packages) in the 621 | Pursuit package authors guide. 622 | 623 | The `pulp` publication commands are: 624 | 625 | ```sh 626 | $ pulp login 627 | ``` 628 | followed by 629 | 630 | ```sh 631 | $ pulp publish 632 | ``` 633 | 634 | For subsequent releases, the process is the same: `pulp version ` 635 | followed by `pulp publish`. When tagging a new version, `pulp version` also 636 | allows you to supply an argument of the form `patch`, `minor`, or `major`, in 637 | addition to specific versions. If you run `pulp version patch`, for example, 638 | Pulp will look through your Git tags to find the version number for the latest 639 | release, and then generate the new verision number by bumping the patch 640 | component. The `minor` and `major` arguments respectively perform minor and 641 | major version bumps in the same way. 642 | 643 | Pulp does not currently support publishing packages which use psc-package 644 | exclusively, because without having submitted your package to a registry such 645 | as the Bower registry, there is no way of making sure that people agree which 646 | package a given package name refers to. This may change in the future. 647 | 648 | ## Development 649 | 650 | To work on `pulp`, after cloning the repository, run: 651 | 652 | ``` 653 | $ npm install 654 | $ bower install 655 | ``` 656 | 657 | to install dependencies. Then, you can run 658 | 659 | ``` 660 | $ npm run -s build 661 | ``` 662 | 663 | to compile `pulp`, and 664 | 665 | ``` 666 | $ npm test 667 | ``` 668 | 669 | to run the tests. 670 | 671 | ## Licence 672 | 673 | Copyright 2014-2017 Bodil Stokke, Harry Garrood 674 | 675 | This program is free software: you can redistribute it and/or modify 676 | it under the terms of the GNU Lesser General Public License as 677 | published by the Free Software Foundation, either version 3 of the 678 | License, or (at your option) any later version. 679 | 680 | See the [LICENSE](LICENSE.md) file for further details. 681 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulp", 3 | "moduleType": [ 4 | "node" 5 | ], 6 | "ignore": [ 7 | "**/.*", 8 | "node_modules", 9 | "bower_components", 10 | "output" 11 | ], 12 | "dependencies": { 13 | "purescript-ansi": "^6.1.0", 14 | "purescript-avar": "^4.0.0", 15 | "purescript-effect": "^3.0.0", 16 | "purescript-foreign-generic": "^11.0.0", 17 | "purescript-foreign-object": "^3.0.0", 18 | "purescript-node-child-process": "^7.0.0", 19 | "purescript-node-fs-aff": "^7.0.0", 20 | "purescript-node-http": "^6.0.0", 21 | "purescript-node-process": "^8.1.0", 22 | "purescript-now": "^5.0.0", 23 | "purescript-ordered-collections": "^2.0.0", 24 | "purescript-prelude": "^5.0.0", 25 | "purescript-simple-json": "^8.0.0", 26 | "purescript-versions": "^6.0.0", 27 | "purescript-argonaut-codecs": "^8.1.0", 28 | "purescript-argonaut": "^8.0.0" 29 | }, 30 | "devDependencies": { 31 | "purescript-assert": "^5.0.0" 32 | } 33 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("./pulp"); 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pulp", 3 | "version": "16.0.2", 4 | "description": "A build system for PureScript projects", 5 | "keywords": [ 6 | "purescript", 7 | "make", 8 | "build", 9 | "cabal" 10 | ], 11 | "author": "Bodil Stokke", 12 | "contributors": [ 13 | "Harry Garrood" 14 | ], 15 | "license": "LGPL-3.0+", 16 | "homepage": "https://github.com/purescript-contrib/pulp", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/purescript-contrib/pulp.git" 20 | }, 21 | "bin": { 22 | "pulp": "index.js" 23 | }, 24 | "main": "pulp.js", 25 | "files": [ 26 | "index.js", 27 | "pulp.js", 28 | "src", 29 | "bower.json", 30 | "test-js", 31 | ".jshintrc" 32 | ], 33 | "engines": { 34 | "node": ">= 4" 35 | }, 36 | "scripts": { 37 | "prepare": "bower install && npm run build", 38 | "test": "npm run test:unit && npm run test:integration", 39 | "build": "npm run lint && npm run compile && npm run bundle", 40 | "lint": "jshint src", 41 | "compile": "psa -c \"src/**/*.purs\" \"test/**/*.purs\" \"bower_components/purescript-*/src/**/*.purs\" --censor-lib --censor-codes=ImplicitImport,HidingImport", 42 | "bundle": "purs bundle \"output/*/*.js\" --output pulp.js --module Main --main Main", 43 | "test:unit": "purs bundle \"output/*/*.js\" --output unit-tests.js --module Test.Main --main Test.Main && node unit-tests.js", 44 | "test:integration": "mocha test-js --require babel/register" 45 | }, 46 | "dependencies": { 47 | "browserify": "^16.2.3", 48 | "browserify-incremental": "^3.1.1", 49 | "concat-stream": "^2.0.0", 50 | "gaze": "^1.1.3", 51 | "glob": "^7.1.3", 52 | "mold-source-map": "^0.4.0", 53 | "node-static": "^0.7.11", 54 | "read": "^1.0.7", 55 | "sorcery": "^0.10.0", 56 | "temp": "^0.9.0", 57 | "through": "^2.3.8", 58 | "tree-kill": "^1.2.1", 59 | "which": "^1.3.1", 60 | "wordwrap": "1.0.0" 61 | }, 62 | "devDependencies": { 63 | "babel": "^5.8.22", 64 | "bower": "^1.8.4", 65 | "chai": "^4.2.0", 66 | "co": "^4.6.0", 67 | "fs-promise": "^2.0.3", 68 | "jshint": "^2.9.7", 69 | "mkdirp": "^0.5.1", 70 | "mocha": "^5.2.0", 71 | "psc-package": "^4.0.1", 72 | "purescript-psa": "^0.7.3", 73 | "semver": "^5.6.0", 74 | "touch": "^3.1.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Main.js: -------------------------------------------------------------------------------- 1 | // module Main 2 | 3 | "use strict"; 4 | 5 | exports.logStack = function logStack(err) { 6 | return function() { 7 | console.log(err.stack); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/Main.purs: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Except (runExcept) 6 | import Data.Array (head, drop) 7 | import Data.Either (Either(..), either) 8 | import Data.Foldable (elem) 9 | import Data.List (List(Nil)) 10 | import Data.Map (insert) 11 | import Data.Maybe (Maybe(..)) 12 | import Data.Version (Version, showVersion, version) 13 | import Effect (Effect) 14 | import Effect.Aff (Aff, attempt, runAff, throwError) 15 | import Effect.Class (liftEffect) 16 | import Effect.Console as Console 17 | import Effect.Exception (Error, catchException, error, message, throwException) 18 | import Effect.Unsafe (unsafePerformEffect) 19 | import Foreign (Foreign, unsafeToForeign, readString) 20 | import Foreign.Index (readProp) 21 | import Foreign.JSON (parseJSON) 22 | import Node.Encoding (Encoding(UTF8)) 23 | import Node.FS.Sync (readTextFile) 24 | import Node.Path as Path 25 | import Node.Process as Process 26 | import Pulp.Args as Args 27 | import Pulp.Args.Get (getFlag, getOption) 28 | import Pulp.Args.Help (printCommandHelp, printHelp) 29 | import Pulp.Args.Parser (parse) 30 | import Pulp.Args.Types as Type 31 | import Pulp.Browserify as Browserify 32 | import Pulp.Build as Build 33 | import Pulp.BumpVersion as BumpVersion 34 | import Pulp.Docs as Docs 35 | import Pulp.Init as Init 36 | import Pulp.Login as Login 37 | import Pulp.Outputter (getOutputter, makeOutputter) 38 | import Pulp.Project (getProject) 39 | import Pulp.Publish as Publish 40 | import Pulp.Repl as Repl 41 | import Pulp.Run as Run 42 | import Pulp.Server as Server 43 | import Pulp.Shell as Shell 44 | import Pulp.System.FFI (unsafeInspect) 45 | import Pulp.Test as Test 46 | import Pulp.Validate (getNodeVersion, validate) 47 | import Pulp.Version (printVersion) 48 | import Pulp.Watch as Watch 49 | import Text.Parsing.Parser (parseErrorMessage) 50 | 51 | globals :: Array Args.Option 52 | globals = [ 53 | Args.option "bowerFile" ["--bower-file", "-b"] Type.file 54 | "Read this bower.json file instead of autodetecting it.", 55 | Args.option "pscPackage" ["--psc-package"] Type.flag 56 | "Use psc-package for package management.", 57 | Args.option "debug" ["--debug"] Type.flag 58 | "Enable debug logging", 59 | Args.option "watch" ["--watch", "-w"] Type.flag 60 | "Watch source directories and re-run command if something changes.", 61 | Args.option "monochrome" ["--monochrome"] Type.flag 62 | "Don't colourise log output.", 63 | Args.option "before" ["--before"] Type.string 64 | "Run a shell command before the operation begins. Useful with `--watch`, eg. `--watch --before clear`.", 65 | Args.option "then" ["--then"] Type.string 66 | "Run a shell command after the operation finishes successfully. Useful with `--watch`, eg. `--watch --then 'say Done'`", 67 | Args.option "else" ["--else"] Type.string 68 | "Run a shell command if an operation failed. Useful with `--watch`, eg. `--watch --then 'say Done' --else 'say Failed'`", 69 | Args.option "version" ["--version", "-v"] Type.flag 70 | "Show current pulp version." 71 | ] 72 | 73 | defaultDependencyPath :: String 74 | defaultDependencyPath = 75 | unsafePerformEffect (catchException (const (pure "bower_components")) readFromBowerRc) 76 | where 77 | readFromBowerRc = do 78 | json <- readTextFile UTF8 ".bowerrc" 79 | case runExcept (parseJSON json >>= readProp "directory" >>= readString) of 80 | Right dir -> pure dir 81 | Left err -> throwException (error (show err)) 82 | 83 | dependencyPathOption :: Args.Option 84 | dependencyPathOption = 85 | Args.optionDefault "dependencyPath" ["--dependency-path"] Type.directory 86 | "Directory for PureScript dependency files." defaultDependencyPath 87 | 88 | -- | Options for any command requiring paths 89 | pathArgs :: Array Args.Option 90 | pathArgs = [ 91 | Args.optionDefault "includePaths" ["--include", "-I"] Type.directories 92 | ("Additional directories for PureScript source files, separated by `" <> Path.delimiter <> "`.") 93 | ([] :: Array String), 94 | Args.optionDefault "srcPath" ["--src-path"] Type.directory 95 | "Directory for PureScript source files." "src", 96 | Args.optionDefault "testPath" ["--test-path"] Type.directory 97 | "Directory for PureScript test files." "test", 98 | dependencyPathOption 99 | ] 100 | 101 | buildPath :: Args.Option 102 | buildPath = 103 | Args.optionDefault "buildPath" ["--build-path", "-o"] Type.string 104 | "Path for compiler output." "./output" 105 | 106 | -- | Options common to 'build', 'test', and 'browserify' 107 | buildishArgs :: Array Args.Option 108 | buildishArgs = [ 109 | buildPath, 110 | Args.option "noPsa" ["--no-psa"] Type.flag 111 | "Do not attempt to use the psa frontend instead of purs compile" 112 | ] <> pathArgs 113 | 114 | runArgs :: Array Args.Option 115 | runArgs = [ 116 | Args.optionDefault "main" ["--main", "-m"] Type.string 117 | "Module to be used as the application's entry point." "Main", 118 | Args.option "jobs" ["--jobs", "-j"] Type.int 119 | "Tell purs to use the specified number of cores." 120 | ] <> buildishArgs 121 | 122 | buildArgs :: Array Args.Option 123 | buildArgs = [ 124 | Args.option "to" ["--to", "-t"] Type.string 125 | "Output file name (stdout if not specified).", 126 | Args.option "optimise" ["--optimise", "-O"] Type.flag 127 | "Perform dead code elimination.", 128 | Args.option "skipEntryPoint" ["--skip-entry-point"] Type.flag 129 | "Don't add code to automatically invoke Main.", 130 | Args.option "sourceMaps" ["--source-maps"] Type.flag 131 | "Generate source maps" 132 | ] <> runArgs 133 | 134 | -- TODO: This is possibly just a temporary separation from buildArgs; at the 135 | -- moment, the browserify action doesn't support this option, but it's 136 | -- definitely in the realm of possibility. 137 | moduleArgs :: Array Args.Option 138 | moduleArgs = [ 139 | Args.option "modules" ["--modules"] Type.string 140 | "Additional modules to be included in the output bundle (comma-separated list)." 141 | ] 142 | 143 | remainderToPurs :: Maybe String 144 | remainderToPurs = Just "Passthrough options are sent to `purs compile`." 145 | 146 | remainderToTest :: Maybe String 147 | remainderToTest = 148 | Just ("Passthrough options are sent to the test program. " <> 149 | "This can be useful for only running one particular test, for instance.") 150 | 151 | remainderToBundle :: Maybe String 152 | remainderToBundle = 153 | Just "Passthrough options are sent to `purs bundle`." 154 | 155 | remainderToProgram :: Maybe String 156 | remainderToProgram = 157 | Just "Passthrough options are sent to your program." 158 | 159 | remainderToDocs :: Maybe String 160 | remainderToDocs = 161 | Just "Passthrough options are sent to `purs docs`." 162 | 163 | remainderToRepl :: Maybe String 164 | remainderToRepl = 165 | Just "Passthrough options are sent to `purs repl`." 166 | 167 | commands :: Array Args.Command 168 | commands = [ 169 | Args.command "init" "Generate an example PureScript project." Nothing Init.action [ 170 | Args.option "force" ["--force"] Type.flag 171 | "Overwrite any project found in the current directory.", 172 | Args.option "withEff" ["--with-eff"] Type.flag 173 | "Generate project using Eff, regardless of the detected compiler version.", 174 | Args.option "withEffect" ["--with-effect"] Type.flag 175 | "Generate project using Effect, regardless of the detected compiler version." 176 | ], 177 | Args.command "build" "Build the project." remainderToPurs Build.action $ 178 | buildArgs <> moduleArgs, 179 | Args.command "test" "Run project tests." remainderToTest Test.action $ [ 180 | Args.optionDefault "main" ["--main", "-m"] Type.string 181 | "Test entry point." "Test.Main", 182 | Args.optionDefault "runtime" ["--runtime", "-r"] Type.string 183 | "Run test script using this command instead of Node." "node" 184 | ] <> buildishArgs, 185 | Args.command "browserify" 186 | "Produce a deployable bundle using Browserify." remainderToBundle Browserify.action $ [ 187 | Args.option "transform" ["--transform"] Type.string 188 | "Apply a Browserify transform.", 189 | Args.option "force" ["--force"] Type.flag 190 | "Force a non-incremental build by deleting the build cache.", 191 | Args.option "standalone" ["--standalone"] Type.string 192 | "Output a UMD bundle with the given external module name.", 193 | Args.option "skipCompile" ["--skip-compile"] Type.flag 194 | "Assume PureScript code has already been compiled. Useful for when you want to pass options to purs." 195 | ] <> buildArgs, 196 | Args.command "run" "Compile and run the project." remainderToProgram Run.action $ [ 197 | Args.optionDefault "runtime" ["--runtime", "-r"] Type.string 198 | "Run the program using this command instead of Node." "node" 199 | ] <> runArgs, 200 | Args.command "docs" "Generate project documentation." remainderToDocs Docs.action $ [ 201 | buildPath, 202 | Args.option "withTests" ["--with-tests", "-t"] Type.flag 203 | "Include tests." 204 | ] <> pathArgs, 205 | Args.commandWithAlias "repl" 206 | "Launch a PureScript REPL configured for the project." remainderToRepl 207 | Repl.action pathArgs 208 | ["psci"], 209 | Args.command "server" "Launch a development server." Nothing Server.action $ [ 210 | Args.optionDefault "main" ["--main", "-m"] Type.string 211 | "Application's entry point." "Main", 212 | Args.optionDefault "port" ["--port", "-p"] Type.int 213 | "Port number to listen on." 1337, 214 | Args.optionDefault "host" ["--host"] Type.string 215 | "IP address to bind the server to." "localhost", 216 | Args.option "quiet" ["--quiet", "-q"] Type.flag 217 | "Display nothing to the console when rebuilding." 218 | ] <> buildishArgs, 219 | Args.command "login" "Obtain and store a token for uploading packages to Pursuit." Nothing Login.action [], 220 | Args.commandWithArgs "version" "Bump and tag a new version in preparation for release." Nothing BumpVersion.action [ dependencyPathOption ] 221 | [ Args.argument "bump" Type.versionBump "How to bump the version. Acceptable values: 'major', 'minor', 'patch', or any specific version. If omitted, Pulp will prompt you for a version." false 222 | ], 223 | Args.command "publish" "Publish a previously tagged version to Bower and Pursuit." Nothing Publish.action [ 224 | Args.optionDefault "pushTo" ["--push-to"] Type.string 225 | "The Git remote to push commits and tags to." "origin", 226 | Args.option "noPush" ["--no-push"] Type.flag 227 | "Skip pushing commits or tags to any remote.", 228 | dependencyPathOption 229 | ] 230 | ] 231 | 232 | failed :: forall a. Error -> Effect a 233 | failed err = do 234 | Console.error $ "* ERROR: " <> message err 235 | -- logStack err 236 | Process.exit 1 237 | 238 | foreign import logStack :: Error -> Effect Unit 239 | 240 | succeeded :: Unit -> Effect Unit 241 | succeeded = const (pure unit) 242 | 243 | main :: Effect Unit 244 | main = void $ runAff (either failed succeeded) do 245 | requireNodeAtLeast (version 12 0 0 Nil Nil) 246 | argv <- drop 2 <$> liftEffect Process.argv 247 | args <- parse globals commands argv 248 | case args of 249 | Left err -> 250 | handleParseError (head argv) (parseErrorMessage err) 251 | Right (Left (Args.Help command)) -> 252 | printCommandHelp out globals command 253 | Right (Right args') -> 254 | runWithArgs args' 255 | where 256 | handleParseError (Just x) _ 257 | -- TODO: this is kind of gross, especially that --version and --help are 258 | -- repeated 259 | | x `elem` ["--version", "-v"] = printVersion 260 | | x `elem` ["--help", "-h"] = printHelp out globals commands 261 | 262 | handleParseError _ err = do 263 | out.err $ "Error: " <> err 264 | printHelp out globals commands 265 | liftEffect $ Process.exit 1 266 | 267 | out = makeOutputter false false 268 | 269 | runWithArgs :: Args.Args -> Aff Unit 270 | runWithArgs args = do 271 | out <- getOutputter args 272 | _ <- validate out 273 | watch <- getFlag "watch" args.globalOpts 274 | args' <- addProject args 275 | if watch && args.command.name /= "server" 276 | then 277 | Args.runAction Watch.action args' 278 | else do 279 | runShellForOption "before" args'.globalOpts out 280 | result <- attempt $ Args.runAction args.command.action args' 281 | case result of 282 | Left e -> do 283 | runShellForOption "else" args'.globalOpts out 284 | liftEffect $ throwException e 285 | Right _ -> 286 | runShellForOption "then" args'.globalOpts out 287 | where 288 | noProject = ["init", "login"] 289 | 290 | -- This is really quite gross, especially with _project. Not sure exactly 291 | -- how to go about improving this. 292 | addProject as = 293 | if as.command.name `elem` noProject 294 | then pure as 295 | else do 296 | proj <- getProject as.globalOpts 297 | let globalOpts' = insert "_project" (Just (unsafeToForeign proj)) as.globalOpts 298 | pure $ as { globalOpts = globalOpts' } 299 | 300 | runShellForOption option opts out = do 301 | triggerCommand <- getOption option opts 302 | case triggerCommand of 303 | Just cmd -> Shell.shell out cmd 304 | Nothing -> pure unit 305 | 306 | requireNodeAtLeast :: Version -> Aff Unit 307 | requireNodeAtLeast minimum = do 308 | actual <- getNodeVersion 309 | when (actual < minimum) do 310 | throwError $ error $ 311 | "Your node.js version is too old " <> 312 | "(required: " <> showVersion minimum <> 313 | ", actual: " <> showVersion actual <> ")" 314 | 315 | argsParserDiagnostics :: Args.Args -> Aff Unit 316 | argsParserDiagnostics opts = do 317 | let out = makeOutputter false true 318 | out.log $ "Globals: " <> show ((map <<< map) showForeign opts.globalOpts) 319 | out.log $ "Command: " <> opts.command.name 320 | out.log $ "Locals: " <> show ((map <<< map) showForeign opts.commandOpts) 321 | out.log $ "Remainder: " <> show opts.remainder 322 | where 323 | showForeign :: Foreign -> String 324 | showForeign = unsafeInspect 325 | -------------------------------------------------------------------------------- /src/Pulp/Args.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Args where 2 | 3 | import Prelude 4 | 5 | import Data.List (List) 6 | import Data.Map (Map) 7 | import Data.Maybe (Maybe(..)) 8 | import Effect.Aff (Aff) 9 | import Foreign (Foreign, unsafeToForeign) 10 | import Text.Parsing.Parser (ParserT) 11 | 12 | type Options = Map String (Maybe Foreign) 13 | 14 | -- | Action is a newtype because a normal type synonym would lead to a loop, 15 | -- | which is disallowed by the compiler. 16 | newtype Action = Action (Args -> Aff Unit) 17 | 18 | runAction :: Action -> Args -> Aff Unit 19 | runAction (Action f) = f 20 | 21 | type OptParser a = ParserT (List String) Aff a 22 | 23 | newtype Help = Help Command 24 | 25 | -- | We use Foreign for the result of the parser because we want to be able to 26 | -- | put any type in at first. Then, we can use other functions in Foreign 27 | -- | to get it out again, or throw an error if the types don't match. 28 | -- | 29 | -- | I expect there is a better way of doing this but this will do for now. 30 | -- | It's no less safe than the JS implementation, at least. 31 | type OptionParser = { 32 | name :: Maybe String, 33 | parser :: String -> OptParser (Maybe Foreign) 34 | } 35 | 36 | type ArgumentParser = String -> OptParser Foreign 37 | 38 | -- | A command line option, like `--output` or `--verbose`. 39 | type Option = { 40 | name :: String, 41 | match :: Array String, 42 | parser :: OptionParser, 43 | desc :: String, 44 | defaultValue :: Maybe Foreign 45 | } 46 | 47 | -- | A positional command line argument, like `major` in `pulp version 48 | -- | major`. 49 | type Argument = { 50 | name :: String, 51 | parser :: ArgumentParser, 52 | desc :: String, 53 | required :: Boolean 54 | } 55 | 56 | type Command = { 57 | name :: String, 58 | desc :: String, 59 | alias :: Array String, 60 | passthroughDesc :: Maybe String, 61 | options :: Array Option, 62 | arguments :: Array Argument, 63 | action :: Action 64 | } 65 | 66 | type Args = { 67 | globalOpts :: Options, 68 | commandOpts :: Options, 69 | commandArgs :: Options, -- TODO: this is a bit gross. 70 | command :: Command, 71 | remainder :: Array String 72 | } 73 | 74 | option :: String -> Array String -> OptionParser -> String -> Option 75 | option name match parser desc = { 76 | name, 77 | match, 78 | parser, 79 | desc, 80 | defaultValue: Nothing 81 | } 82 | 83 | optionDefault :: forall a. String -> Array String -> OptionParser -> String -> a -> Option 84 | optionDefault n m p d defaultValue = 85 | (option n m p d) { defaultValue = Just (unsafeToForeign defaultValue) } 86 | 87 | argument :: String -> ArgumentParser -> String -> Boolean -> Argument 88 | argument name parser desc required = { 89 | name, 90 | parser, 91 | desc, 92 | required 93 | } 94 | 95 | command :: String -> String -> Maybe String -> Action -> Array Option -> Command 96 | command name desc passthroughDesc action options = { 97 | name, 98 | desc, 99 | passthroughDesc, 100 | options, 101 | action, 102 | arguments: [], 103 | alias: [] 104 | } 105 | 106 | commandWithArgs :: String -> String -> Maybe String -> Action -> Array Option -> Array Argument -> Command 107 | commandWithArgs name desc passthroughDesc action options args = 108 | (command name desc passthroughDesc action options) { arguments = args } 109 | 110 | commandWithAlias :: String -> String -> Maybe String -> Action -> Array Option -> Array String -> Command 111 | commandWithAlias name desc passthroughDesc action options alias = 112 | (command name desc passthroughDesc action options) { alias = alias } 113 | -------------------------------------------------------------------------------- /src/Pulp/Args/Get.purs: -------------------------------------------------------------------------------- 1 | -- | Functions for getting data back out of an `Options` value. 2 | module Pulp.Args.Get 3 | ( getOption 4 | , getOption' 5 | , getFlag 6 | , hasOption 7 | ) where 8 | 9 | import Prelude 10 | 11 | import Control.Monad.Error.Class (throwError) 12 | import Control.Monad.Except (runExcept) 13 | import Data.Either (Either(..)) 14 | import Data.Map (lookup) 15 | import Data.Maybe (Maybe(..), isJust) 16 | import Data.String (joinWith) 17 | import Effect.Aff (Aff) 18 | import Effect.Exception (error) 19 | import Foreign (Foreign) 20 | import Foreign.Class (class Decode, decode) 21 | import Pulp.Args (Options) 22 | import Pulp.System.FFI (unsafeInspect) 23 | 24 | -- | Get an option out of the `Options` value. If the option has no default and 25 | -- | was not specified at the command line, the result will be `Nothing`. For 26 | -- | options which do have defaults, you probably want the primed version 27 | -- | instead, `getOption'`. 28 | getOption :: forall a. Decode a => String -> Options -> Aff (Maybe a) 29 | getOption name opts = do 30 | case lookup name opts of 31 | Just (Just thing) -> 32 | Just <$> readForeign name thing 33 | Just Nothing -> 34 | let msg = "Tried to read a flag as an option: " <> name 35 | in internalError msg 36 | Nothing -> 37 | pure Nothing 38 | 39 | -- | Get an option which was declared with a default value, and therefore 40 | -- | should always have a value. 41 | getOption' :: forall a. Decode a => String -> Options -> Aff a 42 | getOption' name opts = do 43 | mval <- getOption name opts 44 | case mval of 45 | Just val -> 46 | pure val 47 | Nothing -> 48 | let msg = "Missing default value for option: " <> name 49 | in internalError msg 50 | 51 | -- | Get a flag out of the `Options` value. If it was specified at the command 52 | -- | line, the result is `true`, otherwise, `false`. 53 | getFlag :: String -> Options -> Aff Boolean 54 | getFlag name opts = do 55 | case lookup name opts of 56 | Just (Just _) -> 57 | let msg = "Tried to read an option as a flag: " <> name 58 | in internalError msg 59 | Just Nothing -> 60 | pure true 61 | Nothing -> 62 | pure false 63 | 64 | -- | True if a given option exists in the `Options` map, false otherwise. 65 | hasOption :: String -> Options -> Aff Boolean 66 | hasOption name opts = isJust <$> opt 67 | where 68 | opt :: Aff (Maybe Foreign) 69 | opt = getOption name opts 70 | 71 | readForeign :: forall a. Decode a => String -> Foreign -> Aff a 72 | readForeign name thing = 73 | case runExcept (decode thing) of 74 | Left e -> 75 | internalError $ joinWith "\n" 76 | [ "Failed to read option: " <> name 77 | , "The value was: " <> unsafeInspect thing 78 | , "Foreign.read failed: " <> show e 79 | ] 80 | Right x -> 81 | pure x 82 | 83 | internalError :: forall b. String -> Aff b 84 | internalError msg = 85 | throwError (error 86 | ("Internal error in Pulp.Args.Get: " <> msg <> "\n" <> 87 | "This is a bug. Please report it.\n")) 88 | -------------------------------------------------------------------------------- /src/Pulp/Args/Help.js: -------------------------------------------------------------------------------- 1 | // module Pulp.Args.Help 2 | "use strict"; 3 | 4 | exports.pad = function pad(n) { 5 | return new Array(n + 1).join(" "); 6 | }; 7 | 8 | exports.wrap = function wrap(s) { 9 | return function(indent) { 10 | return function() { 11 | var cols = process.stdout.columns; 12 | return cols ? require("wordwrap")(indent, cols)(s).slice(indent) : s; 13 | }; 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/Pulp/Args/Help.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Args.Help 2 | ( printHelp 3 | , printCommandHelp 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Control.Monad.Except (runExcept) 9 | import Data.Array (sort, (!!), null) 10 | import Data.Either (Either(..)) 11 | import Data.Foldable (foldr, maximum) 12 | import Data.Maybe (Maybe(..), fromMaybe, maybe, fromJust) 13 | import Data.String as Str 14 | import Data.Traversable (sequence) 15 | import Effect (Effect) 16 | import Effect.Aff (Aff) 17 | import Effect.Class (liftEffect) 18 | import Foreign (F) 19 | import Foreign.Class (decode) 20 | import Foreign.Object (Object, keys, lookup, insert, empty) 21 | import Node.Path as Path 22 | import Node.Process as Process 23 | import Partial.Unsafe (unsafePartial) 24 | import Pulp.Args (Argument, Command, Option, option) 25 | import Pulp.Args.Types as Type 26 | import Pulp.Outputter (Outputter) 27 | 28 | foreign import pad :: Int -> String 29 | 30 | foreign import wrap :: String -> Int -> Effect String 31 | 32 | formatTable :: Object String -> Effect String 33 | formatTable table = 34 | let headers = sort $ keys table 35 | longest = fromMaybe 0 $ maximum $ headers <#> Str.length 36 | formatEntry key = unsafePartial $ fromJust (lookup key table) # \entry -> 37 | let padding = longest - Str.length key 38 | in do 39 | formatted <- wrap entry (longest + 4) 40 | pure (" " <> key <> pad (padding + 2) <> formatted <> "\n") 41 | in do 42 | entries <- sequence $ headers <#> formatEntry 43 | pure $ Str.joinWith "" entries 44 | 45 | describeOpt :: Option -> String 46 | describeOpt opt = opt.desc <> case opt.defaultValue of 47 | Nothing -> "" 48 | Just def -> maybe "" (\d -> " [Default: " <> d <> "]") (tryDefault def) 49 | where 50 | tryDefault def = 51 | case runExcept (decode def :: F String) of 52 | Right str -> 53 | Just (show str) 54 | Left _ -> 55 | case runExcept (decode def :: F Int) of 56 | Right int -> 57 | Just (show int) 58 | Left _ -> 59 | Nothing 60 | 61 | prepareOpts :: Array Option -> Object String 62 | prepareOpts = foldr foldOpts empty 63 | where formatKey n = (Str.joinWith " " n.match) <> case n.parser.name of 64 | Nothing -> "" 65 | Just arg -> " " <> arg 66 | foldOpts n = insert (formatKey n) (describeOpt n) 67 | 68 | formatOpts :: Array Option -> Aff String 69 | formatOpts = liftEffect <<< formatTable <<< prepareOpts 70 | 71 | prepareCmds :: Array Command -> Object String 72 | prepareCmds = foldr foldCmds empty 73 | where foldCmds n = insert n.name n.desc 74 | 75 | formatCmds :: Array Command -> Aff String 76 | formatCmds = liftEffect <<< formatTable <<< prepareCmds 77 | 78 | formatPassThrough :: Maybe String -> Aff String 79 | formatPassThrough mdesc = 80 | let desc = fromMaybe "Passthrough options are ignored." mdesc 81 | in liftEffect (wrap (" " <> desc) 2) 82 | 83 | prepareArguments :: Array Argument -> Object String 84 | prepareArguments = foldr foldOpts empty 85 | where formatKey arg = Str.toUpper arg.name 86 | foldOpts arg = insert (formatKey arg) arg.desc 87 | 88 | formatArguments :: Array Argument -> Aff String 89 | formatArguments = liftEffect <<< formatTable <<< prepareArguments 90 | 91 | argumentSynopsis :: Array Argument -> String 92 | argumentSynopsis = map format >>> Str.joinWith " " 93 | where 94 | format arg = 95 | Str.toUpper $ 96 | if arg.required 97 | then arg.name 98 | else "[" <> arg.name <> "]" 99 | 100 | helpOpt :: Option 101 | helpOpt = option "help" ["--help", "-h"] Type.flag 102 | "Show this help message." 103 | 104 | printHelp :: Outputter -> Array Option -> Array Command -> Aff Unit 105 | printHelp out globals commands = do 106 | commandName <- liftEffect getCommandName 107 | out.write $ "Usage: " <> commandName <> " [global-options] [command-options]\n" 108 | out.bolded "\nGlobal options:\n" 109 | formatOpts (globals <> [helpOpt]) >>= out.write 110 | out.bolded "\nCommands:\n" 111 | formatCmds commands >>= out.write 112 | helpText <- liftEffect $ wrap ("Use `" <> commandName <> 113 | " --help` to " <> 114 | "learn about command specific options.") 2 115 | out.write $ "\n" <> helpText <> "\n\n" 116 | 117 | printCommandHelp :: Outputter -> Array Option -> Command -> Aff Unit 118 | printCommandHelp out globals command = do 119 | commandName <- liftEffect getCommandName 120 | out.write $ "Usage: " <> commandName <> " [global-options] " <> 121 | command.name <> " " <> 122 | (if hasArguments then argumentSynopsis command.arguments <> " " else "") <> 123 | (if hasCommandOpts then "[command-options]" else "") <> "\n" 124 | out.bolded $ "\nCommand: " <> command.name <> "\n" 125 | out.write $ " " <> command.desc <> "\n" 126 | when hasArguments do 127 | out.bolded "\nArguments:\n" 128 | formatArguments command.arguments >>= out.write 129 | when hasCommandOpts do 130 | out.bolded "\nCommand options:\n" 131 | formatOpts (command.options) >>= out.write 132 | out.bolded "\nGlobal options:\n" 133 | formatOpts (globals <> [helpOpt]) >>= out.write 134 | out.bolded "\nPassthrough options:\n" 135 | formatPassThrough command.passthroughDesc >>= out.write 136 | out.write "\n" 137 | 138 | where 139 | hasCommandOpts = not (null command.options) 140 | hasArguments = not (null command.arguments) 141 | 142 | getCommandName :: Effect String 143 | getCommandName = maybe "pulp" (_.name <<< Path.parse) <<< (_ !! 1) <$> Process.argv 144 | -------------------------------------------------------------------------------- /src/Pulp/Args/Parser.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Args.Parser where 2 | 3 | import Prelude hiding (when) 4 | 5 | import Control.Alt ((<|>)) 6 | import Control.Monad.State.Class (get) 7 | import Control.Monad.Trans.Class (lift) 8 | import Data.Array (many) 9 | import Data.Either (Either(..)) 10 | import Data.Foldable (find, elem) 11 | import Data.List (List) 12 | import Data.List as List 13 | import Data.Map as Map 14 | import Data.Maybe (Maybe(..), maybe) 15 | import Data.String (joinWith) 16 | import Data.Traversable (traverse) 17 | import Data.Tuple (Tuple(..)) 18 | import Effect.Aff (Aff) 19 | import Foreign (unsafeToForeign) 20 | import Pulp.Args (Args, Argument, Command, Help(..), OptParser, Option, Options) 21 | import Pulp.Utils (throw) 22 | import Text.Parsing.Parser (ParseError, ParseState(..), ParserT, fail, runParserT) 23 | import Text.Parsing.Parser.Combinators ((), try, optionMaybe) 24 | import Text.Parsing.Parser.Pos as Pos 25 | import Text.Parsing.Parser.Token as Token 26 | 27 | halt :: forall a. String -> OptParser a 28 | halt err = lift $ throw err 29 | 30 | matchNamed :: forall a r. Eq a => { name :: a, alias :: Array a | r } -> a -> Boolean 31 | matchNamed o key = o.name == key || elem key o.alias 32 | 33 | matchOpt :: Option -> String -> Boolean 34 | matchOpt o key = elem key o.match 35 | 36 | -- | A version of Text.Parsing.Parser.Token.token which lies about the position, 37 | -- | since we don't care about it here. 38 | token :: forall m a. Monad m => ParserT (List a) m a 39 | token = Token.token (const Pos.initialPos) 40 | 41 | -- | A version of Text.Parsing.Parser.Token.match which lies about the position, 42 | -- | since we don't care about it here. 43 | match :: forall m a. Monad m => Eq a => a -> ParserT (List a) m a 44 | match = Token.match (const Pos.initialPos) 45 | 46 | -- | A version of Text.Parsing.Parser.Token.when which lies about the position, 47 | -- | since we don't care about it here. 48 | when :: forall m a. Monad m => (a -> Boolean) -> ParserT (List a) m a 49 | when = Token.when (const Pos.initialPos) 50 | 51 | lookup :: forall m a b. Monad m => Eq b => Show b => (a -> b -> Boolean) -> Array a -> ParserT (List b) m (Tuple b a) 52 | lookup matches table = do 53 | next <- token 54 | case find (\i -> matches i next) table of 55 | Just entry -> pure $ Tuple next entry 56 | Nothing -> fail ("Unknown command: " <> show next) 57 | 58 | lookupOpt :: Array Option -> OptParser (Tuple String Option) 59 | lookupOpt = lookup matchOpt 60 | 61 | lookupCmd :: Array Command -> OptParser (Tuple String Command) 62 | lookupCmd = lookup matchNamed 63 | 64 | opt :: Array Option -> OptParser Options 65 | opt opts = do 66 | o <- lookupOpt opts 67 | case o of 68 | (Tuple key option) -> do 69 | val <- option.parser.parser key 70 | pure $ Map.singleton option.name val 71 | 72 | arg :: Argument -> OptParser Options 73 | arg a | a.required = do 74 | next <- token <|> fail ("Required argument \"" <> a.name <> "\" missing.") 75 | val <- a.parser next 76 | pure (Map.singleton a.name (Just val)) 77 | arg a = do 78 | val <- (try (Just <$> (token >>= a.parser))) <|> pure Nothing 79 | pure (maybe Map.empty (Map.singleton a.name <<< Just) val) 80 | 81 | cmd :: Array Command -> OptParser Command 82 | cmd cmds = do 83 | o <- lookupCmd cmds "command" 84 | case o of 85 | (Tuple _ option) -> pure option 86 | 87 | extractDefault :: Option -> Options 88 | extractDefault o = 89 | case o.defaultValue of 90 | Just def -> 91 | Map.singleton o.name (Just (unsafeToForeign def)) 92 | Nothing -> 93 | Map.empty 94 | 95 | -- See also https://github.com/purescript-contrib/purescript-parsing/issues/25 96 | eof :: forall m a. Monad m => (List a -> String) -> ParserT (List a) m Unit 97 | eof msg = 98 | get >>= \(ParseState (input :: List a) _ _) -> 99 | unless (List.null input) (fail (msg input)) 100 | 101 | parseArgv :: Array Option -> Array Command -> OptParser (Either Help Args) 102 | parseArgv globals commands = do 103 | globalOpts <- globalDefaults <$> many (try (opt globals)) 104 | command <- cmd commands 105 | helpForCommand command <|> normalCommand globalOpts command 106 | 107 | where 108 | normalCommand globalOpts command = do 109 | commandArgs <- traverse arg command.arguments 110 | commandOpts <- many $ try $ opt command.options 111 | restSep <- optionMaybe $ match "--" 112 | remainder <- maybe (pure []) (const (many token)) restSep 113 | eof unrecognised 114 | pure $ Right { 115 | globalOpts, 116 | command, 117 | commandOpts: Map.unions (commandOpts <> defs command.options), 118 | commandArgs: Map.unions commandArgs, 119 | remainder 120 | } 121 | 122 | helpForCommand command = 123 | matchHelp *> pure (Left (Help command)) 124 | 125 | defs = map extractDefault 126 | unrecognised = 127 | ("Unrecognised arguments: " <> _) 128 | <<< joinWith ", " 129 | <<< List.toUnfoldable 130 | 131 | globalDefaults opts = 132 | Map.unions (opts <> defs globals) 133 | 134 | -- match a single "-h" or "--help" 135 | matchHelp = 136 | void (when (_ `elem` ["-h", "--help"])) 137 | 138 | parse :: Array Option -> Array Command -> Array String -> Aff (Either ParseError (Either Help Args)) 139 | parse globals commands s = 140 | runParserT (List.fromFoldable s) (parseArgv globals commands) 141 | -------------------------------------------------------------------------------- /src/Pulp/Args/Types.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Args.Types 2 | ( flag 3 | , string 4 | , file 5 | , int 6 | , directory 7 | , directories 8 | , versionBump 9 | ) where 10 | 11 | import Prelude 12 | 13 | import Control.Alt ((<|>)) 14 | import Control.Monad.Trans.Class (lift) 15 | import Data.Array (filter) 16 | import Data.Foldable (for_) 17 | import Data.Int (fromString) 18 | import Data.Maybe (Maybe(..)) 19 | import Data.String (null, split, Pattern(..)) 20 | import Foreign (unsafeToForeign) 21 | import Node.FS.Aff (stat) 22 | import Node.FS.Stats (Stats, isFile, isDirectory) 23 | import Node.Path as Path 24 | import Pulp.Args (ArgumentParser, OptParser, OptionParser) 25 | import Pulp.Args.Parser (halt, token) 26 | import Pulp.VersionBump (parseBump) 27 | import Text.Parsing.Parser (fail) 28 | 29 | argErr :: forall a. String -> String -> OptParser a 30 | argErr arg msg = 31 | halt ("Argument " <> arg <> ": " <> msg) 32 | 33 | flag :: OptionParser 34 | flag = { 35 | name: Nothing, 36 | parser: \_ -> pure Nothing 37 | } 38 | 39 | string :: OptionParser 40 | string = { 41 | name: Just "", 42 | parser: \arg -> 43 | (Just <<< unsafeToForeign) <$> (token <|> argErr arg "Needs a string argument.") 44 | } 45 | 46 | int :: OptionParser 47 | int = { 48 | name: Just "", 49 | parser: \arg -> do 50 | let err = argErr arg "Needs an int argument." :: forall a. OptParser a 51 | mint <- fromString <$> (token <|> err) 52 | case mint of 53 | Just i -> pure (Just (unsafeToForeign i)) 54 | Nothing -> err 55 | } 56 | 57 | require :: (Stats -> Boolean) -> String -> String -> OptParser Unit 58 | require pred typ path = do 59 | s <- lift (stat path) <|> halt (typ <> " '" <> path <> "' not found.") 60 | unless (pred s) 61 | (halt ("Path '" <> path <> "' is not a " <> typ <> ".")) 62 | 63 | requireFile :: String -> OptParser Unit 64 | requireFile = require isFile "File" 65 | 66 | requireDirectory :: String -> OptParser Unit 67 | requireDirectory = require isDirectory "Directory" 68 | 69 | file :: OptionParser 70 | file = { 71 | name: Just "", 72 | parser: \arg -> do 73 | path <- token <|> argErr arg "Needs a file argument." 74 | requireFile path 75 | pure $ Just (unsafeToForeign path) 76 | } 77 | 78 | directory :: OptionParser 79 | directory = { 80 | name: Just "", 81 | parser: \arg -> do 82 | path <- token <|> argErr arg "Needs a directory argument." 83 | requireDirectory path 84 | pure $ Just (unsafeToForeign path) 85 | } 86 | 87 | directories :: OptionParser 88 | directories = { 89 | name: Just "", 90 | parser: \arg -> do 91 | paths <- token <|> argErr arg "Needs a directory argument." 92 | let paths' = filter (not <<< null) $ split (Pattern Path.delimiter) paths 93 | for_ paths' requireDirectory 94 | pure $ Just (unsafeToForeign paths') 95 | } 96 | 97 | -- TODO: this is gross; we end up parsing the version twice. Probably should 98 | -- fix this by parameterising OptionParsers and ArgumentParsers based on the 99 | -- type of the thing they parse. 100 | versionBump :: ArgumentParser 101 | versionBump arg = 102 | case parseBump arg of 103 | Just _ -> 104 | pure (unsafeToForeign arg) 105 | Nothing -> 106 | fail ("Not a valid version bump. Must be: 'major', 'minor', 'patch', " 107 | <> "or a version.") 108 | -------------------------------------------------------------------------------- /src/Pulp/Browserify.js: -------------------------------------------------------------------------------- 1 | // module Pulp.Browserify 2 | 3 | "use strict"; 4 | 5 | function write(input, output, callback) { 6 | var pipe = require("through")(); 7 | input.pipe(pipe); 8 | pipe.pipe(output, {end: false}); 9 | pipe.on("end", callback); 10 | } 11 | 12 | exports.browserifyBundleImpl = function browserifyBundle$prime(opts, callback) { 13 | var stream = new require("stream").Readable(); 14 | var browserify = require("browserify"); 15 | var mold = require("mold-source-map"); 16 | var path = require("path"); 17 | stream.push(opts.src); 18 | stream.push(null); 19 | var b = browserify({ 20 | basedir: opts.basedir, 21 | entries: stream, 22 | standalone: opts.standalone, 23 | debug: opts.debug 24 | }); 25 | if (opts.transform) { 26 | b.transform(opts.transform); 27 | } 28 | var bundle = b.bundle(); 29 | if (opts.debug) { 30 | var tmpRoot = path.dirname(opts.tmpFilePath); 31 | bundle = bundle 32 | .pipe(mold.transformSourcesContent(function (s, i) { 33 | if (i === 1) { 34 | return s.replace('//# sourceMappingURL=', "$&" + tmpRoot + "/"); 35 | } 36 | return s; 37 | }) 38 | ); 39 | } 40 | write(bundle, opts.out, callback); 41 | }; 42 | 43 | exports.browserifyIncBundleImpl = function browserifyIncBundle$prime(opts, callback) { 44 | var browserifyInc = require("browserify-incremental"); 45 | var mold = require("mold-source-map"); 46 | var path = require('path'); 47 | var b = browserifyInc({ 48 | basedir: opts.buildPath, 49 | cacheFile: opts.cachePath, 50 | standalone: opts.standalone, 51 | debug: opts.debug 52 | }); 53 | b.add(opts.path); 54 | if (opts.transform) b.transform(opts.transform); 55 | var bundle = b.bundle(); 56 | if (opts.debug) { 57 | bundle = bundle.pipe(mold.transform(function (map) { 58 | map.sourceRoot(path.resolve()); 59 | return map.toComment(); 60 | })); 61 | } 62 | write(bundle, opts.out, callback); 63 | }; 64 | -------------------------------------------------------------------------------- /src/Pulp/Browserify.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Browserify where 3 | 4 | import Prelude 5 | 6 | import Data.Argonaut (Json, caseJsonArray, caseJsonObject, caseJsonString, fromArray, fromObject, fromString, jsonEmptyObject, jsonNull, jsonParser) 7 | import Data.Argonaut.Core (stringify) 8 | import Data.Either (Either(..)) 9 | import Data.Foldable (fold) 10 | import Data.Function.Uncurried (Fn2, runFn2) 11 | import Data.Map as Map 12 | import Data.Maybe (Maybe(..), isJust, maybe) 13 | import Data.Nullable (Nullable, toNullable) 14 | import Data.Traversable (traverse) 15 | import Effect (Effect) 16 | import Effect.Aff (Aff, apathize) 17 | import Effect.Class (liftEffect) 18 | import Foreign.Object as Object 19 | import Node.Encoding (Encoding(UTF8)) 20 | import Node.FS.Aff (unlink, writeTextFile, readTextFile) 21 | import Node.Path as Path 22 | import Node.Process as Process 23 | import Pulp.Args (Action(..), Args, Options, runAction) 24 | import Pulp.Args.Get (getFlag, getOption, getOption') 25 | import Pulp.Build as Build 26 | import Pulp.Exec (pursBundle) 27 | import Pulp.Files (outputModules) 28 | import Pulp.Outputter (getOutputter) 29 | import Pulp.Project (Project(..)) 30 | import Pulp.Run (jsEscape, makeCjsEntry) 31 | import Pulp.Sorcery (sorcery) 32 | import Pulp.System.FFI (Callback, runNode) 33 | import Pulp.System.Files (openTemp) 34 | import Pulp.System.Stream (WritableStream) 35 | import Pulp.Validate (failIfUsingEsModulesPsVersion) 36 | 37 | action :: Action 38 | action = Action \args -> do 39 | out <- getOutputter args 40 | 41 | failIfUsingEsModulesPsVersion out $ Just 42 | "Code path reason: browserify only works on CommonJS modules" 43 | 44 | cwd <- liftEffect Process.cwd 45 | out.log $ "Browserifying project in " <> cwd 46 | 47 | optimise <- getFlag "optimise" args.commandOpts 48 | let act = if optimise then optimising else incremental 49 | 50 | buildForBrowserify args 51 | runAction act args 52 | 53 | out.log "Browserified." 54 | 55 | makeExport :: String -> Boolean -> String 56 | makeExport main export = 57 | if export 58 | then "module.exports = require(\"" <> jsEscape main <> "\");\n" 59 | else makeCjsEntry main 60 | 61 | makeOptExport :: String -> String 62 | makeOptExport main = "module.exports = PS[\"" <> jsEscape main <> "\"];\n" 63 | 64 | buildForBrowserify :: Args -> Aff Unit 65 | buildForBrowserify args = do 66 | skip <- getFlag "skipCompile" args.commandOpts 67 | when (not skip) do 68 | let munge = Map.delete "to" >>> Map.delete "optimise" 69 | Build.build $ args { commandOpts = munge args.commandOpts, remainder = [] } 70 | 71 | shouldSkipEntryPoint :: Options -> Aff Boolean 72 | shouldSkipEntryPoint opts = do 73 | skipEntryPoint <- getFlag "skipEntryPoint" opts 74 | standalone :: Maybe String <- getOption "standalone" opts 75 | pure (skipEntryPoint || isJust standalone) 76 | 77 | optimising :: Action 78 | optimising = Action \args -> do 79 | out <- getOutputter args 80 | 81 | let opts = Map.union args.globalOpts args.commandOpts 82 | 83 | buildPath <- getOption' "buildPath" opts 84 | main <- getOption' "main" opts 85 | transform <- getOption "transform" opts 86 | standalone <- getOption "standalone" opts 87 | sourceMaps <- getFlag "sourceMaps" opts 88 | toOpt <- getOption "to" opts 89 | 90 | { path: tmpFilePath } <- openTemp { prefix: "pulp-browserify-bundle-", suffix: ".js" } 91 | 92 | skipEntryPoint <- shouldSkipEntryPoint opts 93 | let bundleArgs = fold 94 | [ ["--module=" <> main] 95 | , if skipEntryPoint then [] else ["--main=" <> main] 96 | , if sourceMaps then ["--source-maps"] else [] 97 | , ["-o", tmpFilePath] 98 | , args.remainder 99 | ] 100 | 101 | _ <- pursBundle (outputModules buildPath) bundleArgs Nothing 102 | bundledJs <- readTextFile UTF8 tmpFilePath 103 | 104 | let mapFile = tmpFilePath <> ".map" 105 | when sourceMaps do 106 | smText <- readTextFile UTF8 mapFile 107 | path <- liftEffect $ updateSourceMapPaths (Path.dirname mapFile) smText 108 | writeTextFile UTF8 mapFile path 109 | 110 | out.log "Browserifying..." 111 | 112 | liftEffect $ setupNodePath buildPath 113 | 114 | Build.withOutputStream opts $ \out' -> do 115 | basedir <- liftEffect $ Path.resolve [] buildPath 116 | outDir <- liftEffect $ maybe (pure buildPath) (Path.resolve [ buildPath ] <<< Path.dirname) toOpt 117 | browserifyBundle 118 | { basedir 119 | , src: bundledJs <> if isJust standalone then makeOptExport main else "" 120 | , transform: toNullable transform 121 | , standalone: toNullable standalone 122 | , out: out' 123 | , debug: sourceMaps 124 | , outDir 125 | , tmpFilePath: tmpFilePath 126 | } 127 | case toOpt of 128 | Just to | sourceMaps -> do 129 | sorcery to 130 | unlink mapFile 131 | _ -> pure unit 132 | 133 | incremental :: Action 134 | incremental = Action \args -> do 135 | out <- getOutputter args 136 | 137 | let opts = Map.union args.globalOpts args.commandOpts 138 | 139 | out.log "Browserifying..." 140 | 141 | buildPath <- getOption' "buildPath" opts 142 | liftEffect $ setupNodePath buildPath 143 | 144 | force <- getFlag "force" opts 145 | Project pro <- getOption' "_project" opts 146 | cachePath <- liftEffect $ Path.resolve [pro.cache] "browserify.json" 147 | when force 148 | (apathize $ unlink cachePath) 149 | 150 | transform <- getOption "transform" opts 151 | standalone <- getOption "standalone" opts 152 | main <- getOption' "main" opts 153 | sourceMaps <- getFlag "sourceMaps" opts 154 | 155 | skipEntryPoint <- shouldSkipEntryPoint opts 156 | path <- if skipEntryPoint 157 | then 158 | pure $ Path.concat [buildPath, main] 159 | else do 160 | let entryJs = makeExport main $ isJust standalone 161 | let entryPath = Path.concat [buildPath, "browserify.js"] 162 | writeTextFile UTF8 entryPath entryJs 163 | pure entryPath 164 | 165 | toOpt <- getOption "to" opts 166 | Build.withOutputStream opts $ \out' -> do 167 | outDir <- liftEffect $ maybe (pure buildPath) (Path.resolve [ buildPath ] <<< Path.dirname) toOpt 168 | browserifyIncBundle 169 | { basedir: buildPath 170 | , cacheFile: cachePath 171 | , path: path 172 | , transform: toNullable transform 173 | , standalone: toNullable standalone 174 | , out: out' 175 | , debug: sourceMaps 176 | , outDir 177 | } 178 | case toOpt of 179 | Just to | sourceMaps -> sorcery to 180 | _ -> pure unit 181 | 182 | -- | Given the build path, modify this process' NODE_PATH environment variable 183 | -- | for browserify. 184 | setupNodePath :: String -> Effect Unit 185 | setupNodePath buildPath = do 186 | nodePath <- Process.lookupEnv "NODE_PATH" 187 | buildPath' <- Path.resolve [] buildPath 188 | Process.setEnv "NODE_PATH" $ 189 | case nodePath of 190 | Just p -> buildPath' <> Path.delimiter <> p 191 | Nothing -> buildPath' 192 | 193 | type BrowserifyOptions = 194 | { basedir :: String 195 | , src :: String 196 | , transform :: Nullable String 197 | , standalone :: Nullable String 198 | , out :: WritableStream 199 | , debug :: Boolean 200 | , outDir :: String 201 | , tmpFilePath:: String 202 | } 203 | 204 | foreign import browserifyBundleImpl :: Fn2 BrowserifyOptions 205 | (Callback Unit) 206 | Unit 207 | 208 | browserifyBundle :: BrowserifyOptions -> Aff Unit 209 | browserifyBundle opts = runNode $ runFn2 browserifyBundleImpl opts 210 | 211 | type BrowserifyIncOptions = 212 | { basedir :: String 213 | , cacheFile :: String 214 | , path :: String 215 | , transform :: Nullable String 216 | , standalone :: Nullable String 217 | , out :: WritableStream 218 | , debug :: Boolean 219 | , outDir :: String 220 | } 221 | 222 | foreign import browserifyIncBundleImpl :: Fn2 BrowserifyIncOptions 223 | (Callback Unit) 224 | Unit 225 | 226 | browserifyIncBundle :: BrowserifyIncOptions -> Aff Unit 227 | browserifyIncBundle opts = runNode $ runFn2 browserifyIncBundleImpl opts 228 | 229 | updateSourceMapPaths :: String -> String -> Effect String 230 | updateSourceMapPaths basePath text = 231 | case jsonParser text of 232 | Left _ -> pure text 233 | Right json -> do 234 | resolutions <- caseJsonObject (pure jsonEmptyObject) (map fromObject <<< updateWithEffect resolveFiles "sources") json 235 | pure (stringify resolutions) 236 | where 237 | updateWithEffect effect key map = do 238 | value <- maybe (pure Nothing) effect (Object.lookup key map) 239 | pure $ Object.update (const value) key map 240 | 241 | resolveFiles :: Json -> Effect (Maybe Json) 242 | resolveFiles = caseJsonArray (pure Nothing) (map (Just <<< fromArray) <<< traverse resolveFile) 243 | 244 | resolveFile :: Json -> Effect Json 245 | resolveFile = caseJsonString (pure jsonNull) (map fromString <<< Path.resolve [ basePath ]) 246 | -------------------------------------------------------------------------------- /src/Pulp/Build.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Build 2 | ( action 3 | , build 4 | , testBuild 5 | , runBuild 6 | , withOutputStream 7 | , shouldBundle 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Control.Monad.Error.Class (throwError) 13 | import Data.Either (Either(..)) 14 | import Data.Foldable (fold) 15 | import Data.List (List(..)) 16 | import Data.List.NonEmpty as NEL 17 | import Data.Map (union) 18 | import Data.Maybe (Maybe(..), maybe) 19 | import Data.Set as Set 20 | import Data.String (Pattern(..), split) 21 | import Data.Version.Haskell (Version(..)) 22 | import Effect.Aff (Aff, apathize, attempt) 23 | import Effect.Class (liftEffect) 24 | import Node.FS.Aff as FS 25 | import Node.Path as Path 26 | import Node.Process as Process 27 | import Pulp.Args (Action(..), Args, Options, runAction) 28 | import Pulp.Args.Get (getFlag, getOption, getOption', hasOption) 29 | import Pulp.Exec (psa, pursBuild, pursBundle) 30 | import Pulp.Files (defaultGlobs, outputModules, sources, testGlobs) 31 | import Pulp.Outputter (getOutputter) 32 | import Pulp.Sorcery (sorcery) 33 | import Pulp.System.Files as Files 34 | import Pulp.System.Stream (write, end, WritableStream, stdout) 35 | import Pulp.Validate (dropPreRelBuildMeta, failIfUsingEsModulesPsVersion, getPsaVersion, getPursVersion) 36 | import Pulp.Versions.PureScript (psVersions) 37 | 38 | data BuildType = NormalBuild | TestBuild | RunBuild 39 | 40 | derive instance eqBuildType :: Eq BuildType 41 | 42 | action :: Action 43 | action = go NormalBuild 44 | 45 | build :: Args -> Aff Unit 46 | build = runAction action 47 | 48 | testBuild :: Args -> Aff Unit 49 | testBuild = runAction (go TestBuild) 50 | 51 | runBuild :: Args -> Aff Unit 52 | runBuild = runAction (go RunBuild) 53 | 54 | go :: BuildType -> Action 55 | go buildType = Action \args -> do 56 | let opts = union args.globalOpts args.commandOpts 57 | out <- getOutputter args 58 | 59 | cwd <- liftEffect Process.cwd 60 | out.log $ "Building project in " <> cwd 61 | 62 | globs <- Set.union <$> defaultGlobs opts 63 | <*> (if buildType == TestBuild 64 | then testGlobs opts 65 | else pure Set.empty) 66 | 67 | buildPath <- getOption' "buildPath" args.commandOpts 68 | sourceMaps <- getFlag "sourceMaps" args.commandOpts 69 | ver <- getPursVersion out 70 | jobs :: Maybe Int <- getOption "jobs" args.commandOpts 71 | let jobsArgs = maybe [] (\j -> ["+RTS", "-N" <> show j, "-RTS"]) jobs 72 | sourceMapArg = case sourceMaps of 73 | true | (dropPreRelBuildMeta ver) >= psVersions.v0_12_0 -> [ "--codegen", "sourcemaps" ] 74 | true -> ["--source-maps"] 75 | _ -> [] 76 | sourceGlobs = sources globs 77 | extraArgs = if buildType /= RunBuild then args.remainder else [] 78 | binArgs = ["-o", buildPath] <> sourceMapArg <> jobsArgs <> extraArgs 79 | 80 | usePsa <- shouldUsePsa args 81 | if usePsa 82 | then do 83 | monochrome <- getFlag "monochrome" args.globalOpts 84 | dependencyPath <- getOption' "dependencyPath" args.commandOpts 85 | let binArgs' = binArgs <> ["--is-lib=" <> dependencyPath] 86 | <> (if monochrome 87 | then ["--monochrome"] 88 | else []) 89 | psa sourceGlobs binArgs' Nothing 90 | else 91 | pursBuild sourceGlobs binArgs Nothing 92 | 93 | out.log "Build successful." 94 | 95 | shouldBundle' <- shouldBundle args 96 | when shouldBundle' do 97 | failIfUsingEsModulesPsVersion out $ Just 98 | "Code path reason: you used the --optimize and/or --to flag(s)" 99 | bundle args 100 | 101 | shouldBundle :: Args -> Aff Boolean 102 | shouldBundle args = do 103 | let opts = union args.globalOpts args.commandOpts 104 | (||) <$> getFlag "optimise" opts <*> hasOption "to" opts 105 | 106 | shouldUsePsa :: Args -> Aff Boolean 107 | shouldUsePsa args = do 108 | noPsa <- getFlag "noPsa" args.commandOpts 109 | 110 | if noPsa 111 | then 112 | pure false 113 | else do 114 | out <- getOutputter args 115 | r <- attempt (getPsaVersion out) 116 | case r of 117 | Left _ -> 118 | pure false 119 | Right v -> 120 | pure (v >= minimumPsaVersion) 121 | 122 | where 123 | -- TODO this is actually semver 124 | minimumPsaVersion = Version (NEL.cons' 0 (Cons 7 (Cons 0 Nil))) Nil 125 | 126 | bundle :: Args -> Aff Unit 127 | bundle args = do 128 | let opts = union args.globalOpts args.commandOpts 129 | out <- getOutputter args 130 | 131 | out.log "Bundling JavaScript..." 132 | 133 | skipEntry <- getFlag "skipEntryPoint" opts 134 | modules <- parseModulesOption <$> getOption "modules" opts 135 | buildPath <- getOption' "buildPath" opts 136 | main <- getOption' "main" opts 137 | sourceMaps <- getFlag "sourceMaps" args.commandOpts 138 | to :: Maybe String <- getOption "to" opts 139 | 140 | let bundleArgs = fold 141 | [ ["--module=" <> main] 142 | , if skipEntry then [] else ["--main=" <> main] 143 | , map (\m -> "--module=" <> m) modules 144 | , if sourceMaps then ["--source-maps"] else [] 145 | , maybe [] (\f -> ["-o", f]) to 146 | , args.remainder 147 | ] 148 | 149 | bundledJs <- pursBundle (outputModules buildPath) bundleArgs Nothing 150 | 151 | case to of 152 | Just to' | sourceMaps -> sorcery to' 153 | Just _ -> pure unit 154 | _ -> withOutputStream opts $ \out' -> write out' bundledJs 155 | 156 | out.log "Bundled." 157 | 158 | where 159 | parseModulesOption = maybe [] (split (Pattern ",")) 160 | 161 | -- | Get a writable stream which output should be written to, based on the 162 | -- | value of the 'to' option. 163 | withOutputStream :: Options -> (WritableStream -> Aff Unit) -> Aff Unit 164 | withOutputStream opts aff = do 165 | to :: Maybe String <- getOption "to" opts 166 | case to of 167 | Just destFile -> 168 | do 169 | let dir = Path.dirname destFile 170 | let tmpFile = dir <> Path.sep <> "." <> Path.basename destFile 171 | Files.mkdirIfNotExist dir 172 | res <- attempt do 173 | stream <- liftEffect $ Files.createWriteStream tmpFile 174 | void $ aff stream 175 | void $ end stream 176 | case res of 177 | Right _ -> 178 | FS.rename tmpFile destFile 179 | Left err -> do 180 | void $ apathize $ FS.unlink tmpFile 181 | throwError err 182 | Nothing -> 183 | aff stdout 184 | -------------------------------------------------------------------------------- /src/Pulp/BumpVersion.purs: -------------------------------------------------------------------------------- 1 | module Pulp.BumpVersion ( action ) where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Error.Class (throwError) 6 | import Data.Either (Either(..)) 7 | import Data.Foldable (lookup) 8 | import Data.Foldable as Foldable 9 | import Data.List (List(..)) 10 | import Data.Maybe (Maybe(..), maybe) 11 | import Data.String as String 12 | import Data.Tuple (Tuple(..), snd) 13 | import Data.Version (Version) 14 | import Data.Version as Version 15 | import Effect.Aff (Aff) 16 | import Effect.Exception (error) 17 | import Pulp.Args (Action(..), Args) 18 | import Pulp.Args.Get (getOption) 19 | import Pulp.Exec (exec) 20 | import Pulp.Git (dropPrefix, getLatestTaggedVersion, requireCleanGitWorkingTree) 21 | import Pulp.Outputter (Outputter, getOutputter) 22 | import Pulp.Publish (resolutionsFile, parseJsonFile, BowerJson) 23 | import Pulp.System.Read as Read 24 | import Pulp.VersionBump (VersionBump(..), applyBump, parseBump) 25 | 26 | action :: Action 27 | action = Action \args -> do 28 | out <- getOutputter args 29 | 30 | requireCleanGitWorkingTree 31 | checkPursPublish args 32 | version <- bumpVersion args 33 | tagNewVersion version 34 | out.log ("Bumped to: v" <> Version.showVersion version) 35 | 36 | -- | Try running `purs publish --dry-run` to make sure the code is suitable for 37 | -- | release. 38 | checkPursPublish :: Args -> Aff Unit 39 | checkPursPublish args = do 40 | out <- getOutputter args 41 | out.log "Checking your package using purs publish..." 42 | manifest :: BowerJson <- parseJsonFile "bower.json" 43 | resolutions <- resolutionsFile manifest args 44 | exec 45 | "purs" 46 | ["publish", "--manifest", "bower.json", "--resolutions", resolutions, "--dry-run"] 47 | Nothing 48 | 49 | -- | Returns the new version that we should bump to. 50 | bumpVersion :: Args -> Aff Version 51 | bumpVersion args = do 52 | out <- getOutputter args 53 | mcurrent <- map (map snd) getLatestTaggedVersion 54 | mbumpStr <- getOption "bump" args.commandArgs 55 | mbump <- case mbumpStr of 56 | -- TODO: this is gross. See also Pulp.Args.Types.versionBump 57 | Just bumpStr -> maybe (internalError "invalid bump") (pure <<< Just) (parseBump bumpStr) 58 | Nothing -> pure Nothing 59 | 60 | newVersion mbump mcurrent out 61 | 62 | newVersion :: Maybe VersionBump -> Maybe Version -> Outputter -> Aff Version 63 | newVersion mbump mcurrent out = case mcurrent, mbump of 64 | _, Just (ToExact version) -> pure version 65 | Just current, Just bump -> pure $ applyBump bump current 66 | Just current, Nothing -> promptCurrent out current 67 | Nothing , _ -> promptInitial out 68 | 69 | tagNewVersion :: Version -> Aff Unit 70 | tagNewVersion version = do 71 | let versionStr = "v" <> Version.showVersion version 72 | exec "git" 73 | [ "commit" 74 | , "--allow-empty" 75 | , "--message=" <> versionStr 76 | ] Nothing 77 | exec "git" 78 | [ "tag" 79 | , "--annotate" 80 | , "--message=" <> versionStr 81 | , versionStr 82 | ] Nothing 83 | 84 | -- | Prompt and ask the user what to use as the initial version. 85 | promptInitial :: Outputter -> Aff Version 86 | promptInitial out = do 87 | out.log "Initial version" 88 | out.write "You can release this code as:\n" 89 | 90 | Foldable.for_ initialOptions \(Tuple letter version) -> 91 | out.write (letter <> ") v" <> Version.showVersion version <> "\n") 92 | 93 | untilJust do 94 | choice <- Read.read { prompt: "Choose one, or enter a specific version:" 95 | , silent: false 96 | } 97 | 98 | case lookup (String.toLower choice) initialOptions of 99 | Just v -> 100 | pure (Just v) 101 | Nothing -> 102 | case Version.parseVersion (dropPrefix "v" choice) of 103 | Right v -> 104 | pure (Just v) 105 | Left _ -> do 106 | out.log "Sorry, that choice wasn't understood." 107 | pure Nothing 108 | 109 | where 110 | initialOptions = 111 | [ Tuple "a" (vers 1 0 0) 112 | , Tuple "b" (vers 0 1 0) 113 | , Tuple "c" (vers 0 0 1) 114 | ] 115 | 116 | vers major minor patch = Version.version major minor patch Nil Nil 117 | 118 | promptCurrent :: Outputter -> Version -> Aff Version 119 | promptCurrent out current = do 120 | out.log ("The current version is v" <> Version.showVersion current) 121 | out.write "You can bump the version to:\n" 122 | 123 | Foldable.for_ bumpOptions \(Tuple letter bump) -> 124 | out.write (letter <> ") v" <> Version.showVersion (applyBump bump current) <> "\n") 125 | 126 | untilJust do 127 | choice <- Read.read { prompt: "Choose one, or enter a specific version:" 128 | , silent: false 129 | } 130 | 131 | case lookup (String.toLower choice) bumpOptions of 132 | Just bump -> 133 | pure (Just (applyBump bump current)) 134 | Nothing -> 135 | case Version.parseVersion (dropPrefix "v" choice) of 136 | Right v -> 137 | pure (Just v) 138 | Left _ -> do 139 | out.log "Sorry, that choice wasn't understood." 140 | pure Nothing 141 | 142 | where 143 | bumpOptions = 144 | [ Tuple "a" Major 145 | , Tuple "b" Minor 146 | , Tuple "c" Patch 147 | ] 148 | 149 | untilJust :: forall m a. (Monad m) => m (Maybe a) -> m a 150 | untilJust act = do 151 | val <- act 152 | case val of 153 | Just x -> 154 | pure x 155 | Nothing -> 156 | untilJust act 157 | 158 | internalError :: forall a. String -> Aff a 159 | internalError msg = 160 | throwError <<< error $ 161 | "Internal error in Pulp.BumpVersion: " <> msg <> "\n" <> 162 | "This is a bug. Please report it.\n" 163 | -------------------------------------------------------------------------------- /src/Pulp/Docs.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Docs where 3 | 4 | import Prelude 5 | 6 | import Data.Map as Map 7 | import Data.Maybe (Maybe(..)) 8 | import Data.Set as Set 9 | import Effect.Class (liftEffect) 10 | import Node.Process as Process 11 | import Pulp.Args (Action(..)) 12 | import Pulp.Args.Get (getFlag, getOption') 13 | import Pulp.Exec (exec) 14 | import Pulp.Files (defaultGlobs, sources, testGlobs) 15 | import Pulp.Outputter (getOutputter) 16 | import Pulp.Validate (dropPreRelBuildMeta, getPursVersion) 17 | import Pulp.Versions.PureScript (psVersions) 18 | 19 | action :: Action 20 | action = Action \args -> do 21 | out <- getOutputter args 22 | pursVersion <- getPursVersion out 23 | 24 | cwd <- liftEffect Process.cwd 25 | out.log $ "Generating documentation in " <> cwd 26 | 27 | let opts = Map.union args.globalOpts args.commandOpts 28 | 29 | withTests <- getFlag "withTests" opts 30 | let includeWhen b act = if b then act else pure Set.empty 31 | globInputFiles <- Set.union <$> includeWhen withTests (testGlobs opts) 32 | <*> defaultGlobs opts 33 | 34 | buildPath <- getOption' "buildPath" opts 35 | 36 | when ((dropPreRelBuildMeta pursVersion) < psVersions.v0_13_0) 37 | (out.log "Warning: 'pulp docs' now only supports 'purs' v0.13.0 and above. Please either update 'purs' or downgrade 'pulp'.") 38 | 39 | exec "purs" (["docs", "--compile-output", buildPath] <> args.remainder <> sources globInputFiles) Nothing 40 | 41 | out.log "Documentation generated." 42 | -------------------------------------------------------------------------------- /src/Pulp/Exec.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Exec 3 | ( exec 4 | , execQuiet 5 | , execWithStdio 6 | , execQuietWithStderr 7 | , execInteractive 8 | , psa 9 | , pursBuild 10 | , pursBundle 11 | ) where 12 | 13 | import Prelude 14 | 15 | import Data.Either (Either(..), either) 16 | import Data.Foldable (for_) 17 | import Data.Maybe (Maybe(..)) 18 | import Data.Posix.Signal (Signal(SIGTERM, SIGINT)) 19 | import Data.String (stripSuffix, Pattern(..)) 20 | import Effect.Aff (Aff, forkAff, makeAff, throwError) 21 | import Effect.Aff.AVar as Avar 22 | import Effect.Class (liftEffect) 23 | import Effect.Exception (error) 24 | import Foreign.Object (Object) 25 | import Node.ChildProcess as CP 26 | import Node.Platform (Platform(Win32)) 27 | import Node.Process as Process 28 | import Pulp.System.Stream (concatStream, stderr, write) 29 | import Unsafe.Coerce (unsafeCoerce) 30 | 31 | psa :: Array String -> Array String -> Maybe (Object String) -> Aff Unit 32 | psa = compiler "psa" 33 | 34 | pursBuild :: Array String -> Array String -> Maybe (Object String) -> Aff Unit 35 | pursBuild deps args = compiler "purs" deps (["compile"] <> args) 36 | 37 | compiler :: String -> Array String -> Array String -> Maybe (Object String) -> Aff Unit 38 | compiler name deps args env = 39 | execWithStdio inheritButOutToErr name (args <> deps) env 40 | where 41 | -- | Like Node.ChildProcess.inherit except the child process' standard output 42 | -- | is sent to Pulp's standard error. 43 | inheritButOutToErr = map Just 44 | [ CP.ShareStream (unsafeCoerce Process.stdin) 45 | , CP.ShareStream (unsafeCoerce Process.stderr) 46 | , CP.ShareStream (unsafeCoerce Process.stderr) 47 | ] 48 | 49 | pursBundle :: Array String -> Array String -> Maybe (Object String) -> Aff String 50 | pursBundle files args env = 51 | execQuiet "purs" (["bundle"] <> files <> args) env 52 | 53 | -- | Start a child process asynchronously, with the given command line 54 | -- | arguments and environment, and wait for it to exit. 55 | -- | On a non-zero exit code, throw an error. 56 | -- | 57 | -- | If the executable was not found and we are on Windows, retry with ".cmd" 58 | -- | appended. 59 | -- | 60 | -- | Stdout, stdin, and stderr of the child process are shared with the pulp 61 | -- | process (that is, data on stdin from pulp is relayed to the child process, 62 | -- | and any stdout and stderr from the child process are relayed back out by 63 | -- | pulp, which usually means they will immediately appear in the terminal). 64 | exec :: String -> Array String -> Maybe (Object String) -> Aff Unit 65 | exec = execWithStdio CP.inherit 66 | 67 | -- | Like exec, but allows you to supply your own StdIOBehaviour. 68 | execWithStdio :: Array (Maybe CP.StdIOBehaviour) -> String -> Array String -> Maybe (Object String) -> Aff Unit 69 | execWithStdio stdio cmd args env = do 70 | child <- liftEffect $ CP.spawn cmd args (def { env = env, stdio = stdio }) 71 | wait child >>= either (handleErrors cmd retry) onExit 72 | 73 | where 74 | def = CP.defaultSpawnOptions 75 | 76 | onExit exit = 77 | case exit of 78 | CP.Normally 0 -> 79 | pure unit 80 | _ -> 81 | throwError $ error $ 82 | "Subcommand terminated " <> showExit exit 83 | 84 | retry newCmd = execWithStdio stdio newCmd args env 85 | 86 | -- | Same as exec, except instead of relaying stdout immediately, it is 87 | -- | captured and returned as a String. 88 | execQuiet :: String -> Array String -> Maybe (Object String) -> Aff String 89 | execQuiet = 90 | execQuietWithStderr (CP.ShareStream (unsafeCoerce Process.stderr)) 91 | 92 | execQuietWithStderr :: CP.StdIOBehaviour -> String -> Array String -> Maybe (Object String) -> Aff String 93 | execQuietWithStderr stderrBehaviour cmd args env = do 94 | let stdio = [ Just (CP.ShareStream (unsafeCoerce Process.stdin)) -- stdin 95 | , Just CP.Pipe -- stdout 96 | , Just stderrBehaviour -- stderr 97 | ] 98 | child <- liftEffect $ CP.spawn cmd args (def { env = env, stdio = stdio }) 99 | outVar <- Avar.empty 100 | _ <- forkAff (concatStream (CP.stdout child) >>= \x -> Avar.put x outVar) 101 | wait child >>= either (handleErrors cmd retry) (onExit outVar) 102 | 103 | where 104 | def = CP.defaultSpawnOptions 105 | 106 | onExit outVar exit = 107 | Avar.take outVar >>= \childOut -> 108 | case exit of 109 | CP.Normally 0 -> 110 | pure childOut 111 | _ -> do 112 | write stderr childOut 113 | throwError $ error $ "Subcommand terminated " <> showExit exit 114 | 115 | retry newCmd = execQuietWithStderr stderrBehaviour newCmd args env 116 | 117 | -- | A version of `exec` which installs signal handlers to make sure that the 118 | -- | signals SIGINT and SIGTERM are relayed to the child process, if received. 119 | execInteractive :: String -> Array String -> Maybe (Object String) -> Aff Unit 120 | execInteractive cmd args env = do 121 | child <- liftEffect $ CP.spawn cmd args (def { env = env 122 | , stdio = CP.inherit }) 123 | liftEffect $ 124 | for_ [SIGTERM, SIGINT] \sig -> 125 | Process.onSignal sig 126 | (void (CP.kill sig child)) 127 | 128 | wait child >>= either (handleErrors cmd retry) (const (pure unit)) 129 | 130 | where 131 | def = CP.defaultSpawnOptions 132 | retry newCmd = exec newCmd args env 133 | 134 | -- | A slightly weird combination of `onError` and `onExit` into one. 135 | wait :: CP.ChildProcess -> Aff (Either CP.Error CP.Exit) 136 | wait child = makeAff \cb -> do 137 | let success = cb <<< Right 138 | CP.onExit child (success <<< Right) 139 | CP.onError child (success <<< Left) 140 | pure mempty 141 | 142 | showExit :: CP.Exit -> String 143 | showExit (CP.Normally x) = "with exit code " <> show x 144 | showExit (CP.BySignal sig) = "as a result of receiving " <> show sig 145 | 146 | handleErrors :: forall a. String -> (String -> Aff a) -> CP.Error -> Aff a 147 | handleErrors cmd retry err 148 | | err.code == "ENOENT" = do 149 | -- On windows, if the executable wasn't found, try adding .cmd 150 | if Process.platform == Just Win32 151 | then case stripSuffix (Pattern ".cmd") cmd of 152 | Nothing -> retry (cmd <> ".cmd") 153 | Just bareCmd -> throwError $ error $ 154 | "`" <> bareCmd <> "` executable not found. (nor `" <> cmd <> "`)" 155 | else 156 | throwError $ error $ 157 | "`" <> cmd <> "` executable not found." 158 | | otherwise = 159 | throwError (CP.toStandardError err) 160 | -------------------------------------------------------------------------------- /src/Pulp/Files.js: -------------------------------------------------------------------------------- 1 | // module Pulp.Files 2 | 3 | "use strict"; 4 | 5 | exports.globImpl = function glob$prime(pat, callback) { 6 | require("glob")(pat, {}, callback); 7 | }; 8 | -------------------------------------------------------------------------------- /src/Pulp/Files.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Files 2 | ( sources 3 | , ffis 4 | , localGlobs 5 | , testGlobs 6 | , dependencyGlobs 7 | , includeGlobs 8 | , defaultGlobs 9 | , outputModules 10 | , resolveGlobs 11 | , glob 12 | ) where 13 | 14 | import Prelude 15 | 16 | import Data.Array (concat, mapMaybe) 17 | import Data.Function.Uncurried (Fn2, runFn2) 18 | import Data.List as List 19 | import Data.Maybe (Maybe(..), fromMaybe) 20 | import Data.Set (Set) 21 | import Data.Set as Set 22 | import Data.String (stripSuffix, Pattern(..), split) 23 | import Data.Traversable (sequence, traverse) 24 | import Effect.Aff (Aff) 25 | import Foreign.Class (class Decode) 26 | import Node.Path as Path 27 | import Pulp.Args (Options) 28 | import Pulp.Args.Get (getOption, getOption') 29 | import Pulp.Exec (execQuiet) 30 | import Pulp.Project (usingPscPackage) 31 | import Pulp.System.FFI (Callback, runNode) 32 | 33 | recursiveGlobWithExtension :: String -> Set String -> Array String 34 | recursiveGlobWithExtension ext = 35 | Set.toUnfoldable >>> map (_ <> ("/**/*." <> ext)) 36 | 37 | sources :: Set String -> Array String 38 | sources = recursiveGlobWithExtension "purs" 39 | 40 | ffis :: Set String -> Array String 41 | ffis = recursiveGlobWithExtension "js" 42 | 43 | globsFromOption' :: forall a. Decode a => (a -> a) -> String -> Options -> Aff (Set a) 44 | globsFromOption' f name opts = do 45 | value <- getOption name opts 46 | pure $ case value of 47 | Just v -> Set.singleton (f v) 48 | Nothing -> Set.empty 49 | 50 | globsFromOption :: forall a. Decode a => String -> Options -> Aff (Set a) 51 | globsFromOption = globsFromOption' identity 52 | 53 | localGlobs :: Options -> Aff (Set String) 54 | localGlobs = globsFromOption "srcPath" 55 | 56 | testGlobs :: Options -> Aff (Set String) 57 | testGlobs = globsFromOption "testPath" 58 | 59 | dependencyGlobs :: Options -> Aff (Set String) 60 | dependencyGlobs opts = do 61 | p <- getOption' "_project" opts 62 | if usingPscPackage p 63 | then pscPackageGlobs 64 | else globsFromOption' (\path -> Path.concat [path, "purescript-*", "src"]) 65 | "dependencyPath" opts 66 | 67 | pscPackageGlobs :: Aff (Set String) 68 | pscPackageGlobs = 69 | execQuiet "psc-package" ["sources"] Nothing <#> processGlobs 70 | where 71 | -- Split on newlines and strip the /**/*/.purs suffixes just to 72 | -- append them later so it plays well with the other globs 73 | processGlobs = 74 | (split (Pattern "\r\n") >=> split (Pattern "\n")) >>> 75 | mapMaybe (stripSuffix (Pattern (Path.sep <> "**" <> Path.sep <> "*.purs"))) >>> 76 | Set.fromFoldable 77 | 78 | includeGlobs :: Options -> Aff (Set String) 79 | includeGlobs opts = mkSet <$> getOption "includePaths" opts 80 | where 81 | mkSet = Set.fromFoldable <<< fromMaybe [] 82 | 83 | defaultGlobs :: Options -> Aff (Set String) 84 | defaultGlobs opts = 85 | Set.unions <$> sequence (List.fromFoldable 86 | [ localGlobs opts 87 | , dependencyGlobs opts 88 | , includeGlobs opts 89 | ]) 90 | 91 | outputModules :: String -> Array String 92 | outputModules buildPath = 93 | [buildPath <> "/*/*.js"] 94 | 95 | resolveGlobs :: Array String -> Aff (Array String) 96 | resolveGlobs patterns = concat <$> traverse glob patterns 97 | 98 | foreign import globImpl :: Fn2 String (Callback (Array String)) Unit 99 | 100 | glob :: String -> Aff (Array String) 101 | glob pattern = runNode $ runFn2 globImpl pattern 102 | -------------------------------------------------------------------------------- /src/Pulp/Git.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Git 2 | ( requireCleanGitWorkingTree 3 | , getVersionFromGitTag 4 | , getLatestTaggedVersion 5 | , dropPrefix 6 | ) where 7 | 8 | import Prelude 9 | 10 | import Data.Array as Array 11 | import Data.Either (either) 12 | import Data.Foldable as Foldable 13 | import Data.Function (on) 14 | import Data.Maybe (Maybe(..), fromMaybe) 15 | import Data.String as String 16 | import Data.Tuple (Tuple(..), snd) 17 | import Data.Version (Version) 18 | import Data.Version as Version 19 | import Effect.Aff (Aff, attempt) 20 | import Node.ChildProcess as CP 21 | import Pulp.Exec (execQuiet, execQuietWithStderr) 22 | import Pulp.Utils (throw) 23 | 24 | -- | Throw an error if the git working tree is dirty. 25 | requireCleanGitWorkingTree :: Aff Unit 26 | requireCleanGitWorkingTree = do 27 | out <- execQuiet "git" ["status", "--porcelain"] Nothing 28 | if Foldable.all String.null (String.split (String.Pattern "\n") out) 29 | then pure unit 30 | else throw ("Your git working tree is dirty. Please commit or stash " <> 31 | "your changes first.") 32 | 33 | -- | Get the version tag pointing to the currently checked out commit, if any. 34 | -- | The tag must start with a "v" and be followed by a valid semver version, 35 | -- | for example "v1.2.3". 36 | -- | 37 | -- | If multiple tags point to the checked out commit, return the latest 38 | -- | version according to semver version comparison. 39 | getVersionFromGitTag :: Aff (Maybe (Tuple String Version)) 40 | getVersionFromGitTag = do 41 | output <- run "git" ["tag", "--points-at", "HEAD"] 42 | pure (maxVersion output) 43 | 44 | -- | Get the latest semver version tag in the repository. The tag must start 45 | -- | with a "v" and be followed by a valid semver version, for example 46 | -- | "v1.2.3". 47 | -- | 48 | -- | Returns Nothing if there are no such tags in the repository. 49 | getLatestTaggedVersion :: Aff (Maybe (Tuple String Version)) 50 | getLatestTaggedVersion = do 51 | output <- attempt $ run "git" ["describe", "--tags", "--abbrev=0", "HEAD"] 52 | pure $ either (const Nothing) maxVersion output 53 | 54 | -- | Run a command, piping stderr to /dev/null 55 | run :: String -> Array String -> Aff String 56 | run cmd args = execQuietWithStderr CP.Ignore cmd args Nothing 57 | 58 | -- | Given a number of lines of text, attempt to parse each line as a version, 59 | -- | and return the maximum. 60 | maxVersion :: String -> Maybe (Tuple String Version) 61 | maxVersion = 62 | String.split (String.Pattern "\n") 63 | >>> Array.mapMaybe (String.trim >>> parseMay) 64 | >>> Foldable.maximumBy (compare `on` snd) 65 | 66 | where 67 | parseMay str = 68 | str 69 | # dropPrefix "v" 70 | # Version.parseVersion 71 | # either (const Nothing) Just 72 | # map (Tuple str) 73 | 74 | dropPrefix :: String -> String -> String 75 | dropPrefix prefix str = 76 | fromMaybe str (String.stripPrefix (String.Pattern prefix) str) 77 | -------------------------------------------------------------------------------- /src/Pulp/Init.js: -------------------------------------------------------------------------------- 1 | // module Pulp.Init 2 | "use strict"; 3 | 4 | exports.bowerFile = function bowerFile(name) { 5 | return JSON.stringify({ 6 | name: name, 7 | ignore: [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "output" 12 | ], 13 | dependencies: { 14 | }, 15 | devDependencies: { 16 | }, 17 | }, null, 2) + "\n"; 18 | }; 19 | -------------------------------------------------------------------------------- /src/Pulp/Init.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Init 2 | ( action 3 | ) where 4 | 5 | import Prelude 6 | 7 | import Control.Monad.Error.Class (throwError) 8 | import Data.Array (cons) 9 | import Data.Foldable (for_) 10 | import Data.Maybe (Maybe(..)) 11 | import Data.String (joinWith) 12 | import Effect.Aff (Aff) 13 | import Effect.Class (liftEffect) 14 | import Effect.Exception (error) 15 | import Node.Encoding (Encoding(UTF8)) 16 | import Node.FS.Aff (writeTextFile, exists) 17 | import Node.Path as Path 18 | import Node.Process as Process 19 | import Pulp.Args (Action(..)) 20 | import Pulp.Args.Get (getFlag) 21 | import Pulp.Outputter (Outputter, getOutputter) 22 | import Pulp.PackageManager (launchBower, launchPscPackage) 23 | import Pulp.System.Files (mkdirIfNotExist) 24 | import Pulp.Utils (throw) 25 | import Pulp.Validate (dropPreRelBuildMeta, failIfUsingEsModulesPsVersion, getPursVersion) 26 | import Pulp.Versions.PureScript (psVersions) 27 | 28 | foreign import bowerFile :: String -> String 29 | 30 | data InitStyle = Bower | PscPackage 31 | 32 | data EffOrEffect = UseEff | UseEffect 33 | 34 | unlines :: Array String -> String 35 | unlines arr = joinWith "\n" arr <> "\n" 36 | 37 | gitignore :: String 38 | gitignore = unlines [ 39 | "/bower_components/", 40 | "/node_modules/", 41 | "/.pulp-cache/", 42 | "/output/", 43 | "/generated-docs/", 44 | "/.psc-package/", 45 | "/.psc*", 46 | "/.purs*", 47 | "/.psa*" 48 | ] 49 | 50 | pursReplFile :: String 51 | pursReplFile = unlines [ 52 | "import Prelude" 53 | ] 54 | 55 | mainFile :: EffOrEffect -> String 56 | mainFile = case _ of 57 | UseEffect -> unlines [ 58 | "module Main where", 59 | "", 60 | "import Prelude", 61 | "import Effect (Effect)", 62 | "import Effect.Console (log)", 63 | "", 64 | "main :: Effect Unit", 65 | "main = do", 66 | " log \"Hello sailor!\"" 67 | ] 68 | UseEff -> unlines [ 69 | "module Main where", 70 | "", 71 | "import Prelude", 72 | "import Control.Monad.Eff (Eff)", 73 | "import Control.Monad.Eff.Console (CONSOLE, log)", 74 | "", 75 | "main :: forall e. Eff (console :: CONSOLE | e) Unit", 76 | "main = do", 77 | " log \"Hello sailor!\"" 78 | ] 79 | 80 | testFile :: EffOrEffect -> String 81 | testFile = case _ of 82 | UseEffect -> unlines [ 83 | "module Test.Main where", 84 | "", 85 | "import Prelude", 86 | "import Effect (Effect)", 87 | "import Effect.Console (log)", 88 | "", 89 | "main :: Effect Unit", 90 | "main = do", 91 | " log \"You should add some tests.\"" 92 | ] 93 | UseEff -> unlines [ 94 | "module Test.Main where", 95 | "", 96 | "import Prelude", 97 | "import Control.Monad.Eff (Eff)", 98 | "import Control.Monad.Eff.Console (CONSOLE, log)", 99 | "", 100 | "main :: forall e. Eff (console :: CONSOLE | e) Unit", 101 | "main = do", 102 | " log \"You should add some tests.\"" 103 | ] 104 | 105 | projectFiles :: InitStyle -> EffOrEffect -> String -> String -> Array { path :: String, content :: String } 106 | projectFiles initStyle effOrEffect pathRoot projectName = 107 | case initStyle of 108 | Bower -> cons bowerJson common 109 | PscPackage -> common 110 | where 111 | fullPath pathParts = Path.concat ([pathRoot] <> pathParts) 112 | bowerJson = { path: fullPath ["bower.json"], content: bowerFile projectName } 113 | common = [ { path: fullPath [".gitignore"], content: gitignore } 114 | , { path: fullPath [".purs-repl"], content: pursReplFile } 115 | , { path: fullPath ["src", "Main.purs"], content: mainFile effOrEffect } 116 | , { path: fullPath ["test", "Main.purs"], content: testFile effOrEffect } 117 | ] 118 | 119 | init :: InitStyle -> EffOrEffect -> Boolean -> Outputter -> Aff Unit 120 | init initStyle effOrEffect force out = do 121 | cwd <- liftEffect Process.cwd 122 | let projectName = Path.basename cwd 123 | out.log $ "Generating project skeleton in " <> cwd 124 | 125 | let files = projectFiles initStyle effOrEffect cwd projectName 126 | 127 | when (not force) do 128 | for_ files \f -> do 129 | fileExists <- exists f.path 130 | when fileExists do 131 | throwError <<< error $ "Found " <> f.path <> ": " 132 | <> "There's already a project here. Run `pulp init --force` " 133 | <> "if you're sure you want to overwrite it." 134 | 135 | for_ files \f -> do 136 | let dir = Path.dirname f.path 137 | when (dir /= cwd) (mkdirIfNotExist dir) 138 | writeTextFile UTF8 f.path f.content 139 | 140 | psVer <- getPursVersion out 141 | 142 | install initStyle effOrEffect (getDepsVersions $ dropPreRelBuildMeta psVer) 143 | 144 | where 145 | install Bower UseEff p = do 146 | failIfUsingEsModulesPsVersion out $ Just 147 | "'purescript-eff' has been archived, so the FFI's CJS modules cannot be migrated to ES modules." 148 | launchBower ["install", "--save", p.prelude, p.console] 149 | launchBower ["install", "--save-dev", p.psciSupport] 150 | 151 | install Bower UseEffect p = do 152 | launchBower ["install", "--save", p.prelude, p.console, p.effect ] 153 | launchBower ["install", "--save-dev", p.psciSupport ] 154 | 155 | install PscPackage UseEff _ = do 156 | failIfUsingPscPackageAndEsModules 157 | 158 | launchPscPackage ["init"] 159 | launchPscPackage ["install", "eff"] 160 | launchPscPackage ["install", "console"] 161 | launchPscPackage ["install", "psci-support"] 162 | 163 | install PscPackage UseEffect _ = do 164 | failIfUsingPscPackageAndEsModules 165 | 166 | launchPscPackage ["init"] 167 | launchPscPackage ["install", "effect"] 168 | launchPscPackage ["install", "console"] 169 | launchPscPackage ["install", "psci-support"] 170 | 171 | failIfUsingPscPackageAndEsModules = do 172 | failIfUsingEsModulesPsVersion out $ Just 173 | "'psc-package' not yet supported on a `purs` version that compiles to ES modules." 174 | 175 | getDepsVersions v 176 | | v >= psVersions.v0_15_0 = 177 | { prelude: "purescript-prelude@v6.0.0" 178 | , console: "purescript-console@v6.0.0" 179 | , effect: "purescript-effect@v4.0.0" 180 | , psciSupport: "purescript-psci-support@v6.0.0" 181 | } 182 | | v >= psVersions.v0_14_0 = 183 | { prelude: "purescript-prelude@v5.0.1" 184 | , console: "purescript-console@v5.0.0" 185 | , effect: "purescript-effect@v3.0.0" 186 | , psciSupport: "purescript-psci-support@v5.0.0" 187 | } 188 | | v >= psVersions.v0_13_0 = 189 | { prelude: "purescript-prelude@v4.1.1" 190 | , console: "purescript-console@v4.4.0" 191 | , effect: "purescript-effect@v2.0.1" 192 | , psciSupport: "purescript-psci-support@v4.0.0" 193 | } 194 | | otherwise = 195 | { prelude: "purescript-prelude@v4.1.1" 196 | , console: "purescript-console@v4.4.0" 197 | , effect: "purescript-effect@v2.0.1" 198 | , psciSupport: "purescript-psci-support@v4.0.0" 199 | } 200 | 201 | action :: Action 202 | action = Action \args -> do 203 | force <- getFlag "force" args.commandOpts 204 | pscPackage <- getFlag "pscPackage" args.globalOpts 205 | withEff <- getFlag "withEff" args.commandOpts 206 | withEffect <- getFlag "withEffect" args.commandOpts 207 | out <- getOutputter args 208 | effOrEffect <- getEffOrEffect out withEff withEffect 209 | 210 | if withEff && withEffect 211 | then throw "Cannot specify both --with-eff and --with-effect. Please choose one and try again." 212 | else init (if pscPackage then PscPackage else Bower) effOrEffect force out 213 | 214 | where 215 | 216 | minEffectVersion = psVersions.v0_12_0 217 | 218 | getEffOrEffect out withEff withEffect 219 | | withEff = pure UseEff 220 | | withEffect = pure UseEffect 221 | | otherwise = do 222 | ver <- getPursVersion out 223 | if (dropPreRelBuildMeta ver) < minEffectVersion 224 | then pure UseEff 225 | else pure UseEffect 226 | -------------------------------------------------------------------------------- /src/Pulp/Login.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Login 2 | ( action 3 | , tokenFilePath 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Control.Monad.Error.Class (throwError) 9 | import Control.Monad.Except (runExcept) 10 | import Data.Either (Either(..)) 11 | import Data.Foldable (fold) 12 | import Data.Maybe (Maybe(..)) 13 | import Data.Options ((:=)) 14 | import Data.String as String 15 | import Data.Tuple.Nested ((/\)) 16 | import Effect.Aff (Aff) 17 | import Effect.Class (liftEffect) 18 | import Effect.Exception (error) 19 | import Foreign (readString) 20 | import Foreign.Index (readProp) 21 | import Foreign.JSON (parseJSON) 22 | import Foreign.Object as Object 23 | import Node.Encoding (Encoding(..)) 24 | import Node.FS.Aff as FS 25 | import Node.FS.Perms (mkPerms, none, read, write) 26 | import Node.HTTP.Client as HTTP 27 | import Node.Path as Path 28 | import Node.Platform (Platform(Win32)) 29 | import Node.Process as Process 30 | import Pulp.Args (Action(..)) 31 | import Pulp.Outputter (Outputter, getOutputter) 32 | import Pulp.System.Files (mkdirIfNotExist) 33 | import Pulp.System.HTTP (httpRequest) 34 | import Pulp.System.Read as Read 35 | import Pulp.System.Stream (concatStream) 36 | import Pulp.Version as PulpVersion 37 | 38 | -- TODO: Obtain tokens automatically after prompting for a username and 39 | -- password. 40 | -- 41 | -- Unfortunately it is not easy to do this without exposing the client secret, 42 | -- so I think we need to add a route to Pursuit itself to support this, so that 43 | -- Pursuit sort of proxies to GitHub and adds its client secret itself. 44 | 45 | action :: Action 46 | action = Action \args -> do 47 | out <- getOutputter args 48 | 49 | token <- obtainTokenFromStdin out 50 | checkToken out token 51 | 52 | writeTokenFile token 53 | 54 | obtainTokenFromStdin :: Outputter -> Aff String 55 | obtainTokenFromStdin out = do 56 | out.write "Please obtain a GitHub personal access token at:\n" 57 | out.write " https://github.com/settings/tokens/new\n" 58 | out.write "No scopes are required, so don't check any of the boxes.\n" 59 | out.write "\n" 60 | String.trim <$> Read.read 61 | { prompt: "After you've done that, paste it in here: " 62 | , silent: true 63 | } 64 | 65 | checkToken :: Outputter -> String -> Aff Unit 66 | checkToken out token = do 67 | res <- httpRequest reqOptions Nothing 68 | 69 | let statusCode = HTTP.statusCode res 70 | resBody <- concatStream (HTTP.responseAsStream res) 71 | unless (statusCode == 200) $ 72 | throwError (error case statusCode of 73 | 401 -> 74 | "Your token was not accepted (401 Unauthorized)." 75 | other -> 76 | let 77 | header = 78 | "Something went wrong (HTTP " <> show other <> " " <> 79 | HTTP.statusMessage res <> ")." 80 | in 81 | header <> "\n" <> resBody) 82 | 83 | case runExcept (parseJSON resBody >>= readProp "login" >>= readString) of 84 | Right login' -> 85 | out.write ("Successfully authenticated as " <> login' <> ".\n") 86 | Left err -> 87 | throwError (error ("Unexpected response from GitHub API: " <> show err)) 88 | 89 | where 90 | reqOptions = fold 91 | [ HTTP.protocol := "https:" 92 | , HTTP.hostname := "api.github.com" 93 | , HTTP.path := "/user" 94 | , HTTP.headers := HTTP.RequestHeaders (Object.fromFoldable 95 | [ "Accept" /\ "application/vnd.github.v3+json" 96 | , "Authorization" /\ ("token " <> token) 97 | , "User-Agent" /\ ("Pulp-" <> PulpVersion.versionString) 98 | ]) 99 | ] 100 | 101 | writeTokenFile :: String -> Aff Unit 102 | writeTokenFile token = do 103 | filepath <- tokenFilePath 104 | mkdirIfNotExist (Path.dirname filepath) 105 | FS.writeTextFile UTF8 filepath token 106 | FS.chmod filepath (mkPerms (read + write) none none) 107 | 108 | tokenFilePath :: Aff String 109 | tokenFilePath = 110 | (<>) <$> getHome <*> pure "/.pulp/github-oauth-token" 111 | 112 | getHome :: Aff String 113 | getHome = do 114 | let homeVar = if Process.platform == Just Win32 then "USERPROFILE" else "HOME" 115 | home <- liftEffect (Process.lookupEnv homeVar) 116 | case home of 117 | Just h -> 118 | pure h 119 | Nothing -> 120 | throwError (error ( 121 | "The " <> homeVar <> " environment variable is not set.")) 122 | -------------------------------------------------------------------------------- /src/Pulp/Outputter.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Outputter 2 | ( Outputter() 3 | , getOutputter 4 | , makeOutputter 5 | ) where 6 | 7 | import Prelude 8 | 9 | import Ansi.Codes (Color(..)) 10 | import Ansi.Output (withGraphics, foreground, bold) 11 | import Effect.Aff (Aff) 12 | import Pulp.Args (Args) 13 | import Pulp.Args.Get (getFlag) 14 | import Pulp.System.Stream (write, WritableStream, stderr) 15 | import Pulp.System.SupportsColor as Color 16 | 17 | type Outputter = 18 | { log :: String -> Aff Unit 19 | , debug :: String -> Aff Unit 20 | , err :: String -> Aff Unit 21 | , write :: String -> Aff Unit 22 | , bolded :: String -> Aff Unit 23 | , monochrome :: Boolean 24 | } 25 | 26 | -- | Get an outputter, with monochrome based on the command line arguments. 27 | getOutputter :: Args -> Aff Outputter 28 | getOutputter args = do 29 | -- Bit of a hack, this is used in `pulp server` 30 | q <- getFlag "_silenced" args.commandOpts 31 | if q 32 | then pure nullOutputter 33 | else makeOutputter <$> getFlag "monochrome" args.globalOpts <*> getFlag "debug" args.globalOpts 34 | 35 | -- | Get an outputter. The argument represents "monochrome"; if true is 36 | -- | supplied, the returned logger will never use color. Otherwise, whether or 37 | -- | not colour is used depends on the "supports-color" module. Note that the 38 | -- | `monochrome` attribute of the returned outputter might not necessarily 39 | -- | be the same as the argument supplied. 40 | makeOutputter :: Boolean -> Boolean -> Outputter 41 | makeOutputter monochrome enableDebug = 42 | if not monochrome && Color.hasBasic 43 | then ansiOutputter enableDebug 44 | else monochromeOutputter enableDebug 45 | 46 | monochromeOutputter :: Boolean -> Outputter 47 | monochromeOutputter enableDebug = 48 | { log: monobullet 49 | , debug: if enableDebug then monobullet else dud 50 | , err: monobullet 51 | , write: write stderr 52 | , bolded: write stderr 53 | , monochrome: true 54 | } 55 | where 56 | monobullet text = write stderr ("* " <> text <> "\n") 57 | 58 | ansiOutputter :: Boolean -> Outputter 59 | ansiOutputter enableDebug = 60 | { log: bullet stderr Green 61 | , debug: if enableDebug then bullet stderr Yellow else dud 62 | , err: bullet stderr Red 63 | , write: write stderr 64 | , bolded: write stderr <<< withGraphics bold 65 | , monochrome: false 66 | } 67 | 68 | bullet :: WritableStream -> Color -> String -> Aff Unit 69 | bullet stream color text = do 70 | write stream (withGraphics (foreground color) "* ") 71 | write stream (text <> "\n") 72 | 73 | -- | An outputter which doesn't ever output anything. 74 | nullOutputter :: Outputter 75 | nullOutputter = 76 | { log: dud 77 | , debug: dud 78 | , err: dud 79 | , write: dud 80 | , bolded: dud 81 | , monochrome: false 82 | } 83 | 84 | dud :: String -> Aff Unit 85 | dud = const (pure unit) 86 | -------------------------------------------------------------------------------- /src/Pulp/PackageManager.purs: -------------------------------------------------------------------------------- 1 | module Pulp.PackageManager 2 | ( launchBower 3 | , launchPscPackage 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Control.Monad.Error.Class (throwError) 9 | import Data.Either (either) 10 | import Data.Maybe (Maybe(..)) 11 | import Effect.Aff (Aff, attempt) 12 | import Effect.Exception (error) 13 | import Pulp.Exec (exec) 14 | import Pulp.System.Which (which) 15 | 16 | run :: String -> String -> Array String -> Aff Unit 17 | run execName errorMsg args = do 18 | executable <- attempt $ which execName 19 | either 20 | (const $ throwError $ error $ errorMsg') 21 | (\e -> exec e args Nothing) 22 | executable 23 | where errorMsg' = "No `" <> execName <> "` executable found.\n\n" <> errorMsg 24 | 25 | launchBower :: Array String -> Aff Unit 26 | launchBower = run "bower" """Pulp no longer bundles Bower. You'll need to install it manually: 27 | 28 | $ npm install -g bower 29 | """ 30 | 31 | launchPscPackage :: Array String -> Aff Unit 32 | launchPscPackage = do 33 | run "psc-package" "Install psc-package from: https://github.com/purescript/psc-package" 34 | -------------------------------------------------------------------------------- /src/Pulp/Project.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Project 2 | ( Project(..) 3 | , getProject 4 | , usingPscPackage 5 | ) where 6 | 7 | import Prelude 8 | 9 | import Control.Alt ((<|>)) 10 | import Control.Monad.Error.Class (throwError) 11 | import Control.Monad.Except (runExcept) 12 | import Data.Either (Either(..)) 13 | import Data.Maybe (Maybe(..), maybe) 14 | import Effect.Aff (Aff) 15 | import Effect.Class (liftEffect) 16 | import Effect.Exception (error) 17 | import Foreign (Foreign, readString) 18 | import Foreign.Class (class Decode) 19 | import Foreign.Index (readProp) 20 | import Foreign.JSON (parseJSON) 21 | import Node.Encoding (Encoding(UTF8)) 22 | import Node.FS.Aff (exists, readTextFile) 23 | import Node.Path as P 24 | import Node.Process as Process 25 | import Pulp.Args (Options) 26 | import Pulp.Args.Get (getOption, getFlag) 27 | import Pulp.System.Files (mkdirIfNotExist) 28 | 29 | newtype Project = Project 30 | { projectFile :: Foreign 31 | , path :: String 32 | , cache :: String 33 | } 34 | 35 | -- | Attempt to find a file in the given directory or any parent of it. 36 | findIn :: String -> String -> Aff (Maybe String) 37 | findIn path file = do 38 | let fullPath = P.concat [path, file] 39 | doesExist <- exists fullPath 40 | 41 | if doesExist 42 | then pure (Just fullPath) 43 | else 44 | let parent = P.dirname path 45 | in if path == parent 46 | then pure Nothing 47 | else findIn parent file 48 | 49 | -- | Read a project's bower file at the given path and construct a Project 50 | -- | value. 51 | readConfig :: String -> Aff Project 52 | readConfig configFilePath = do 53 | json <- readTextFile UTF8 configFilePath 54 | case runExcept (parseJSON json) of 55 | Left err -> 56 | throwError (error ("Unable to parse " <> (P.basename configFilePath) <> ": " <> show err)) 57 | Right pro -> do 58 | let path = P.dirname configFilePath 59 | cachePath <- liftEffect $ P.resolve [path] ".pulp-cache" 60 | liftEffect $ Process.chdir path 61 | mkdirIfNotExist cachePath 62 | pure $ Project { projectFile: pro, cache: cachePath, path: path } 63 | 64 | -- | If project file has a `set` property we assume it's a psc-package project file 65 | usingPscPackage :: Project -> Boolean 66 | usingPscPackage (Project p) = 67 | case runExcept (readProp "set" p.projectFile >>= readString) of 68 | Right _ -> true 69 | _ -> false 70 | 71 | -- | Use the provided project file, or if it is Nothing, try to find a project file 72 | -- | path in this or any parent directory, with Bower taking precedence over psc-package. 73 | getProjectFile :: Maybe String -> Aff String 74 | getProjectFile = maybe search pure 75 | where 76 | search = do 77 | cwd <- liftEffect Process.cwd 78 | mbowerFile <- findIn cwd "bower.json" 79 | mpscPackageFile <- findIn cwd "psc-package.json" 80 | case mbowerFile <|> mpscPackageFile of 81 | Just file -> pure file 82 | Nothing -> throwError <<< error $ 83 | "No bower.json or psc-package.json found in current or parent directories. Are you in a PureScript project?" 84 | 85 | getProject :: Options -> Aff Project 86 | getProject args = do 87 | bower <- getOption "bowerFile" args 88 | pscPackageFlag <- getFlag "pscPackage" args 89 | let pscPackage = if pscPackageFlag then Just "psc-package.json" else Nothing 90 | getProjectFile (bower <|> pscPackage) >>= readConfig 91 | 92 | instance decodeProject :: Decode Project where 93 | decode o = 94 | map Project $ do 95 | projectFile <- readProp "projectFile" o 96 | path <- readProp "path" o >>= readString 97 | cache <- readProp "cache" o >>= readString 98 | pure $ { projectFile, path, cache } 99 | -------------------------------------------------------------------------------- /src/Pulp/Publish.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Publish ( action, resolutionsFile, BowerJson, parseJsonFile ) where 2 | 3 | import Prelude 4 | 5 | import Control.MonadPlus (guard) 6 | import Control.Parallel (parTraverse) 7 | import Data.Array as Array 8 | import Data.Either (Either(..)) 9 | import Data.Foldable (fold, or) 10 | import Data.Maybe (Maybe(..), maybe) 11 | import Data.Options ((:=)) 12 | import Data.String as String 13 | import Data.Tuple (Tuple(..)) 14 | import Data.Tuple.Nested ((/\)) 15 | import Data.Version (Version) 16 | import Data.Version as Version 17 | import Effect.Aff (Aff, attempt, throwError) 18 | import Effect.Class (liftEffect) 19 | import Foreign (renderForeignError) 20 | import Foreign.Object (Object) 21 | import Foreign.Object as Object 22 | import Node.Buffer (Buffer) 23 | import Node.Buffer as Buffer 24 | import Node.Encoding (Encoding(..)) 25 | import Node.FS.Aff (readTextFile) 26 | import Node.FS.Aff as FS 27 | import Node.HTTP.Client as HTTP 28 | import Node.Path as Path 29 | import Node.Stream (pipe) 30 | import Pulp.Args (Action(..), Args) 31 | import Pulp.Args.Get (getFlag, getOption') 32 | import Pulp.Exec (exec, execQuiet) 33 | import Pulp.Git (getVersionFromGitTag, requireCleanGitWorkingTree) 34 | import Pulp.Login (tokenFilePath) 35 | import Pulp.Outputter (Outputter, getOutputter) 36 | import Pulp.System.Files (isENOENT, openTemp) 37 | import Pulp.System.HTTP (httpRequest) 38 | import Pulp.System.Read as Read 39 | import Pulp.System.Stream (concatStream, concatStreamToBuffer, createGzip, streamFromString) 40 | import Pulp.Utils (orErr, throw) 41 | import Pulp.Validate (dropPreRelBuildMeta, getPursVersion) 42 | import Pulp.Versions.PureScript (psVersions) 43 | import Simple.JSON as SimpleJSON 44 | 45 | -- TODO: 46 | -- * Check that the 'origin' remote matches with bower.json 47 | -- * Better handling for the situation where the person running 'pulp publish' 48 | -- doesn't actually own the repo. 49 | 50 | action :: Action 51 | action = Action \args -> do 52 | out <- getOutputter args 53 | out.debug "Checking bower project" 54 | checkBowerProject 55 | 56 | out.debug "Checking clean git working tree" 57 | requireCleanGitWorkingTree 58 | out.debug "Getting auth token" 59 | authToken <- readTokenFile 60 | out.debug "Parsing bower.json file" 61 | manifest :: BowerJson <- parseJsonFile "bower.json" 62 | out.debug $ "Parsed manifest:\n" <> show manifest 63 | 64 | out.debug "Getting resolutions file..." 65 | resolutionsPath <- resolutionsFile manifest args 66 | content <- readTextFile UTF8 resolutionsPath 67 | out.debug $ "Resolutions file:\n" <> content 68 | gzippedJson <- pursPublish resolutionsPath >>= gzip 69 | 70 | out.debug "Getting repo url" 71 | repoUrl <- map _.url manifest.repository # orErr "'repository' key not present in bower.json" 72 | out.debug "Verifying that repo is registered under url" 73 | checkRegistered out manifest.name repoUrl 74 | 75 | out.debug "Getting version" 76 | Tuple tagStr tagVersion <- getVersion 77 | confirm ("Publishing " <> manifest.name <> " at v" <> Version.showVersion tagVersion <> ". Is this ok?") 78 | 79 | noPush <- getFlag "noPush" args.commandOpts 80 | unless noPush do 81 | remote <- getOption' "pushTo" args.commandOpts 82 | confirmRun out "git" ["push", remote, "HEAD", "refs/tags/" <> tagStr] 83 | 84 | out.log "Uploading documentation to Pursuit..." 85 | uploadPursuitDocs out authToken gzippedJson 86 | 87 | out.log "Done." 88 | out.log ("You can view your package's documentation at: " <> 89 | pursuitUrl manifest.name tagVersion) 90 | 91 | where 92 | getVersion = 93 | getVersionFromGitTag 94 | >>= maybe (throw ( 95 | "Internal error: No version could be extracted from the git tags" 96 | <> " in this repository. This should not have happened. Please" 97 | <> " report this: https://github.com/bodil/pulp/issues/new")) 98 | pure 99 | 100 | checkBowerProject :: Aff Unit 101 | checkBowerProject = do 102 | bower <- FS.exists "bower.json" 103 | if bower then pure unit 104 | else throw ("For the time being, libraries should be installable with Bower" 105 | <> " before being submitted to Pursuit. Please create a " 106 | <> " bower.json file first.") 107 | 108 | checkRegistered :: Outputter -> String -> String -> Aff Unit 109 | checkRegistered out pkgName repoUrl = do 110 | out.write "Checking your package is registered in purescript/registry... " 111 | bowerPkgs <- get "bower-packages.json" >>= parseJsonText "registry bower-packages.json" 112 | newPkgs <- get "new-packages.json" >>= parseJsonText "registry new-packages.json" 113 | case Object.lookup pkgName (Object.union bowerPkgs newPkgs) of 114 | Just repoUrl' -> do 115 | if (packageUrlIsEqual repoUrl repoUrl') 116 | then out.write "ok\n" 117 | else do 118 | out.write "\n" 119 | out.err $ 120 | "A package with the name " 121 | <> pkgName 122 | <> " already exists in the registry, but the repository urls did not match." 123 | out.err "Repository url in your bower.json file:" 124 | out.err $ " " <> repoUrl 125 | out.err "Repository url in the registry:" 126 | out.err $ " " <> repoUrl' 127 | out.err "Please make sure these urls match." 128 | throw "Package repository url mismatch" 129 | Nothing -> do 130 | out.write "\n" 131 | out.err $ 132 | "No package with the name " 133 | <> pkgName 134 | <> " exists in the registry." 135 | out.err $ 136 | "Please register your package by sending a PR to purescript/registry first, adding your package to `new-packages.json`" 137 | throw "Package not registered" 138 | 139 | where 140 | get :: String -> Aff String 141 | get filepath = do 142 | let 143 | reqOptions = fold 144 | [ HTTP.method := "GET" 145 | , HTTP.protocol := "https:" 146 | , HTTP.hostname := "raw.githubusercontent.com" 147 | , HTTP.path := ("/purescript/registry/master/" <> filepath) 148 | ] 149 | res <- httpRequest reqOptions Nothing 150 | case HTTP.statusCode res of 151 | 200 -> 152 | concatStream (HTTP.responseAsStream res) 153 | other -> do 154 | let msg = "Unable to fetch file " <> filepath <> " from purescript/registry" 155 | out.err msg 156 | out.err ("HTTP " <> show other <> " " <> HTTP.statusMessage res) 157 | out.err =<< concatStream (HTTP.responseAsStream res) 158 | throw msg 159 | 160 | -- | Like normal string equality, except also allow cases where one is the same 161 | -- | as the other except for a trailing ".git". 162 | packageUrlIsEqual :: String -> String -> Boolean 163 | packageUrlIsEqual a b = 164 | or 165 | [ a == b 166 | , a <> ".git" == b 167 | , a == b <> ".git" 168 | ] 169 | 170 | gzip :: String -> Aff Buffer 171 | gzip str = do 172 | inputStream <- liftEffect $ streamFromString str 173 | gzipStream <- liftEffect createGzip 174 | _ <- liftEffect $ inputStream `pipe` gzipStream 175 | concatStreamToBuffer gzipStream 176 | 177 | -- Format for actual bower.json files, written by project maintainers. This 178 | -- type synonym only contains the fields we care about. 179 | type BowerJson = 180 | { name :: String 181 | , dependencies :: Maybe (Object String) 182 | , devDependencies :: Maybe (Object String) 183 | , repository :: 184 | Maybe { url :: String 185 | , type :: String 186 | } 187 | } 188 | 189 | -- Format for .bower.json files written automatically by Bower inside 190 | -- subdirectories of bower_components. This type synonym only contains the 191 | -- fields we care about for extracting the necessary information for passing on 192 | -- to `purs publish`. 193 | type InstalledBowerJson = 194 | { name :: String 195 | , version :: String 196 | , _resolution :: 197 | { type :: String 198 | } 199 | } 200 | 201 | -- | Create a resolutions file, using the new format where the installed 202 | -- | version of `purs` is recent enough to be able to understand it, and using 203 | -- | the legacy format otherwise. Returns the created file path. 204 | resolutionsFile :: BowerJson -> Args -> Aff String 205 | resolutionsFile manifest args = do 206 | out <- getOutputter args 207 | ver <- getPursVersion out 208 | resolutionsData <- 209 | if (dropPreRelBuildMeta ver) >= psVersions.v0_12_4 210 | then do 211 | let hasDependencies = 212 | (maybe false (not <<< Object.isEmpty) $ manifest.dependencies) 213 | || (maybe false (not <<< Object.isEmpty) $ manifest.devDependencies) 214 | dependencyPath <- getOption' "dependencyPath" args.commandOpts 215 | getResolutions hasDependencies dependencyPath 216 | else 217 | getResolutionsLegacy 218 | writeResolutionsFile resolutionsData 219 | 220 | -- Obtain resolutions information for a Bower project as a string containing 221 | -- JSON, using the new format. 222 | getResolutions :: Boolean -> String -> Aff String 223 | getResolutions hasDeps dependencyPath = do 224 | serializeResolutions <$> 225 | if hasDeps 226 | then getResolutionsBower dependencyPath 227 | else pure [] 228 | 229 | -- Obtain resolutions information for a Bower project. If a dependency has been 230 | -- installed in a non-standard way, e.g. via a particular branch or commit 231 | -- rather than a published version, the `version` field for that package in the 232 | -- result will be Nothing. 233 | getResolutionsBower :: 234 | String -> 235 | Aff 236 | (Array 237 | { packageName :: String 238 | , version :: Maybe String 239 | , path :: String 240 | }) 241 | getResolutionsBower dependencyPath = do 242 | dependencyDirs <- FS.readdir dependencyPath 243 | flip parTraverse dependencyDirs \dir -> do 244 | pkgInfo :: InstalledBowerJson <- 245 | parseJsonFile (Path.concat [dependencyPath, dir, ".bower.json"]) 246 | let 247 | packageName = 248 | pkgInfo.name 249 | version = 250 | guard (pkgInfo._resolution."type" == "version") 251 | *> Just pkgInfo.version 252 | path = 253 | dependencyPath <> Path.sep <> dir 254 | pure 255 | { packageName 256 | , version 257 | , path 258 | } 259 | 260 | serializeResolutions :: 261 | Array 262 | { packageName :: String 263 | , version :: Maybe String 264 | , path :: String 265 | } -> 266 | String 267 | serializeResolutions rs = 268 | let 269 | toKeyValuePair { packageName, version, path } = 270 | Tuple packageName { version, path } 271 | obj = 272 | Object.fromFoldable (map toKeyValuePair rs) 273 | in 274 | SimpleJSON.writeJSON obj 275 | 276 | getResolutionsLegacy :: Aff String 277 | getResolutionsLegacy = 278 | execQuiet "bower" ["list", "--json", "--offline"] Nothing 279 | 280 | writeResolutionsFile :: String -> Aff String 281 | writeResolutionsFile resolutionsContents = do 282 | info <- openTemp { prefix: "pulp-publish", suffix: ".json" } 283 | _ <- FS.fdAppend info.fd =<< liftEffect (Buffer.fromString resolutionsContents UTF8) 284 | _ <- FS.fdClose info.fd 285 | pure info.path 286 | 287 | pursPublish :: String -> Aff String 288 | pursPublish resolutionsPath = 289 | execQuiet 290 | "purs" 291 | ["publish", "--manifest", "bower.json", "--resolutions", resolutionsPath] 292 | Nothing 293 | 294 | confirmRun :: Outputter -> String -> Array String -> Aff Unit 295 | confirmRun out cmd args = do 296 | out.log "About to execute:" 297 | out.write ("> " <> cmd <> " " <> String.joinWith " " args <> "\n") 298 | confirm "Ok?" 299 | exec cmd args Nothing 300 | 301 | confirm :: String -> Aff Unit 302 | confirm q = do 303 | answer <- Read.read { prompt: q <> " [y/n] ", silent: false } 304 | case String.trim (String.toLower answer) of 305 | "y" -> 306 | pure unit 307 | _ -> 308 | throw "Aborted" 309 | 310 | readTokenFile :: Aff String 311 | readTokenFile = do 312 | path <- tokenFilePath 313 | r <- attempt (FS.readTextFile UTF8 path) 314 | case r of 315 | Right token -> 316 | pure token 317 | Left err | isENOENT err -> 318 | throw "Pursuit authentication token not found. Try running `pulp login` first." 319 | Left err -> 320 | throwError err 321 | 322 | pursuitUrl :: String -> Version -> String 323 | pursuitUrl name vers = 324 | "https://pursuit.purescript.org/packages/" <> name <> "/" <> Version.showVersion vers 325 | 326 | uploadPursuitDocs :: Outputter -> String -> Buffer -> Aff Unit 327 | uploadPursuitDocs out authToken gzippedJson = do 328 | res <- httpRequest reqOptions (Just gzippedJson) 329 | case HTTP.statusCode res of 330 | 201 -> 331 | pure unit 332 | other -> do 333 | out.err =<< concatStream (HTTP.responseAsStream res) 334 | out.err $ HTTP.statusMessage res 335 | out.bolded $ "This command may fail with a 400 error from Pursuit on the first run. Try running it a second time before debugging further." 336 | throw ("Expected an HTTP 201 response from Pursuit, got: " <> show other) 337 | 338 | where 339 | headers = 340 | HTTP.RequestHeaders (Object.fromFoldable 341 | [ "Accept" /\ "application/json" 342 | , "Authorization" /\ ("token " <> authToken) 343 | , "Content-Encoding" /\ "gzip" 344 | ]) 345 | 346 | reqOptions = fold 347 | [ HTTP.method := "POST" 348 | , HTTP.protocol := "https:" 349 | , HTTP.hostname := "pursuit.purescript.org" 350 | , HTTP.path := "/packages" 351 | , HTTP.headers := headers 352 | ] 353 | 354 | -- | Read a file containing JSON text and parse it, or throw an error. 355 | parseJsonFile :: forall a. SimpleJSON.ReadForeign a => String -> Aff a 356 | parseJsonFile filePath = do 357 | json <- FS.readTextFile UTF8 filePath 358 | parseJsonText ("file " <> filePath) json 359 | 360 | -- | Parse some JSON, or throw an error. 361 | parseJsonText :: forall a. SimpleJSON.ReadForeign a => String -> String -> Aff a 362 | parseJsonText source json = do 363 | case SimpleJSON.readJSON json of 364 | Left errs -> 365 | throw ("Error while decoding " <> source <> ":\n" 366 | <> String.joinWith "; " (Array.fromFoldable (map renderForeignError errs))) 367 | Right x -> 368 | pure x 369 | -------------------------------------------------------------------------------- /src/Pulp/Repl.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Repl where 2 | 3 | import Prelude 4 | 5 | import Data.Map as Map 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Set as Set 8 | import Pulp.Args (Action(..)) 9 | import Pulp.Exec (execInteractive) 10 | import Pulp.Files (defaultGlobs, sources, testGlobs) 11 | 12 | action :: Action 13 | action = Action \args -> do 14 | let opts = Map.union args.globalOpts args.commandOpts 15 | globs <- Set.union <$> defaultGlobs opts 16 | <*> testGlobs opts 17 | execInteractive "purs" (["repl"] <> sources globs <> args.remainder) Nothing 18 | -------------------------------------------------------------------------------- /src/Pulp/Run.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Run where 2 | 3 | import Prelude 4 | 5 | import Data.Array (fold) 6 | import Data.List (List(..)) 7 | import Data.Map as Map 8 | import Data.Maybe (Maybe(..)) 9 | import Data.String (Pattern(..), Replacement(..), replace, replaceAll) 10 | import Data.Version (version) 11 | import Effect.Aff (Aff) 12 | import Effect.Class (liftEffect) 13 | import Foreign.Object (Object) 14 | import Foreign.Object as Object 15 | import Node.Buffer as Buffer 16 | import Node.Encoding (Encoding(UTF8)) 17 | import Node.FS.Aff as FSA 18 | import Node.Path as Path 19 | import Node.Process as Process 20 | import Pulp.Args (Action(..)) 21 | import Pulp.Args.Get (getOption') 22 | import Pulp.Build as Build 23 | import Pulp.Exec (exec) 24 | import Pulp.Outputter (Outputter, getOutputter) 25 | import Pulp.System.Files (tempDir) 26 | import Pulp.Validate (dropPreRelBuildMeta, getNodeVersion, getPursVersion) 27 | import Pulp.Versions.PureScript (psVersions) 28 | 29 | action :: Action 30 | action = Action \args -> do 31 | let opts = Map.union args.globalOpts args.commandOpts 32 | out <- getOutputter args 33 | 34 | Build.runBuild args 35 | 36 | main <- getOption' "main" opts 37 | 38 | buildPath <- getOption' "buildPath" opts 39 | runtime <- getOption' "runtime" opts 40 | scriptFilePath <- makeRunnableScript { out, buildPath, prefix: "pulp-run", moduleName: main } 41 | env <- setupEnv buildPath 42 | nodeFlags <- getNodeFlags out runtime 43 | exec runtime (nodeFlags <> [scriptFilePath] <> args.remainder) (Just env) 44 | 45 | -- | Given a build path, create an environment that is just like this process' 46 | -- | environment, except with NODE_PATH set up for commands like `pulp run`. 47 | setupEnv :: String -> Aff (Object String) 48 | setupEnv buildPath = do 49 | env <- liftEffect Process.getEnv 50 | path <- liftEffect (Path.resolve [] buildPath) 51 | pure $ Object.alter (prependPath path) 52 | "NODE_PATH" 53 | env 54 | 55 | prependPath :: String -> Maybe String -> Maybe String 56 | prependPath newPath paths = 57 | Just $ case paths of 58 | Nothing -> newPath 59 | Just p -> newPath <> Path.delimiter <> p 60 | 61 | -- | Escape a string for insertion into a JS string literal. 62 | jsEscape :: String -> String 63 | jsEscape = 64 | replace (Pattern "'") (Replacement "\\'") <<< 65 | replace (Pattern "\\") (Replacement "\\\\") 66 | 67 | -- | Returns an empty array or `[ "--experimental-modules" ]` 68 | -- | if using a version of PureScript that outputs ES modules 69 | -- | on a Node runtime with a version < `13.0.0`. 70 | getNodeFlags :: Outputter -> String -> Aff (Array String) 71 | getNodeFlags out runtime 72 | | runtime == "node" = do 73 | nodeVer <- getNodeVersion 74 | psVer <- getPursVersion out 75 | let 76 | usingEsModules = (dropPreRelBuildMeta psVer) >= psVersions.v0_15_0 77 | nodeNeedsFlag = nodeVer < (version 13 0 0 Nil Nil) 78 | pure if usingEsModules && nodeNeedsFlag then [ "--experimental-modules" ] else [] 79 | | otherwise = pure [] 80 | 81 | makeRunnableScript :: { out :: Outputter, buildPath :: String, prefix :: String, moduleName :: String } -> Aff String 82 | makeRunnableScript { out, buildPath, prefix, moduleName } = do 83 | psVer <- getPursVersion out 84 | fullPath' <- liftEffect $ Path.resolve [] buildPath 85 | let 86 | fullPath = replaceAll (Pattern "\\") (Replacement "/") fullPath' 87 | { makeEntry, writePackageJsonFile } = 88 | if (dropPreRelBuildMeta psVer) < psVersions.v0_15_0 then 89 | { makeEntry: makeCjsEntry, writePackageJsonFile: false } 90 | else 91 | { makeEntry: makeEsEntry fullPath, writePackageJsonFile: true } 92 | src <- liftEffect $ Buffer.fromString (makeEntry moduleName) UTF8 93 | 94 | parentDir <- tempDir { prefix, suffix: ".js" } 95 | let 96 | scriptFile = Path.concat [ parentDir, "index.js" ] 97 | packageJson = Path.concat [ parentDir, "package.json" ] 98 | FSA.writeFile scriptFile src 99 | when writePackageJsonFile do 100 | FSA.writeTextFile UTF8 packageJson """{"type": "module"}""" 101 | pure scriptFile 102 | 103 | -- | Construct a JS string to be used as an entry point from a module name. 104 | makeCjsEntry :: String -> String 105 | makeCjsEntry main = "require('" <> jsEscape main <> "').main();\n" 106 | 107 | makeEsEntry :: String -> String -> String 108 | makeEsEntry buildPath main = fold 109 | [ "import { main } from 'file://" 110 | , buildPath 111 | , "/" 112 | , jsEscape main 113 | , "/index.js'; main();" 114 | ] 115 | -------------------------------------------------------------------------------- /src/Pulp/Server.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Server 3 | ( action 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Data.Either (Either(..)) 9 | import Data.Map as Map 10 | import Data.Maybe (Maybe(..)) 11 | import Effect (Effect) 12 | import Effect.Aff (Aff, attempt, launchAff, makeAff) 13 | import Effect.Aff.AVar as AVar 14 | import Effect.Class (liftEffect) 15 | import Foreign (unsafeToForeign) 16 | import Node.Encoding (Encoding(..)) 17 | import Node.HTTP as HTTP 18 | import Node.Path as Path 19 | import Node.Stream as Stream 20 | import Pulp.Args (Action(..), Args, Options) 21 | import Pulp.Args.Get (getFlag, getOption') 22 | import Pulp.Build as Build 23 | import Pulp.Outputter (getOutputter) 24 | import Pulp.System.StaticServer as StaticServer 25 | import Pulp.Utils (orErr) 26 | import Pulp.Validate (failIfUsingEsModulesPsVersion) 27 | import Pulp.Watch (watchAff, watchDirectories) 28 | 29 | data BuildResult 30 | = Succeeded 31 | | Failed 32 | 33 | getBundleFileName :: Options -> Aff String 34 | getBundleFileName opts = 35 | (_ <> "/app.js") <$> getOption' "buildPath" opts 36 | 37 | action :: Action 38 | action = Action \args -> do 39 | let opts = Map.union args.globalOpts args.commandOpts 40 | out <- getOutputter args 41 | 42 | whenM (Build.shouldBundle args) do 43 | failIfUsingEsModulesPsVersion out $ Just 44 | "Code path reason: `pulp server` uses `purs bundle` implicitly." 45 | 46 | bundleFileName <- getBundleFileName opts 47 | hostname <- getOption' "host" opts 48 | port <- getOption' "port" opts 49 | 50 | -- This AVar should be 'full' (i.e. contain a value) if and only if the 51 | -- most recent build attempt has finished; in this case, the value inside 52 | -- tells you whether the build was successful and the bundle can be served 53 | -- to the client. 54 | rebuildV <- AVar.empty 55 | 56 | server <- liftEffect $ createServer rebuildV bundleFileName 57 | listen server { hostname, port, backlog: Nothing } 58 | 59 | out.log $ "Server listening on http://" <> hostname <> ":" <> show port <> "/" 60 | 61 | quiet <- getFlag "quiet" opts 62 | let rebuild = do 63 | r <- attempt (rebuildWith { bundleFileName, quiet } args) 64 | case r of 65 | Right _ -> 66 | AVar.put Succeeded rebuildV 67 | Left _ -> do 68 | AVar.put Failed rebuildV 69 | out.err $ "Failed to rebuild; try to fix the compile errors" 70 | rebuild 71 | 72 | dirs <- watchDirectories opts >>= orErr "Internal error: unexpected Nothing" 73 | let pattern = map (\d -> Path.concat [d, "**", "*"]) dirs 74 | watchAff pattern \_ -> do 75 | void $ AVar.take rebuildV 76 | rebuild 77 | 78 | createServer :: AVar.AVar BuildResult -> String -> Effect HTTP.Server 79 | createServer rebuildV bundleFileName = do 80 | static <- StaticServer.new "." 81 | HTTP.createServer \req res -> 82 | case (HTTP.requestURL req) of 83 | "/app.js" -> 84 | void $ launchAff do 85 | -- The effect of this line should be to block until the current 86 | -- rebuild is finished (if any). 87 | r <- AVar.read rebuildV 88 | liftEffect $ case r of 89 | Succeeded -> 90 | StaticServer.serveFile static bundleFileName 200 req res 91 | Failed -> do 92 | HTTP.setStatusCode res 400 93 | HTTP.setStatusMessage res "Rebuild failed" 94 | let resS = HTTP.responseAsStream res 95 | void $ 96 | Stream.writeString resS UTF8 "Compile error in pulp server" $ 97 | Stream.end resS (pure unit) 98 | 99 | _ -> 100 | StaticServer.serve static req res 101 | 102 | listen :: HTTP.Server -> HTTP.ListenOptions -> Aff Unit 103 | listen server opts = 104 | -- TODO: error handling? 105 | makeAff \cb -> mempty <* HTTP.listen server opts (cb (Right unit)) 106 | 107 | rebuildWith :: { bundleFileName :: String, quiet :: Boolean } -> Args -> Aff Unit 108 | rebuildWith { bundleFileName, quiet } args = 109 | Build.build (args { commandOpts = addExtras args.commandOpts }) 110 | where 111 | addExtras = 112 | Map.insert "to" (Just (unsafeToForeign bundleFileName)) 113 | >>> if quiet then Map.insert "_silenced" Nothing else identity 114 | -------------------------------------------------------------------------------- /src/Pulp/Shell.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Shell (shell) where 3 | 4 | import Prelude 5 | 6 | import Data.Maybe (Maybe(..)) 7 | import Effect.Aff (Aff) 8 | import Effect.Class (liftEffect) 9 | import Node.Buffer as Buffer 10 | import Node.Encoding (Encoding(UTF8)) 11 | import Node.FS.Aff as FS 12 | import Node.Platform (Platform(Win32)) 13 | import Node.Process as Process 14 | import Pulp.Exec (exec) 15 | import Pulp.Outputter (Outputter) 16 | import Pulp.System.Files (openTemp) 17 | 18 | shell :: Outputter -> String -> Aff Unit 19 | shell out cmd = do 20 | if Process.platform == Just Win32 21 | then shell' out cmd 22 | { extension: ".cmd" 23 | , executable: "cmd" 24 | , extraArgs: ["/s", "/c"] 25 | } 26 | else shell' out cmd 27 | { extension: ".sh" 28 | , executable: "sh" 29 | , extraArgs: [] 30 | } 31 | 32 | type ShellOptions = 33 | { extension :: String 34 | , executable :: String 35 | , extraArgs :: Array String 36 | } 37 | 38 | shell' :: Outputter -> String -> ShellOptions -> Aff Unit 39 | shell' out cmd opts = do 40 | out.log $ "Executing " <> cmd 41 | cmdBuf <- liftEffect $ Buffer.fromString cmd UTF8 42 | info <- openTemp { prefix: "pulp-cmd-", suffix: opts.extension } 43 | _ <- FS.fdAppend info.fd cmdBuf 44 | _ <- FS.fdClose info.fd 45 | exec opts.executable (opts.extraArgs <> [info.path]) Nothing 46 | out.log "Done." 47 | -------------------------------------------------------------------------------- /src/Pulp/Sorcery.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.sorceryImpl = function sorceryImpl(file, succ, err) { 4 | var sorcery = require('sorcery'); 5 | sorcery.load(file).then(function (chain) { 6 | if (!chain) { 7 | err(new Error("Sorcery did not resolve chain for " + file)); 8 | return; 9 | } 10 | chain.write().then(succ, err); 11 | }, err); 12 | }; 13 | -------------------------------------------------------------------------------- /src/Pulp/Sorcery.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Sorcery (sorcery) where 3 | 4 | import Prelude 5 | 6 | import Data.Either (Either(..)) 7 | import Effect (Effect) 8 | import Effect.Aff (Aff, makeAff) 9 | import Effect.Exception (Error) 10 | import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn3) 11 | 12 | foreign import sorceryImpl :: EffectFn3 String (Effect Unit) (EffectFn1 Error Unit) Unit 13 | 14 | -- | Run sorcery given JS file 15 | sorcery :: String -> Aff Unit 16 | sorcery file = makeAff \cb -> mempty <* runEffectFn3 sorceryImpl file (cb (Right unit)) (mkEffectFn1 (cb <<< Left)) 17 | -------------------------------------------------------------------------------- /src/Pulp/System/FFI.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.FFI 2 | 3 | "use strict"; 4 | 5 | exports.runNodeImpl = function runNode$prime(error, success, fn) { 6 | return function() { 7 | fn(function(err, val) { 8 | if (err) { error(err)(); } else { success(val)(); } 9 | }); 10 | }; 11 | }; 12 | 13 | exports.unsafeInspect = function unsafeInspect(obj) { 14 | return require('util').inspect(obj); 15 | }; 16 | -------------------------------------------------------------------------------- /src/Pulp/System/FFI.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.FFI where 2 | 3 | import Prelude 4 | 5 | import Data.Either (Either(..)) 6 | import Data.Function.Uncurried (Fn3, runFn3) 7 | import Effect (Effect) 8 | import Effect.Aff (Aff, makeAff) 9 | import Effect.Exception (Error) 10 | 11 | foreign import data NodeError :: Type 12 | 13 | -- | A normal side-effecting node callback, taking 2 parameters: the first for 14 | -- | an error, the second for success. The type of the success value should be 15 | -- | the same as the type parameter. 16 | foreign import data Callback :: Type -> Type 17 | 18 | foreign import runNodeImpl :: forall a. Fn3 (Error -> Effect Unit) (a -> Effect Unit) (Callback a -> Unit) (Effect Unit) 19 | 20 | runNode :: forall a. (Callback a -> Unit) -> Aff a 21 | runNode fn = makeAff (\cb -> mempty <* runFn3 runNodeImpl (cb <<< Left) (cb <<< Right) fn) 22 | 23 | -- | This is quite unsafe but often useful. 24 | foreign import unsafeInspect :: forall a. a -> String 25 | -------------------------------------------------------------------------------- /src/Pulp/System/Files.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.Files 2 | 3 | "use strict"; 4 | 5 | exports.isEEXIST = function isEEXIST(err) { 6 | return err && err.code === 'EEXIST'; 7 | }; 8 | 9 | var temp = require('temp').track(); 10 | exports.openTempImpl = function openTemp$prime(opts, callback) { 11 | temp.open(opts, callback); 12 | }; 13 | 14 | 15 | exports.tempDirImpl = function tempDir$prime(opts, callback) { 16 | temp.mkdir(opts, callback); 17 | }; 18 | 19 | exports.createWriteStream = function createWriteStream(path) { 20 | return function() { 21 | return require('fs').createWriteStream(path); 22 | }; 23 | }; 24 | 25 | exports.isENOENT = function isENOENT(error) { 26 | return error.code === "ENOENT"; 27 | }; 28 | 29 | exports.touchImpl = function touch$prime(path, callback) { 30 | require("touch")(path, callback); 31 | }; 32 | -------------------------------------------------------------------------------- /src/Pulp/System/Files.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.System.Files 3 | ( mkdirIfNotExist 4 | , openTemp 5 | , tempDir 6 | , createWriteStream 7 | , isENOENT 8 | , touch 9 | ) where 10 | 11 | import Prelude 12 | 13 | import Control.Monad.Error.Class (catchJust) 14 | import Data.Function.Uncurried (Fn2, runFn2) 15 | import Data.Maybe (Maybe(..)) 16 | import Effect (Effect) 17 | import Effect.Aff (Aff) 18 | import Effect.Exception (Error) 19 | import Node.FS (FileDescriptor) 20 | import Node.FS.Aff as FS 21 | import Pulp.System.FFI (Callback, runNode) 22 | import Pulp.System.Stream (WritableStream) 23 | 24 | foreign import isEEXIST :: Error -> Boolean 25 | 26 | mkdirIfNotExist :: String -> Aff Unit 27 | mkdirIfNotExist dir = 28 | catchJust (\e -> if isEEXIST e then Just unit else Nothing) 29 | (FS.mkdir dir) 30 | pure 31 | 32 | type TempOptions = { prefix :: String, suffix :: String } 33 | type TempFileInfo = { path :: String, fd :: FileDescriptor } 34 | 35 | foreign import openTempImpl :: Fn2 TempOptions (Callback TempFileInfo) Unit 36 | foreign import tempDirImpl :: Fn2 TempOptions (Callback String) Unit 37 | 38 | openTemp :: TempOptions -> Aff TempFileInfo 39 | openTemp opts = runNode $ runFn2 openTempImpl opts 40 | 41 | -- | Create a temporary directory and return its path. 42 | tempDir :: TempOptions -> Aff String 43 | tempDir opts = runNode $ runFn2 tempDirImpl opts 44 | 45 | foreign import createWriteStream :: String -> Effect WritableStream 46 | 47 | foreign import isENOENT :: Error -> Boolean 48 | 49 | foreign import touchImpl :: Fn2 String (Callback Unit) Unit 50 | 51 | touch :: String -> Aff Unit 52 | touch path = runNode $ runFn2 touchImpl path 53 | -------------------------------------------------------------------------------- /src/Pulp/System/HTTP.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.HTTP where 2 | 3 | import Prelude 4 | 5 | import Data.Either (Either(..)) 6 | import Data.Maybe (Maybe(..)) 7 | import Data.Options (Options) 8 | import Effect.Aff (Aff, makeAff) 9 | import Node.Buffer (Buffer) 10 | import Node.HTTP.Client as HTTP 11 | import Node.Stream as Stream 12 | 13 | httpRequest :: Options HTTP.RequestOptions -> Maybe Buffer -> Aff HTTP.Response 14 | httpRequest reqOptions reqBody = 15 | makeAff \cb -> do 16 | req <- HTTP.request reqOptions (cb <<< Right) 17 | let reqStream = HTTP.requestAsStream req 18 | Stream.onError reqStream (cb <<< Left) 19 | maybeWrite reqStream reqBody do 20 | Stream.end reqStream do 21 | pure unit 22 | pure mempty 23 | where 24 | maybeWrite stream (Just body) next = void (Stream.write stream body next) 25 | maybeWrite _ Nothing next = next 26 | -------------------------------------------------------------------------------- /src/Pulp/System/Read.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.Read 2 | 3 | "use strict"; 4 | 5 | exports.readImpl = require("read"); 6 | -------------------------------------------------------------------------------- /src/Pulp/System/Read.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.Read ( read ) where 2 | 3 | import Prelude 4 | 5 | import Data.Function.Uncurried (Fn2, runFn2) 6 | import Effect.Aff (Aff) 7 | import Pulp.System.FFI (Callback, runNode) 8 | 9 | type ReadOptions = { prompt :: String, silent :: Boolean } 10 | 11 | foreign import readImpl :: Fn2 ReadOptions (Callback String) Unit 12 | 13 | read :: ReadOptions -> Aff String 14 | read opts = runNode $ runFn2 readImpl opts 15 | -------------------------------------------------------------------------------- /src/Pulp/System/Require.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.Require 2 | "use strict"; 3 | 4 | exports.requireResolve = function requireResolve(path) { 5 | return function() { 6 | return require.resolve(path); 7 | }; 8 | }; 9 | 10 | exports.unsafeRequire = function unsafeRequire(path) { 11 | return function() { 12 | return require(path); 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/Pulp/System/Require.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.Require where 2 | 3 | import Effect (Effect) 4 | 5 | foreign import unsafeRequire :: forall a. String -> Effect a 6 | foreign import requireResolve :: String -> Effect String 7 | -------------------------------------------------------------------------------- /src/Pulp/System/StaticServer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports["new"] = function(path) { 4 | return function() { 5 | var s = require('node-static'); 6 | return new s.Server(path, { 7 | headers: { 8 | 'cache-control': 'no-cache' 9 | } 10 | }); 11 | }; 12 | }; 13 | 14 | exports.serve = function(server) { 15 | return function(req) { 16 | return function(res) { 17 | return function() { 18 | server.serve(req, res); 19 | }; 20 | }; 21 | }; 22 | }; 23 | 24 | exports.serveFile = function(server) { 25 | return function(file) { 26 | return function(statusCode) { 27 | return function(req) { 28 | return function(res) { 29 | return function() { 30 | server.serveFile(file, statusCode, {}, req, res); 31 | }; 32 | }; 33 | }; 34 | }; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /src/Pulp/System/StaticServer.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.StaticServer where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Node.HTTP as HTTP 7 | 8 | foreign import data StaticServer :: Type 9 | 10 | -- | Create a static file server, given a base directory to serve files from. 11 | foreign import new :: String -> Effect StaticServer 12 | 13 | -- | Serve files; intended to be used within the callback to Node.HTTP.createServer. 14 | foreign import serve :: StaticServer -> HTTP.Request -> HTTP.Response -> Effect Unit 15 | 16 | -- | Serve a specific file; intended to be used within the callback to Node.HTTP.createServer. 17 | -- | The `String` and `Int` arguments are the file path and status code respectively. 18 | foreign import serveFile :: StaticServer -> String -> Int -> HTTP.Request -> HTTP.Response -> Effect Unit 19 | -------------------------------------------------------------------------------- /src/Pulp/System/Stream.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.Stream 2 | "use strict"; 3 | var Readable = require('stream').Readable; 4 | 5 | exports.concatStreamToBufferImpl = function concatStream$prime(stream, callback) { 6 | var concat = require("concat-stream"); 7 | 8 | var onSuccess = function (buf) { 9 | callback(null, buf); 10 | }; 11 | 12 | var onError = function (err) { 13 | callback(err, null); 14 | }; 15 | 16 | stream.on('error', onError); 17 | stream.pipe(concat(onSuccess)); 18 | }; 19 | 20 | exports.createGzip = require("zlib").createGzip; 21 | 22 | exports.streamFromString = function (str) { 23 | return function () { 24 | return Readable.from(str); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/Pulp/System/Stream.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.Stream 2 | ( AnyStream(..) 3 | , ReadableStream(..) 4 | , WritableStream(..) 5 | , concatStream 6 | , concatStreamToBuffer 7 | , createGzip 8 | , end 9 | , forget 10 | , stderr 11 | , stdout 12 | , streamFromString 13 | , write 14 | ) 15 | where 16 | 17 | import Prelude 18 | 19 | import Data.Either (Either(..)) 20 | import Data.Function.Uncurried (runFn2, Fn2) 21 | import Effect (Effect) 22 | import Effect.Aff (Aff, makeAff) 23 | import Effect.Class (liftEffect) 24 | import Node.Buffer (Buffer) 25 | import Node.Buffer as Buffer 26 | import Node.Encoding (Encoding(UTF8)) 27 | import Node.Process as Process 28 | import Node.Stream as Node 29 | import Pulp.System.FFI (Callback, runNode) 30 | import Unsafe.Coerce (unsafeCoerce) 31 | 32 | type ReadableStream = Node.Readable () 33 | type WritableStream = Node.Writable () 34 | 35 | -- | A stream which might or might not be readable or writable. 36 | foreign import data AnyStream :: Type 37 | 38 | -- | Forget about whether a particular stream is readable or writable. 39 | forget :: forall r. Node.Stream r -> AnyStream 40 | forget = unsafeCoerce 41 | 42 | write :: forall r. Node.Writable r -> String -> Aff Unit 43 | write stream str = makeAff (\cb -> mempty <* void (Node.writeString stream UTF8 str (cb (Right unit)))) 44 | 45 | end :: forall r. Node.Writable r -> Aff Unit 46 | end stream = makeAff (\cb -> mempty <* void (Node.end stream (cb (Right unit)))) 47 | 48 | concatStream :: forall w. Node.Readable w -> Aff String 49 | concatStream stream = do 50 | buf <- concatStreamToBuffer stream 51 | liftEffect (Buffer.toString UTF8 buf) 52 | 53 | concatStreamToBuffer :: forall w. Node.Readable w -> Aff Buffer 54 | concatStreamToBuffer stream = runNode $ runFn2 concatStreamToBufferImpl stream 55 | 56 | foreign import concatStreamToBufferImpl :: forall w. Fn2 (Node.Readable w) (Callback Buffer) Unit 57 | 58 | foreign import createGzip :: Effect (Node.Duplex) 59 | 60 | stdout :: WritableStream 61 | stdout = unsafeCoerce Process.stdout 62 | 63 | stderr :: WritableStream 64 | stderr = unsafeCoerce Process.stderr 65 | 66 | foreign import streamFromString :: forall w. String -> Effect (Node.Readable w) 67 | -------------------------------------------------------------------------------- /src/Pulp/System/SupportsColor.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.SupportsColor 2 | "use strict"; 3 | 4 | // The MIT License (MIT) 5 | // 6 | // Copyright (c) Sindre Sorhus (sindresorhus.com) 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | exports.supportLevel = (function() { 27 | if (process.stderr && !process.stderr.isTTY) { 28 | return 0; 29 | } 30 | 31 | if (process.platform === 'win32') { 32 | return 1; 33 | } 34 | 35 | if ('COLORTERM' in process.env) { 36 | return 1; 37 | } 38 | 39 | if (process.env.TERM === 'dumb') { 40 | return 0; 41 | } 42 | 43 | if (/^xterm-256(?:color)?/.test(process.env.TERM)) { 44 | return 2; 45 | } 46 | 47 | if (/^screen|^xterm|^vt100|color|ansi|cygwin|linux/i.test(process.env.TERM)) { 48 | return 1; 49 | } 50 | 51 | return 0; 52 | })(); 53 | -------------------------------------------------------------------------------- /src/Pulp/System/SupportsColor.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.SupportsColor 2 | ( hasBasic 3 | , has256 4 | ) where 5 | 6 | import Prelude 7 | 8 | foreign import supportLevel :: Int 9 | 10 | hasBasic :: Boolean 11 | hasBasic = supportLevel >= 1 12 | 13 | has256 :: Boolean 14 | has256 = supportLevel >= 2 15 | -------------------------------------------------------------------------------- /src/Pulp/System/TreeKill.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.TreeKill 2 | "use strict"; 3 | 4 | exports.treeKill = function treeKill(pid) { 5 | return function(signal) { 6 | return function() { 7 | require("tree-kill")(pid, signal); 8 | }; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/Pulp/System/TreeKill.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.TreeKill 2 | ( treeKill ) where 3 | 4 | import Prelude 5 | 6 | import Data.Posix (Pid) 7 | import Effect (Effect) 8 | 9 | foreign import treeKill :: Pid -> String -> Effect Unit 10 | -------------------------------------------------------------------------------- /src/Pulp/System/Which.js: -------------------------------------------------------------------------------- 1 | // module Pulp.System.Which 2 | 3 | "use strict"; 4 | 5 | exports.whichImpl = require("which"); 6 | -------------------------------------------------------------------------------- /src/Pulp/System/Which.purs: -------------------------------------------------------------------------------- 1 | module Pulp.System.Which ( which ) where 2 | 3 | import Prelude 4 | 5 | import Data.Function.Uncurried (Fn2, runFn2) 6 | import Effect.Aff (Aff) 7 | import Pulp.System.FFI (Callback, runNode) 8 | 9 | foreign import whichImpl :: Fn2 String (Callback String) Unit 10 | 11 | which :: String -> Aff String 12 | which cmd = runNode $ runFn2 whichImpl cmd 13 | -------------------------------------------------------------------------------- /src/Pulp/Test.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Test 3 | ( action 4 | ) where 5 | 6 | import Prelude 7 | 8 | import Data.Map as Map 9 | import Data.Maybe (Maybe(..)) 10 | import Foreign (unsafeToForeign) 11 | import Pulp.Args (Action(..), Options) 12 | import Pulp.Args.Get (getOption') 13 | import Pulp.Build as Build 14 | import Pulp.Exec (exec) 15 | import Pulp.Outputter (getOutputter) 16 | import Pulp.Run (getNodeFlags, makeRunnableScript, setupEnv) 17 | 18 | action :: Action 19 | action = Action \args -> do 20 | let opts = Map.union args.globalOpts args.commandOpts 21 | out <- getOutputter args 22 | 23 | runtime <- getOption' "runtime" opts 24 | let isNode = runtime == "node" 25 | let changeOpts = if isNode 26 | then identity :: Options -> Options -- helps type inference 27 | else Map.insert "to" (Just (unsafeToForeign "./output/test.js")) 28 | 29 | let buildArgs = args { remainder = [] 30 | , commandOpts = changeOpts args.commandOpts 31 | } 32 | Build.testBuild buildArgs 33 | 34 | out.log "Running tests..." 35 | if isNode 36 | then do 37 | main <- getOption' "main" opts 38 | buildPath <- getOption' "buildPath" opts 39 | env <- setupEnv buildPath 40 | scriptFilePath <- makeRunnableScript { out, buildPath, prefix: "pulp-test", moduleName: main } 41 | nodeFlags <- getNodeFlags out runtime 42 | exec runtime 43 | (nodeFlags <> [scriptFilePath] <> args.remainder) 44 | (Just env) 45 | else do 46 | to <- getOption' "to" buildArgs.commandOpts 47 | exec runtime ([to] <> args.remainder) Nothing 48 | 49 | out.log "Tests OK." 50 | -------------------------------------------------------------------------------- /src/Pulp/Utils.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Utils where 2 | 3 | import Prelude 4 | 5 | import Control.Monad.Error.Class (class MonadError, throwError) 6 | import Data.Maybe (Maybe, maybe) 7 | import Effect.Exception (Error, error) 8 | 9 | orErr :: forall m a. MonadError Error m => String -> Maybe a -> m a 10 | orErr msg = maybe (throw msg) pure 11 | 12 | throw :: forall m a. MonadError Error m => String -> m a 13 | throw = throwError <<< error 14 | -------------------------------------------------------------------------------- /src/Pulp/Validate.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Validate 2 | ( validate 3 | , getPursVersion 4 | , getPsaVersion 5 | , getNodeVersion 6 | , dropPreRelBuildMeta 7 | , failIfUsingEsModulesPsVersion 8 | ) where 9 | 10 | import Prelude 11 | 12 | import Control.Monad.Error.Class (throwError) 13 | import Data.Array (fold) 14 | import Data.Either (Either(..)) 15 | import Data.Foldable (for_) 16 | import Data.List (List(..)) 17 | import Data.Maybe (Maybe(..), fromMaybe) 18 | import Data.String (Pattern(..), codePointFromChar, stripPrefix, takeWhile, trim) 19 | import Data.Version.Haskell (Version(..), parseVersion, showVersion) 20 | import Data.Version.Haskell as HVersion 21 | import Data.Version as SemVer 22 | import Effect.Aff (Aff) 23 | import Effect.Class (liftEffect) 24 | import Effect.Exception (error, throw) 25 | import Node.Process as Process 26 | import Pulp.Exec (execQuiet) 27 | import Pulp.Outputter (Outputter) 28 | import Pulp.Versions.PureScript (psVersions) 29 | import Text.Parsing.Parser (parseErrorMessage) 30 | 31 | validate :: Outputter -> Aff Version 32 | validate out = do 33 | ver <- getPursVersion out 34 | when (ver < minimumPursVersion) $ do 35 | out.err $ "This version of Pulp requires version " 36 | <> showVersion minimumPursVersion <> " of the PureScript compiler " 37 | <> "or higher." 38 | out.err $ "Your installed version is " <> showVersion ver <> "." 39 | out.err $ "Please either upgrade PureScript or downgrade Pulp to version 12.4.2." 40 | throwError $ error "Minimum purs version not satisfied" 41 | pure ver 42 | 43 | getPursVersion :: Outputter -> Aff Version 44 | getPursVersion = getVersionFrom "purs" 45 | 46 | minimumPursVersion :: Version 47 | minimumPursVersion = psVersions.v0_12_0 48 | 49 | getPsaVersion :: Outputter -> Aff Version 50 | getPsaVersion = getVersionFrom "psa" 51 | 52 | getNodeVersion :: Aff SemVer.Version 53 | getNodeVersion = do 54 | case SemVer.parseVersion (stripV Process.version) of 55 | Left err -> 56 | let message = parseErrorMessage err 57 | in throwError (error ("Failed to parse node.js version: " <> message)) 58 | Right actual -> 59 | pure actual 60 | where 61 | stripV str = 62 | fromMaybe str (stripPrefix (Pattern "v") str) 63 | 64 | getVersionFrom :: String -> Outputter -> Aff Version 65 | getVersionFrom bin out = do 66 | verStr <- takeWhile (_ /= codePointFromChar ' ') <$> trim <$> execQuiet bin ["--version"] Nothing 67 | case parseVersion verStr of 68 | Right v -> 69 | pure v 70 | Left _ -> do 71 | out.err $ "Unable to parse the version from " <> bin <> ". (It was: " <> verStr <> ")" 72 | out.err $ "Please check that the right executable is on your PATH." 73 | throwError $ error ("Couldn't parse version from " <> bin) 74 | 75 | failIfUsingEsModulesPsVersion :: Outputter -> Maybe String -> Aff Unit 76 | failIfUsingEsModulesPsVersion out mbMsg = do 77 | psVer <- getPursVersion out 78 | unless ((dropPreRelBuildMeta psVer) < psVersions.v0_15_0) do 79 | out.err $ fold 80 | [ "This code path implicitly uses `purs bundle` or CommonsJS modules, both of which are no longer supported in PureScript v0.15.0. " 81 | , "You are using PureScript " <> HVersion.showVersion psVer <> ". " 82 | , "See https://github.com/purescript/documentation/blob/master/migration-guides/0.15-Migration-Guide.md" 83 | ] 84 | for_ mbMsg out.err 85 | liftEffect $ throw $ "Your version of PureScript cannot use `purs bundle` or CommonJS modules. Please use another bundler (e.g. esbuild) instead." 86 | 87 | dropPreRelBuildMeta :: Version -> Version 88 | dropPreRelBuildMeta (Version mmp _) = Version mmp Nil 89 | -------------------------------------------------------------------------------- /src/Pulp/Version.purs: -------------------------------------------------------------------------------- 1 | 2 | module Pulp.Version ( version, versionString, printVersion ) where 3 | 4 | import Prelude 5 | 6 | import Control.Monad.Except (runExcept) 7 | import Data.Either (Either(..), either) 8 | import Data.Maybe (Maybe(..)) 9 | import Data.String (trim) 10 | import Data.Version (Version, parseVersion, showVersion) 11 | import Effect.Aff (Aff, attempt) 12 | import Effect.Class (liftEffect) 13 | import Effect.Console as Console 14 | import Effect.Exception (throwException, error) 15 | import Effect.Exception.Unsafe (unsafeThrow) 16 | import Effect.Unsafe (unsafePerformEffect) 17 | import Foreign (readString) 18 | import Foreign.Index (readProp) 19 | import Foreign.JSON (parseJSON) 20 | import Node.Encoding (Encoding(..)) 21 | import Node.FS.Sync as FS 22 | import Node.Globals (__dirname) 23 | import Node.Path as Path 24 | import Pulp.Exec (execQuiet) 25 | import Pulp.System.Which (which) 26 | 27 | version :: Version 28 | version = 29 | case parseVersion versionString of 30 | Right v -> v 31 | Left err -> unsafeThrow $ "pulp: Unable to parse version from package.json: " 32 | <> show err 33 | 34 | versionString :: String 35 | versionString = 36 | unsafePerformEffect $ do 37 | json <- FS.readTextFile UTF8 (Path.concat [__dirname, "package.json"]) 38 | case runExcept (parseJSON json >>= readProp "version" >>= readString) of 39 | Left err -> 40 | throwException (error ("pulp: Unable to parse package.json: " <> show err)) 41 | Right v -> 42 | pure v 43 | 44 | printVersion :: Aff Unit 45 | printVersion = do 46 | pursVersion <- execQuiet "purs" ["--version"] Nothing 47 | pursPath <- attempt $ which "purs" 48 | liftEffect $ Console.log $ 49 | "Pulp version " <> showVersion version <> 50 | "\npurs version " <> trim pursVersion <> 51 | either (const "") (\p -> " using " <> trim p) pursPath 52 | -------------------------------------------------------------------------------- /src/Pulp/VersionBump.purs: -------------------------------------------------------------------------------- 1 | -- | Defines a VersionBump type and associated functions. 2 | module Pulp.VersionBump where 3 | 4 | import Prelude 5 | 6 | import Data.Either (either) 7 | import Data.Maybe (Maybe(..)) 8 | import Data.String as String 9 | import Data.Version (Version) 10 | import Data.Version as Version 11 | 12 | data VersionBump = Major | Minor | Patch | ToExact Version 13 | 14 | instance showBump :: Show VersionBump where 15 | show Major = "Major" 16 | show Minor = "Minor" 17 | show Patch = "Patch" 18 | show (ToExact v) = "(ToExact " <> Version.showVersion v <> ")" 19 | 20 | parseBump :: String -> Maybe VersionBump 21 | parseBump str = 22 | case String.toLower str of 23 | "major" -> Just Major 24 | "minor" -> Just Minor 25 | "patch" -> Just Patch 26 | _ -> ToExact <$> either (const Nothing) Just (Version.parseVersion str) 27 | 28 | applyBump :: VersionBump -> Version -> Version 29 | applyBump b = case b of 30 | Major -> Version.bumpMajor 31 | Minor -> Version.bumpMinor 32 | Patch -> Version.bumpPatch 33 | ToExact v -> const v 34 | -------------------------------------------------------------------------------- /src/Pulp/Versions/PureScript.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Versions.PureScript where 2 | 3 | import Data.List (List(..), (:)) 4 | import Data.List.NonEmpty as NEL 5 | import Data.Version.Haskell (Version(..)) 6 | 7 | type PureScriptVersions = 8 | { v0_12_0 :: Version 9 | , v0_12_4 :: Version 10 | , v0_13_0 :: Version 11 | , v0_14_0 :: Version 12 | , v0_15_0 :: Version 13 | } 14 | 15 | psVersions :: PureScriptVersions 16 | psVersions = 17 | { v0_12_0: Version (NEL.cons' 0 (12 : 0 : Nil)) Nil 18 | , v0_12_4: Version (NEL.cons' 0 (12 : 4 : Nil)) Nil 19 | , v0_13_0: Version (NEL.cons' 0 (13 : 0 : Nil)) Nil 20 | , v0_14_0: Version (NEL.cons' 0 (14 : 0 : Nil)) Nil 21 | , v0_15_0: Version (NEL.cons' 0 (15 : 0 : Nil)) Nil 22 | } 23 | -------------------------------------------------------------------------------- /src/Pulp/Watch.js: -------------------------------------------------------------------------------- 1 | // module Pulp.Watch 2 | 3 | "use strict"; 4 | 5 | exports.watch = function(pattern) { 6 | return function(act) { 7 | return function() { 8 | var Gaze = require("gaze").Gaze; 9 | 10 | var gaze = new Gaze(pattern, { follow: true }); 11 | 12 | gaze.on("all", function(_, path) { 13 | act(path)(); 14 | }); 15 | }; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/Pulp/Watch.purs: -------------------------------------------------------------------------------- 1 | module Pulp.Watch 2 | ( watch 3 | , watchAff 4 | , watchDirectories 5 | , action 6 | ) where 7 | 8 | import Prelude 9 | 10 | import Data.Array as Array 11 | import Data.DateTime (DateTime) 12 | import Data.DateTime as DateTime 13 | import Data.Foldable (notElem) 14 | import Data.Map as Map 15 | import Data.Maybe (Maybe, fromMaybe) 16 | import Data.Set as Set 17 | import Data.Time.Duration (Milliseconds(..)) 18 | import Data.Traversable (traverse, sequence) 19 | import Effect (Effect) 20 | import Effect.Aff (Aff, launchAff) 21 | import Effect.Aff.AVar as AVar 22 | import Effect.Class (liftEffect) 23 | import Effect.Now as Now 24 | import Effect.Ref as Ref 25 | import Node.ChildProcess (fork, pid) 26 | import Node.Globals (__filename) 27 | import Node.Process as Process 28 | import Pulp.Args (Action(..), Options) 29 | import Pulp.Args.Get (getOption) 30 | import Pulp.Files (defaultGlobs, ffis, sources, testGlobs) 31 | import Pulp.Outputter (getOutputter) 32 | import Pulp.System.TreeKill (treeKill) 33 | import Pulp.Utils (orErr) 34 | 35 | foreign import watch :: 36 | Array String 37 | -> (String -> Effect Unit) 38 | -> Effect Unit 39 | 40 | watchAff :: Array String -> (String -> Aff Unit) -> Aff Unit 41 | watchAff dirs callback = liftEffect do 42 | debouncedCallback <- debounce (Milliseconds 100.0) 43 | (callback 44 | >>> launchAff 45 | >>> void) 46 | watch dirs debouncedCallback 47 | 48 | -- | Ensure that a callback is only called at some given maximum frequency, 49 | -- | by returning a new callback that does nothing if an attempt is made to 50 | -- | perform it again sooner than the given duration since the last attempt. 51 | debounce :: forall a. Milliseconds -> (a -> Effect Unit) -> Effect (a -> Effect Unit) 52 | debounce cooldown callback = do 53 | timer <- Ref.new (bottom :: DateTime) 54 | pure \info -> do 55 | lastPerformed <- Ref.read timer 56 | now <- Now.nowDateTime 57 | when (DateTime.diff now lastPerformed > cooldown) do 58 | Ref.write now timer 59 | callback info 60 | 61 | -- Returns Nothing if the given Options did not include the relevant options 62 | -- i.e. watching does not make sense with this command. 63 | watchDirectories :: Options -> Aff (Maybe (Array String)) 64 | watchDirectories opts = do 65 | -- If any of these give Nothing, we shouldn't be using watchDirectories 66 | let basicPathOpts = ["srcPath", "testPath", "dependencyPath"] 67 | basicPaths <- traverse (flip getOption opts) basicPathOpts 68 | 69 | -- It's ok if this is Nothing, though. 70 | includePaths <- fromMaybe [] <$> getOption "includePaths" opts 71 | 72 | pure $ map (_ <> includePaths) (sequence basicPaths) 73 | 74 | action :: Action 75 | action = Action \args -> do 76 | let opts = Map.union args.globalOpts args.commandOpts 77 | out <- getOutputter args 78 | 79 | -- It is important to do this before attempting to `fork` a separate process. 80 | _ <- watchDirectories opts >>= orErr "This command does not work with --watch" 81 | 82 | argv' <- liftEffect $ Array.filter (_ `notElem` ["-w", "--watch"]) <<< Array.drop 2 <$> Process.argv 83 | childV <- AVar.empty 84 | liftEffect (fork __filename argv') >>= \x -> AVar.put x childV 85 | 86 | globs <- Set.union <$> defaultGlobs opts <*> testGlobs opts 87 | let fileGlobs = sources globs <> ffis globs 88 | 89 | watchAff fileGlobs $ \_ -> do 90 | child <- AVar.take childV 91 | liftEffect $ treeKill (pid child) "SIGTERM" 92 | out.write "---\n" 93 | out.log "Source tree changed; restarting:" 94 | liftEffect (fork __filename argv') >>= AVar.put <@> childV 95 | -------------------------------------------------------------------------------- /test-js/cjs/Main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // module Test.Main 4 | 5 | exports.argv = function argv() { 6 | return process.argv; 7 | }; 8 | 9 | exports.slice = function slice(n) { 10 | return function (arr) { 11 | return arr.slice(n); 12 | }; 13 | }; 14 | 15 | exports.length = function length(arr) { 16 | return arr.length; 17 | }; 18 | -------------------------------------------------------------------------------- /test-js/cjs/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Effect (Effect, foreachE) 5 | import Effect.Console (log) 6 | 7 | foreign import argv :: Effect (Array String) 8 | foreign import slice :: forall a. Int -> Array a -> Array a 9 | foreign import length :: forall a. Array a -> Int 10 | 11 | drop :: forall a. Int -> Array a -> Array a 12 | drop n arr 13 | | n < 1 = arr 14 | | n >= length arr = [] 15 | | otherwise = slice n arr 16 | 17 | main :: Effect Unit 18 | main = do 19 | args <- drop 2 <$> argv 20 | foreachE args log 21 | -------------------------------------------------------------------------------- /test-js/es/Main.js: -------------------------------------------------------------------------------- 1 | // module Test.Main 2 | 3 | export const argv = function argv() { 4 | return process.argv; 5 | }; 6 | 7 | export const slice = function slice(n) { 8 | return function (arr) { 9 | return arr.slice(n); 10 | }; 11 | }; 12 | 13 | export const length = function length(arr) { 14 | return arr.length; 15 | }; 16 | -------------------------------------------------------------------------------- /test-js/es/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | import Effect (Effect, foreachE) 5 | import Effect.Console (log) 6 | 7 | foreign import argv :: Effect (Array String) 8 | foreign import slice :: forall a. Int -> Array a -> Array a 9 | foreign import length :: forall a. Array a -> Int 10 | 11 | drop :: forall a. Int -> Array a -> Array a 12 | drop n arr 13 | | n < 1 = arr 14 | | n >= length arr = [] 15 | | otherwise = slice n arr 16 | 17 | main :: Effect Unit 18 | main = do 19 | args <- drop 2 <$> argv 20 | foreachE args log 21 | -------------------------------------------------------------------------------- /test-js/sh.js: -------------------------------------------------------------------------------- 1 | import co from "co"; 2 | import { exec } from "child_process"; 3 | import _temp from "temp"; 4 | import { resolve } from "path"; 5 | import { assert } from "chai"; 6 | import fs from "fs"; 7 | import which from "which"; 8 | 9 | const temp = _temp.track(); 10 | 11 | function sh(cwd, cmd, input, opts) { 12 | opts = opts || {}; 13 | return new Promise((resolve, reject) => { 14 | const procOpts = { cwd: opts.cwd || cwd }; 15 | if (opts.path) { 16 | procOpts.env = {...process.env}; 17 | const pathVar = process.platform === "win32" ? "Path" : "PATH"; 18 | procOpts.env[pathVar] = opts.path; 19 | } 20 | const proc = exec(cmd, procOpts, (error, stdout, stderr) => { 21 | resolve({ error, stdout, stderr }); 22 | }); 23 | proc.stdin.end(input || ""); 24 | }).then(function(r) { 25 | const expectedExitCode = (opts && opts.expectedExitCode) || 0; 26 | const exitCode = (r.error && r.error.code) || 0; 27 | if (expectedExitCode !== exitCode) { 28 | let msg = (r.error && r.error.message) || ""; 29 | msg += "Expected exit code " + expectedExitCode + 30 | " but got " + exitCode + "."; 31 | const newErr = new Error(msg); 32 | newErr.innerError = r.error; 33 | throw newErr; 34 | } 35 | 36 | return [r.stdout, r.stderr]; 37 | }); 38 | } 39 | 40 | function asserts(path) { 41 | const file = (filename, pred) => { 42 | const data = fs.readFileSync(resolve(path, filename), "utf-8"); 43 | pred ? pred(data) : true; 44 | }; 45 | 46 | const exists = (filename) => file(filename, (data) => true); 47 | 48 | return Object.assign({}, assert, { file, exists }); 49 | } 50 | 51 | function resolvePath(cmd) { 52 | return new Promise((resolve, reject) => { 53 | which(cmd, (err, res) => err ? reject(err) : resolve(res)); 54 | }); 55 | } 56 | 57 | function pulpFn(path, pulpPath) { 58 | return (cmd, input, opts) => 59 | resolvePath("node").then((node) => 60 | sh(path, `"${node}" "${pulpPath}" ${cmd}`, input, opts)); 61 | } 62 | 63 | export default function run(fn) { 64 | return function(done) { 65 | temp.mkdir("pulp-test-", (err, path) => { 66 | if (err) { 67 | throw err; 68 | } else { 69 | const pulpPath = resolve(__dirname, "..", "index.js"); 70 | const pulp = pulpFn(path, pulpPath); 71 | co(fn(sh.bind(null, path), pulp, asserts(path), path)).then(done, done); 72 | } 73 | }); 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Data.List (List(..)) 6 | import Data.Maybe (Maybe(..), isNothing) 7 | import Data.Tuple (Tuple(..)) 8 | import Data.Version (Version, version) 9 | import Effect.Aff (Aff, launchAff) 10 | import Effect.Class (liftEffect) 11 | import Effect.Class.Console (log) 12 | import Node.ChildProcess as CP 13 | import Node.Encoding (Encoding(UTF8)) 14 | import Node.FS.Aff as FS 15 | import Node.Process as Process 16 | import Pulp.Exec (execQuiet) 17 | import Pulp.Git (getVersionFromGitTag) 18 | import Pulp.System.Files (tempDir) 19 | import Test.Assert (assertEqual) 20 | 21 | -- | Run a shell command 22 | run :: String -> Array String -> Aff String 23 | run cmd args = execQuiet cmd args Nothing 24 | 25 | run_ :: String -> Array String -> Aff Unit 26 | run_ cmd args = void $ run cmd args 27 | 28 | commitFile :: String -> String -> Aff Unit 29 | commitFile name contents = do 30 | FS.writeTextFile UTF8 name contents 31 | run_ "git" ["add", name] 32 | run_ "git" ["commit", "--message", "add " <> name] 33 | 34 | assertEq :: forall a. Show a => Eq a => a -> a -> Aff Unit 35 | assertEq expected actual = 36 | liftEffect $ assertEqual { actual, expected } 37 | 38 | main = launchAff do 39 | dir <- tempDir { prefix: "pulp-unit-test-", suffix: "" } 40 | liftEffect $ Process.chdir dir 41 | run_ "git" ["init"] 42 | run_ "git" ["config", "user.email", "pulp-test@example.com"] 43 | run_ "git" ["config", "user.name", "Pulp Tester"] 44 | 45 | ----------------------------------------------------------------------------- 46 | log "getVersionFromGitTag (basic)" 47 | 48 | commitFile "file1.txt" "abcde\n" 49 | commitFile "file2.txt" "fghij\n" 50 | 51 | run_ "git" ["tag", "v1.0.0"] 52 | 53 | result <- getVersionFromGitTag 54 | assertEq (Just (Tuple "v1.0.0" (version 1 0 0 Nil Nil))) result 55 | 56 | ----------------------------------------------------------------------------- 57 | log "getVersionFromGitTag doesn't give old tags" 58 | 59 | commitFile "file3.txt" "klmno\n" 60 | result <- getVersionFromGitTag 61 | assertEq Nothing result 62 | 63 | ----------------------------------------------------------------------------- 64 | log "getVersionFromGitTag gives the latest version tag pointing to HEAD" 65 | 66 | run_ "git" ["commit", "--allow-empty", "--message", "an empty commit"] 67 | run_ "git" ["tag", "blahblahblah"] 68 | run_ "git" ["tag", "v2.0.0"] 69 | run_ "git" ["tag", "v2.0.1"] 70 | 71 | result <- getVersionFromGitTag 72 | assertEq (Just (Tuple "v2.0.1" (version 2 0 1 Nil Nil))) result 73 | 74 | ----------------------------------------------------------------------------- 75 | log "getVersionFromGitTag works with annotated tags" 76 | 77 | run_ "git" ["commit", "--allow-empty", "--message", "another empty commit"] 78 | run_ "git" ["tag", "--annotate", "--message", "lmao", "v3.0.0"] 79 | 80 | result <- getVersionFromGitTag 81 | assertEq (Just (Tuple "v3.0.0" (version 3 0 0 Nil Nil))) result 82 | --------------------------------------------------------------------------------