├── .babelrc ├── .flowconfig ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── bin.js ├── fixtures └── basic │ └── examples │ └── basic.js ├── index.js ├── package.json ├── src ├── app │ ├── App.js │ └── index.js ├── cli │ ├── App.js │ ├── Example.js │ ├── Runner.js │ ├── Watcher.js │ ├── index.js │ └── utils │ │ ├── constants.js │ │ ├── fs.js │ │ └── parcel.js └── types.js ├── test.js ├── test └── cli.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | "flow", 6 | "react", 7 | [ 8 | "env", 9 | { 10 | "loose": true, 11 | "targets": { 12 | "node": "current" 13 | } 14 | } 15 | ] 16 | ], 17 | "plugins": [ 18 | "transform-class-properties", 19 | "transform-object-rest-spread", 20 | "transform-runtime" 21 | ] 22 | }, 23 | "legacy": { 24 | "presets": [ 25 | "flow", 26 | "react", 27 | [ 28 | "env", 29 | { 30 | "loose": true, 31 | "targets": { 32 | "node": 4 33 | } 34 | } 35 | ] 36 | ], 37 | "plugins": [ 38 | "transform-class-properties", 39 | "transform-object-rest-spread", 40 | "transform-runtime" 41 | ], 42 | "ignore": ["__mocks__", "__tests__", "__fixtures__", "node_modules"] 43 | }, 44 | "modern": { 45 | "presets": [ 46 | "flow", 47 | "react", 48 | [ 49 | "env", 50 | { 51 | "loose": true, 52 | "targets": { 53 | "node": 8 54 | } 55 | } 56 | ] 57 | ], 58 | "plugins": [ 59 | "transform-class-properties", 60 | "transform-object-rest-spread", 61 | "transform-runtime" 62 | ], 63 | "ignore": ["__mocks__", "__tests__", "__fixtures__", "node_modules"] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | 11 | [strict] 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: yarn 5 | script: yarn test && yarn flow 6 | sudo: false 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-present James Kyle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paranormal 2 | 3 | > Phenomenal Code Examples 4 | 5 | ## Install 6 | 7 | ```sh 8 | yarn add --dev paranormal 9 | ``` 10 | 11 | ## Usage 12 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var ver = process.versions.node; 5 | var majorVer = parseInt(ver.split('.')[0], 10); 6 | var cli; 7 | 8 | if (majorVer < 4) { 9 | throw new Error( 10 | 'Node version ' + 11 | ver + 12 | ' is not supported in Paranormal, please use Node.js 4.0 or higher.', 13 | ); 14 | } else if (majorVer < 8) { 15 | cli = require('./dist/legacy/cli').default; 16 | } else { 17 | cli = require('./dist/modern/cli').default; 18 | } 19 | 20 | cli(process.argv.slice(2)).catch(err => { 21 | console.error(err); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /fixtures/basic/examples/basic.js: -------------------------------------------------------------------------------- 1 | export default function example() { 2 | console.log('hello'); 3 | } 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ver = process.versions.node; 4 | var majorVer = parseInt(ver.split('.')[0], 10); 5 | 6 | if (majorVer < 4) { 7 | console.error( 8 | 'Node version ' + 9 | ver + 10 | ' is not supported in Bolt, please use Node.js 4.0 or higher.', 11 | ); 12 | process.exit(1); 13 | } else if (majorVer < 8) { 14 | module.exports = require('./dist/legacy/index'); 15 | } else { 16 | module.exports = require('./dist/modern/index'); 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paranormal", 3 | "version": "0.1.4", 4 | "description": "Phenomenal Code Examples", 5 | "main": "index.js", 6 | "bin": "bin.js", 7 | "repository": "https://github.com/thejameskyle/paranormal", 8 | "author": "James Kyle ", 9 | "license": "MIT", 10 | "scripts": { 11 | "clean": "rm -rf dist", 12 | "flow": "flow", 13 | "test": "ava", 14 | "format": "prettier --write src/**/*.js", 15 | "build:legacy": "BABEL_ENV=legacy babel src -Dd dist/legacy", 16 | "build:modern": "BABEL_ENV=modern babel src -Dd dist/modern", 17 | "build": "yarn run clean && yarn build:legacy && yarn build:modern", 18 | "dev": "yarn run clean && yarn build:modern --watch", 19 | "prepublish": "yarn build", 20 | "precommit": "lint-staged" 21 | }, 22 | "keywords": [ 23 | "example", 24 | "examples", 25 | "component", 26 | "components", 27 | "utils", 28 | "ui", 29 | "react", 30 | "vue", 31 | "angular", 32 | "ember", 33 | "storybook", 34 | "storybooks", 35 | "website" 36 | ], 37 | "files": [ 38 | "bin.js", 39 | "index.js", 40 | "dist" 41 | ], 42 | "dependencies": { 43 | "babel-runtime": "^6.26.0", 44 | "chalk": "^2.3.0", 45 | "chokidar": "^2.0.2", 46 | "find-up": "^2.1.0", 47 | "globby": "^8.0.1", 48 | "lodash.debounce": "^4.0.8", 49 | "meow": "^4.0.0", 50 | "micromatch": "^3.1.10", 51 | "parcel-bundler": "^1.7.0", 52 | "react": "^16.2.0", 53 | "react-dom": "^16.2.0", 54 | "react-router-dom": "^4.2.2", 55 | "resolve-from": "^4.0.0", 56 | "rimraf": "^2.6.2", 57 | "shuri": "^1.0.1", 58 | "signal-exit": "^3.0.2", 59 | "spawndamnit": "^1.0.0", 60 | "strip-indent": "^2.0.0", 61 | "styled-components": "^3.2.3", 62 | "tempy": "^0.2.1", 63 | "typeable-promisify": "^2.0.1" 64 | }, 65 | "devDependencies": { 66 | "ava": "^0.24.0", 67 | "babel-cli": "^6.26.0", 68 | "babel-plugin-transform-class-properties": "^6.24.1", 69 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 70 | "babel-plugin-transform-runtime": "^6.23.0", 71 | "babel-preset-env": "^1.6.1", 72 | "babel-preset-flow": "^6.23.0", 73 | "babel-preset-react": "^6.24.1", 74 | "fixturez": "^1.0.1", 75 | "flow-bin": "^0.66.0", 76 | "husky": "^0.14.3", 77 | "lint-staged": "^7.0.0", 78 | "prettier": "^1.10.2" 79 | }, 80 | "lint-staged": { 81 | "*.js": [ 82 | "prettier --write", 83 | "git add" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import type { ExamplesData } from '../types'; 5 | 6 | const Container = styled.div` 7 | position: relative; 8 | width: 100%; 9 | height: 100%; 10 | `; 11 | 12 | const NavBar = styled.nav` 13 | position: absolute; 14 | 15 | top: 0; 16 | height: 4rem; 17 | left: 0; 18 | right: 0; 19 | background: black; 20 | color: white; 21 | 22 | display: flex; 23 | align-items: stretch; 24 | `; 25 | 26 | const NavItem = styled.span` 27 | display: flex; 28 | justify-content: center; 29 | flex-direction: column; 30 | text-align: center; 31 | padding: 0.5rem 1.5rem; 32 | `; 33 | 34 | const NavLink = NavItem.withComponent('a').extend` 35 | color: inherit; 36 | text-decoration: none; 37 | 38 | &:hover { 39 | text-decoration: underline; 40 | } 41 | `; 42 | 43 | const NavLogo = NavItem.extend` 44 | font-weight: 900; 45 | font-size: 1.5em; 46 | `; 47 | 48 | const NavSelect = styled.select` 49 | appearance: none; 50 | width: auto; 51 | padding: 0 2em; 52 | border-radius: 0; 53 | font: inherit; 54 | border: none; 55 | margin: 0; 56 | background: transparent; 57 | color: inherit; 58 | `; 59 | 60 | const ExampleFrame = styled.iframe` 61 | position: absolute; 62 | top: 4rem; 63 | bottom: 0; 64 | left: 0; 65 | right: 0; 66 | border: 0; 67 | `; 68 | 69 | export type AppProps = { 70 | examples: ExamplesData, 71 | }; 72 | 73 | export default class App extends React.Component { 74 | state = { 75 | active: this.props.examples[0].href, 76 | }; 77 | 78 | handleChange = event => { 79 | console.log(event.target.value); 80 | this.setState({ active: event.target.value }); 81 | }; 82 | 83 | render() { 84 | console.log(this.props); 85 | return ( 86 | 87 | 88 | 👻 89 | 90 | {this.props.examples.map(example => { 91 | return ( 92 | 95 | ); 96 | })} 97 | 98 | 99 | 100 | 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { injectGlobal } from 'styled-components'; 5 | import App from './App'; 6 | import type { ExamplesData } from '../types'; 7 | 8 | const EXAMPLES_DATA: ExamplesData = window.EXAMPLES_DATA; 9 | 10 | injectGlobal` 11 | html, 12 | body, 13 | #root { 14 | position: relative; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | body { 20 | margin: 0; 21 | font-family: 22 | -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, 23 | sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 24 | -webkit-font-smoothing: antialiased; 25 | } 26 | `; 27 | 28 | let root = document.getElementById('root'); 29 | if (!root) throw new Error('Missing #root'); 30 | render(, root); 31 | -------------------------------------------------------------------------------- /src/cli/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import * as fs from './utils/fs'; 4 | import Example from './Example'; 5 | import stripIndent from 'strip-indent'; 6 | 7 | export type AppOpts = { 8 | tempDir: string, 9 | }; 10 | 11 | export default class App { 12 | tempDir: string; 13 | 14 | indexPath: string; 15 | entriesPath: string; 16 | 17 | constructor(opts: AppOpts) { 18 | this.tempDir = opts.tempDir; 19 | this.indexPath = path.join(this.tempDir, 'index.html'); 20 | this.entriesPath = path.join(this.tempDir, 'entries.html'); 21 | } 22 | 23 | async build(examples: Array) { 24 | let EXAMPLES_DATA: ExamplesData = examples.map(example => { 25 | return { 26 | title: example.title, 27 | href: path.relative(this.tempDir, example.htmlPath), 28 | }; 29 | }); 30 | 31 | let links = []; 32 | 33 | links.push(path.relative(this.tempDir, this.indexPath)); 34 | 35 | examples.forEach(example => { 36 | links.push( 37 | path.relative(this.tempDir, example.htmlPath), 38 | path.relative(this.tempDir, example.txtPath), 39 | path.relative(this.tempDir, example.jsPath), 40 | ); 41 | }); 42 | 43 | let indexContent = stripIndent(` 44 | 45 | 46 | 47 | 48 | 👻 Paranormal 49 | 50 | 51 |
52 | 55 | 56 | 57 | 58 | 59 | `).trim(); 60 | 61 | let entriesContent = stripIndent(` 62 | 63 | 64 | 65 | 66 | 👻 Paranormal 67 | 68 | 69 |
    70 | ${links 71 | .map(link => `
  • ${link}
  • `) 72 | .join('')} 73 |
74 | 75 | 76 | `).trim(); 77 | 78 | await Promise.all([ 79 | fs.writeFile(this.indexPath, indexContent), 80 | fs.writeFile(this.entriesPath, entriesContent), 81 | ]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/cli/Example.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import path from 'path'; 3 | import * as constants from './utils/constants'; 4 | import * as fs from './utils/fs'; 5 | import stripIndent from 'strip-indent'; 6 | import resolveFrom from 'resolve-from'; 7 | 8 | function basename(filePath) { 9 | return path.basename(filePath, path.extname(filePath)); 10 | } 11 | 12 | export type ExampleOpts = { 13 | cwd: string, 14 | tempDir: string, 15 | filePath: string, 16 | }; 17 | 18 | export default class Example { 19 | filePath: string; 20 | 21 | relativePath: string; 22 | relativePathOut: string; 23 | 24 | dirName: string; 25 | dirNameOut: string; 26 | 27 | baseName: string; 28 | baseNameOut: string; 29 | 30 | tempDir: string; 31 | htmlPath: string; 32 | txtPath: string; 33 | jsPath: string; 34 | 35 | title: string; 36 | 37 | htmlContent: string; 38 | jsContent: string; 39 | 40 | constructor(opts: ExampleOpts) { 41 | this.filePath = opts.filePath; 42 | this.relativePath = path.relative(opts.cwd, this.filePath); 43 | this.relativePathOut = this.relativePath 44 | .split(path.sep) 45 | .map(part => part.replace(constants.EXAMPLE_PATH_PART_NUMBER, '$2')) 46 | .join(path.sep); 47 | 48 | this.dirName = path.dirname(this.relativePath); 49 | this.dirNameOut = path.dirname(this.relativePathOut); 50 | 51 | this.baseName = basename(this.relativePath); 52 | this.baseNameOut = basename(this.relativePathOut); 53 | 54 | this.tempDir = path.join(opts.tempDir, this.dirNameOut); 55 | this.htmlPath = path.join(this.tempDir, this.baseNameOut + '.html'); 56 | this.txtPath = path.join(this.tempDir, this.baseNameOut + '.txt'); 57 | this.jsPath = path.join(this.tempDir, this.baseNameOut + '.js'); 58 | 59 | let currentDirName = path.basename(opts.cwd); 60 | let exampleDirName = this.dirNameOut.replace(path.sep, '/'); 61 | 62 | this.title = `${currentDirName}/${exampleDirName}/${this.baseNameOut}`; 63 | 64 | let reactImport = resolveFrom(this.filePath, 'react'); 65 | let reactDomImport = resolveFrom(this.filePath, 'react-dom'); 66 | 67 | let relativeJsImport = path.relative(this.jsPath, this.filePath); 68 | let relativeReactImport = path.relative(this.jsPath, reactImport); 69 | let relativeReactDomImport = path.relative(this.jsPath, reactDomImport); 70 | 71 | this.htmlContent = stripIndent(` 72 | 73 | 74 | 75 | 76 | ${this.title} 77 | 78 | 79 |
80 | 81 | 82 | 83 | `).trim(); 84 | 85 | this.jsContent = stripIndent(` 86 | import React from "${relativeReactImport}"; 87 | import { render } from "${relativeReactDomImport}"; 88 | import Example from "${relativeJsImport}"; 89 | 90 | render(React.createElement(Example), document.getElementById("root")); 91 | `).trim(); 92 | } 93 | 94 | async build() { 95 | let fileContents = await fs.readFile(this.filePath); 96 | await fs.mkdirp(this.tempDir); 97 | await Promise.all([ 98 | fs.writeFile(this.htmlPath, this.htmlContent), 99 | fs.writeFile(this.txtPath, fileContents), 100 | fs.writeFile(this.jsPath, this.jsContent), 101 | ]); 102 | } 103 | 104 | async delete() { 105 | await Promise.all([fs.unlink(example.htmlPath), fs.unlink(example.jsPath)]); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/cli/Runner.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Watcher from './Watcher'; 3 | import Example from './Example'; 4 | import App from './App'; 5 | import * as fs from './utils/fs'; 6 | import * as constants from './utils/constants'; 7 | import path from 'path'; 8 | import chalk from 'chalk'; 9 | import * as parcel from './utils/parcel'; 10 | import stripIndent from 'strip-indent'; 11 | import debounce from 'lodash.debounce'; 12 | import { type ExamplesData } from '../types'; 13 | 14 | type Action = { 15 | kind: 'add' | 'remove' | 'change', 16 | filePath: string, 17 | }; 18 | 19 | type RunnerOpts = { 20 | cwd: string, 21 | outDir: string, 22 | }; 23 | 24 | export default class Runner { 25 | cwd: string; 26 | dirName: string; 27 | tempDir: string; 28 | outDir: string; 29 | 30 | watcher: Watcher; 31 | examples: Map; 32 | app: App; 33 | queue: Array; 34 | 35 | updating: boolean; 36 | watching: boolean; 37 | ready: boolean; 38 | 39 | constructor(opts: RunnerOpts) { 40 | this.cwd = opts.cwd; 41 | this.dirName = path.dirname(this.cwd); 42 | this.tempDir = fs.tempdir(); 43 | this.outDir = opts.outDir; 44 | 45 | this.watcher = new Watcher(); 46 | this.examples = new Map(); 47 | this.app = new App({ tempDir: this.tempDir }); 48 | this.queue = []; 49 | 50 | this.updating = false; 51 | this.watching = false; 52 | this.ready = false; 53 | } 54 | 55 | async run(opts: { match: Array, watch: boolean }) { 56 | if (opts.watch) { 57 | this.watching = true; 58 | this.watcher.on('add', this.onAdd); 59 | this.watcher.on('remove', this.onRemove); 60 | this.watcher.on('change', this.onChange); 61 | this.watcher.watch(this.cwd); 62 | } 63 | 64 | let examplePaths = await fs.findGlobPatterns( 65 | this.cwd, 66 | this.getExampleGlobPatterns(), 67 | ); 68 | 69 | await Promise.all( 70 | examplePaths.map(async examplePath => { 71 | await this.addExample(examplePath); 72 | }), 73 | ); 74 | 75 | await this.app.build(Array.from(this.examples.values())); 76 | 77 | this.ready = true; 78 | 79 | if (opts.watch) { 80 | await this.update(); 81 | await parcel.serve(this.app.indexPath, this.outDir); 82 | } else { 83 | await parcel.build(this.app.indexPath, this.outDir); 84 | } 85 | } 86 | 87 | async addExample(examplePath: string) { 88 | let example = new Example({ 89 | cwd: this.cwd, 90 | tempDir: this.tempDir, 91 | filePath: examplePath, 92 | }); 93 | 94 | if (this.ready) { 95 | console.log(chalk.green(`Example: "${example.title}" (added)`)); 96 | } else { 97 | console.log(chalk.cyan(`Example: "${example.title}"`)); 98 | } 99 | 100 | await example.build(); 101 | this.examples.set(examplePath, example); 102 | } 103 | 104 | async removeExample(examplePath: string) { 105 | let example = this.examples.get(examplePath); 106 | if (!example) return; 107 | console.log(chalk.red(`Example: "${example.title}" (removed)`)); 108 | 109 | this.examples.delete(examplePath); 110 | await example.delete(); 111 | } 112 | 113 | async changeExample(examplePath: string) { 114 | let example = this.examples.get(examplePath); 115 | if (!example) return; 116 | console.log(chalk.cyan(`Example: "${example.title}" (changed)`)); 117 | } 118 | 119 | update = debounce(async () => { 120 | if (!this.ready || this.updating || !this.queue.length) { 121 | return; 122 | } 123 | 124 | this.updating = true; 125 | let queue = this.queue.splice(0); 126 | 127 | for (let action of queue) { 128 | if (action.kind === 'add') { 129 | await this.addExample(action.filePath); 130 | } else if (action.kind === 'remove') { 131 | await this.removeExample(action.filePath); 132 | } else if (action.kind === 'change') { 133 | await this.changeExample(action.filePath); 134 | } 135 | } 136 | 137 | await this.app.build(Array.from(this.examples.values())); 138 | this.updating = false; 139 | 140 | if (this.queue.length) { 141 | await this.update(); 142 | } 143 | }, 0); 144 | 145 | onAdd = (filePath: string) => { 146 | if (this.matches(filePath)) { 147 | this.queue.push({ kind: 'add', filePath }); 148 | this.update(); 149 | } 150 | }; 151 | 152 | onRemove = async (filePath: string) => { 153 | if (this.matches(filePath)) { 154 | this.queue.push({ kind: 'remove', filePath }); 155 | await this.update(); 156 | } 157 | }; 158 | 159 | onChange = async (filePath: string) => { 160 | if (this.matches(filePath)) { 161 | this.queue.push({ kind: 'change', filePath }); 162 | await this.update(); 163 | } 164 | }; 165 | 166 | matches(filePath: string) { 167 | return fs.matchesGlobPatterns( 168 | this.cwd, 169 | filePath, 170 | this.getExampleGlobPatterns(), 171 | ); 172 | } 173 | 174 | getExampleGlobPatterns() { 175 | return [ 176 | constants.DEFAULT_EXAMPLES_GLOB, 177 | constants.IGNORE_NODE_MODULES_GLOB, 178 | `!${path.relative(this.cwd, this.outDir)}/**`, 179 | `!.cache/**`, 180 | ]; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/cli/Watcher.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import EventEmitter from 'events'; 3 | import * as fs from './utils/fs'; 4 | 5 | export default class Watcher extends EventEmitter { 6 | watch(dirPath: string) { 7 | let watcher = fs.watchDirectory(dirPath); 8 | 9 | watcher.on('add', (filePath: string) => { 10 | this.emit('add', filePath); 11 | }); 12 | 13 | watcher.on('unlink', (filePath: string) => { 14 | this.emit('remove', filePath); 15 | }); 16 | 17 | watcher.on('change', (filePath: string) => { 18 | this.emit('change', filePath); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import meow from 'meow'; 3 | import path from 'path'; 4 | import chalk from 'chalk'; 5 | import Runner from './Runner'; 6 | 7 | type Opts = { 8 | cwd: string, 9 | outDir: string, 10 | match: Array, 11 | watch: boolean, 12 | }; 13 | 14 | function cliToOptions(input, flags): Opts { 15 | let match = input; 16 | let cwd; 17 | 18 | if (typeof flags.cwd === 'undefined') { 19 | cwd = process.cwd(); 20 | } else if (typeof flags.cwd === 'string') { 21 | cwd = path.resolve(process.cwd(), flags.cwd); 22 | } else { 23 | throw new Error(`The flag \`--cwd=\` requires a path`); 24 | } 25 | 26 | let outDir; 27 | if (typeof flags.outDir === 'undefined') { 28 | outDir = path.resolve(process.cwd(), 'dist'); 29 | } else if (typeof flags.outDir === 'string') { 30 | outDir = path.resolve(process.cwd(), flags.outDir); 31 | } else { 32 | throw new Error(`The flag \`--outDir=\` requires a path`); 33 | } 34 | 35 | let watch; 36 | if (typeof flags.watch === 'undefined') { 37 | watch = false; 38 | } else if (typeof flags.watch === 'boolean') { 39 | watch = flags.watch; 40 | } else { 41 | throw new Error(`The flag \`--watch/-w\` does not accept an argument`); 42 | } 43 | 44 | return { match, cwd, outDir, watch }; 45 | } 46 | 47 | export default async function cli(argv: Array) { 48 | let start = Date.now(); 49 | 50 | let { pkg, input, flags } = meow({ 51 | argv, 52 | help: ` 53 | Usage 54 | $ paranormal <...globs> <...flags> 55 | 56 | Flags 57 | --watch, -w Watch files and update on changes 58 | --cwd Set the current working directory 59 | --out-dir Directory for output structure 60 | `, 61 | }); 62 | 63 | console.error( 64 | chalk.bold.cyan( 65 | `👻 Paranormal ${pkg.version} (node: ${process.versions.node})`, 66 | ), 67 | ); 68 | 69 | let { cwd, outDir, match, watch } = cliToOptions(input, flags); 70 | let runner = new Runner({ cwd, outDir }); 71 | 72 | await runner.run({ match, watch }); 73 | 74 | if (!watch) { 75 | let timing = (Date.now() - start) / 1000; 76 | let rounded = Math.round(timing * 100) / 100; 77 | 78 | console.error(chalk.dim(`💀 Done in ${rounded}s.`)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/cli/utils/constants.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | export const EXAMPLE_PATH_PART_NUMBER = /^(\d+-)?(.*)/; 3 | export const DEFAULT_EXAMPLES_GLOB = '**/examples/**'; 4 | export const IGNORE_NODE_MODULES_GLOB = '!node_modules'; 5 | -------------------------------------------------------------------------------- /src/cli/utils/fs.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import promisify from 'typeable-promisify'; 3 | import fs, { type FSWatcher } from 'fs'; 4 | import _mkdirp from 'mkdirp'; 5 | import _rimraf from 'rimraf'; 6 | import tempy from 'tempy'; 7 | import globby from 'globby'; 8 | import micromatch from 'micromatch'; 9 | import path from 'path'; 10 | import chokidar from 'chokidar'; 11 | import onExit from 'signal-exit'; 12 | 13 | export function readFile(filePath: string) { 14 | return promisify(cb => fs.readFile(filePath, cb)); 15 | } 16 | 17 | export function writeFile(filePath: string, fileContents: string | Buffer) { 18 | return promisify(cb => fs.writeFile(filePath, fileContents, cb)); 19 | } 20 | 21 | export function stat(filePath: string) { 22 | return promisify(cb => fs.stat(filePath, cb)); 23 | } 24 | 25 | export function lstat(filePath: string) { 26 | return promisify(cb => fs.lstat(filePath, cb)); 27 | } 28 | 29 | export function readdir(filePath: string) { 30 | return promisify(cb => fs.readdir(filePath, cb)); 31 | } 32 | 33 | export function unlink(filePath: string) { 34 | return promisify(cb => fs.unlink(filePath, cb)); 35 | } 36 | 37 | export function mkdirp(dirPath: string) { 38 | return promisify(cb => _mkdirp(dirPath, cb)); 39 | } 40 | 41 | export function rimraf(dirPath: string) { 42 | return promisify(cb => _rimraf(dirPath, cb)); 43 | } 44 | 45 | let TEMP_DIRECTORIES = []; 46 | 47 | export function tempdir() { 48 | let dirPath = tempy.directory(); 49 | TEMP_DIRECTORIES.push(dirPath); 50 | return dirPath; 51 | } 52 | 53 | onExit(async () => { 54 | for (let dirPath of TEMP_DIRECTORIES) { 55 | await rimraf(dirPath); 56 | } 57 | }); 58 | 59 | export async function findGlobPatterns(cwd: string, patterns: Array) { 60 | let matches = await globby(patterns, { cwd }); 61 | return matches.map(match => path.join(cwd, match)); 62 | } 63 | 64 | export function matchesGlobPatterns( 65 | cwd: string, 66 | filePath: string, 67 | patterns: Array, 68 | ) { 69 | return micromatch.every(path.relative(cwd, filePath), patterns); 70 | } 71 | 72 | export function watchDirectory(dirPath: string): FSWatcher { 73 | return chokidar.watch(dirPath, { 74 | recursive: true, 75 | encoding: 'utf8', 76 | persistent: true, 77 | ignoreInitial: true, 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/cli/utils/parcel.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | const path = require('path'); 3 | const spawn = require('spawndamnit'); 4 | 5 | const PARCEL_BIN = require.resolve('parcel-bundler/bin/cli.js'); 6 | 7 | export async function build(entry: string, outDir: string) { 8 | await spawn(PARCEL_BIN, ['build', entry, '--out-dir', outDir], { 9 | stdio: 'inherit', 10 | }); 11 | } 12 | 13 | export async function serve(entry: string, outDir: string) { 14 | await spawn(PARCEL_BIN, ['serve', entry, '--out-dir', outDir], { 15 | stdio: 'inherit', 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | export type ExamplesData = Array<{ 4 | title: string, 5 | url: string, 6 | }>; 7 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 'use strict'; 3 | const test = require('ava'); 4 | const fixtures = require('fixturez'); 5 | const paranormal = require('./'); 6 | 7 | const f = fixtures(__dirname, { root: __dirname }); 8 | 9 | test('paranormal', async t => { 10 | let dir = f.find('basic'); 11 | await paranormal({ cwd: dir }); 12 | t.pass(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paranormaljs/paranormal/d550a23f70acd6c0ca755d58806532e860a9c424/test/cli.js --------------------------------------------------------------------------------