├── .bithoundrc ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── README.md ├── lib ├── cli.js ├── commands │ ├── build │ │ ├── build-cmd.js │ │ └── index.js │ ├── deploy │ │ ├── deploy-cmd.js │ │ ├── index.js │ │ └── providers │ │ │ └── aws.js │ ├── dev │ │ ├── dev-cmd.js │ │ └── index.js │ ├── index.js │ └── init │ │ ├── index.js │ │ └── init-cmd.js ├── engine │ ├── Engine.js │ ├── EngineWorker.js │ ├── buildSpec.schema.js │ ├── configs │ │ ├── dev.js │ │ ├── index.js │ │ └── prod.js │ ├── expandBuildSpec.js │ ├── genWebpackConfig.js │ ├── index.js │ └── readBuildSpec.js ├── server │ ├── DevServer.js │ ├── buildDigest.js │ ├── reloader.js │ └── statuspage │ │ ├── app.js │ │ ├── deps │ │ ├── angular.min.js │ │ ├── bootstrap.min.css │ │ └── jquery.min.js │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── index.html │ │ ├── spinner.gif │ │ └── style.css └── util │ ├── arghelp.js │ ├── argparse.js │ ├── callsite.js │ ├── digest.js │ ├── fstate.js │ ├── hashcache.js │ ├── log.js │ └── syncDir.js ├── package-lock.json ├── package.json ├── test └── engine │ ├── compile-constants.mocha.js │ ├── data-inject.mocha.js │ ├── es-6.mocha.js │ ├── import-static-json.mocha.js │ ├── simple-static.mocha.js │ ├── subpaths.mocha.js │ ├── tree-shaking.mocha.js │ └── vue.mocha.js └── test_data ├── compile-constants ├── app.js ├── index.pug └── martinet.json ├── es6-js ├── entry.js ├── index.html └── martinet.json ├── import-static-json ├── entry.js ├── index.html ├── martinet.json └── static.json ├── js-with-deps ├── entry.js ├── index.html ├── martinet.json └── package.json ├── simple-static ├── css │ ├── more-style.less │ ├── style.css │ └── sub-dir │ │ └── sub-include.less ├── data │ └── sample.json ├── fonts │ └── glyphicons.woff2 ├── img │ └── test.jpg ├── index.pug ├── js │ └── app.js ├── martinet.json ├── pages │ ├── about.pug │ └── partials │ │ └── mixins.pug └── static │ ├── favicon.ico │ └── images │ └── verb-1.jpg ├── single-js ├── entry.js ├── index.html ├── library.js └── martinet.json ├── sub-paths ├── martinet.json └── src │ ├── app │ └── greeter.ts │ ├── assets │ └── img │ │ └── jpg │ │ └── sample.jpg │ └── templates │ └── index.pug ├── template-data-inject ├── data.json ├── index.pug └── martinet.json └── vue-components ├── entry.js ├── index.html ├── inner └── test.jpg ├── martinet.json └── vue-component.vue /.bithoundrc: -------------------------------------------------------------------------------- 1 | { 2 | "critics": { 3 | "wc": { "limit": 5000 } 4 | }, 5 | "ignore": [ 6 | "**/deps/**", 7 | "**/node_modules/**", 8 | "**/thirdparty/**", 9 | "**/third_party/**", 10 | "lib/cli.js" 11 | ], 12 | "dependencies": { 13 | "unused-ignores": [ 14 | "typescript", 15 | "less", 16 | "vue-loader", 17 | "vue-template-compiler", 18 | "url-loader", 19 | "style-loader", 20 | "pug-html-loader", 21 | "less-loader", 22 | "json-loader", 23 | "html-loader", 24 | "file-loader", 25 | "css-loader", 26 | "copy-webpack-plugin", 27 | "babel-preset-react", 28 | "babel-preset-env", 29 | "babel-loader", 30 | "babel-core", 31 | "awesome-typescript-loader", 32 | "angular2-template-loader" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": false, 7 | "impliedStrict": true 8 | }, 9 | "env": { 10 | "node": true 11 | } 12 | }, 13 | "globals": { 14 | "describe": true, 15 | "beforeEach": true, 16 | "afterEach": true, 17 | "before": true, 18 | "after": true, 19 | "it": true 20 | }, 21 | "extends": ["airbnb-base", "plugin:jasmine/recommended"], 22 | "plugins": ["jasmine"], 23 | "rules": { 24 | "one-var": 0, 25 | "guard-for-in": 0, 26 | "no-arg-redefine": 0, 27 | "no-unused-vars": 1, 28 | "no-console": 0, 29 | "global-require": 0, 30 | "prefer-spread": 0, 31 | "no-underscore-dangle": 0, 32 | "no-lonely-if": 0, 33 | "no-empty": 0, 34 | "no-multi-assign": 0, 35 | "class-methods-use-this": 0, 36 | "import/no-extraneous-dependencies": 1, 37 | "import/no-dynamic-require": 0, 38 | "key-spacing": 0, 39 | "no-plusplus": 0, 40 | "one-var-declaration-per-line": 0, 41 | "no-unused-expressions": 1, 42 | "consistent-return": 0, 43 | "no-shadow": 0, 44 | "arrow-body-style": 0, 45 | "no-param-reassign": 0, 46 | "no-unneeded-ternary": 0, 47 | "import/no-extraneous-dependencies": [2, {"devDependencies": true}] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | npm-debug.log 3 | node_modules 4 | .awcache 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [1.1.0] - 2017-07-20 4 | ### Added 5 | - Add support for global script and style definitions in martinet.json 6 | - Parallel Webpack compilation for "martinet build". 7 | 8 | ### Changes 9 | - Upgrade to Webpack 3. 10 | - Lint and ES6 updates. 11 | 12 | ## [1.0.5] - 2017-05-19 13 | ### Fixed 14 | - AWS deploys now work without credentials in ~/.aws/credentials 15 | - "init" creates a better starter project template, with a template for deploy.json 16 | 17 | ## [1.0.4] - 2017-05-17 18 | ### Changed 19 | - Better console output. 20 | 21 | 22 | ## [1.0.3] - 2017-05-17 23 | ### Added 24 | - Allow Martinet to be used from node_modules/.bin 25 | 26 | 27 | ## [1.0.2] - 2017-05-17 28 | ### Added 29 | - Allow stylesheets and favicons to be referenced/bundled via tags. 30 | - Let favicon.ico be URL-encoded in "prod" config. 31 | 32 | ### Fixed 33 | - Serve index.html on naked directory access. 34 | - Update Webpack resolve aliases for Vue. 35 | - Wrong compile-time constants in "dev" config. 36 | - Pug template local correctly rendered inside .vue single-file components. 37 | 38 | 39 | ## [1.0.1] - 2017-04-26 40 | ### Added 41 | - Enable "martinet deploy" to an AWS S3 bucket. 42 | - Add support for 'paths.src' and 'paths.dist' in build specification. 43 | - Add support for compile-time constants in Javascript and Pug. 44 | - Better build status page and error display. 45 | 46 | ### Fixed 47 | - Correct Vue version loaded. 48 | - Dangling empty directories in build output correctly deleted. 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-16 Mayank Lahiri 2 | 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Martinet 2 | 3 | [![Build Status](https://travis-ci.org/iceroad/martinet.svg?branch=master)](https://travis-ci.org/iceroad/martinet) 4 | [![bitHound Code](https://www.bithound.io/github/iceroad/martinet/badges/code.svg)](https://www.bithound.io/github/iceroad/martinet) 5 | [![bitHound Overall Score](https://www.bithound.io/github/iceroad/martinet/badges/score.svg)](https://www.bithound.io/github/iceroad/martinet) 6 | [![bitHound Dependencies](https://www.bithound.io/github/iceroad/martinet/badges/dependencies.svg)](https://www.bithound.io/github/iceroad/martinet/master/dependencies/npm) 7 | 8 | 9 | Martinet is an opinionated, command-line build tool for static websites and single-page webapps, built on the [Webpack](https://webpack.github.io/) module bundler. Its purpose is to bring you all the power and modern features of Webpack, without having to interact with Webpack itself. It is suited for those who want to build static websites, hybrid single page applications, and combinations of the two. 10 | 11 | Martinet starts by looking for a build specification in a file called `martinet.json`. 12 | 13 | This is an example of a build specification that mixes multiple template and style languages, in addition to base HTML and CSS. 14 | 15 | { 16 | "global": { 17 | "styles": [ "style-root.less" ], 18 | "scripts": [ "progressive-enhancement.js" ] 19 | }, 20 | "pages": [ 21 | { 22 | "src": "template.pug", 23 | "data": [ "some-data.json" ], 24 | "scripts": [ "analytics.js" ], 25 | "dist": "index.html" 26 | }, 27 | { 28 | "src": "about.html", 29 | "dist": "about/index.html" 30 | } 31 | ] 32 | } 33 | 34 | Martinet processes all the pages listed in the `pages` section, producing an optimized web distribution that includes all the following features out of the box: 35 | 36 | * Modern language support: [ES2017](https://babeljs.io/docs/plugins/preset-latest/), [TypeScript](https://www.typescriptlang.org/), [LessCSS](http://lesscss.org/), [Pug](https://pugjs.org/api/getting-started.html), in addition to HTML and CSS. 37 | * Support for dependencies installed via `npm`. 38 | * Parallel Webpack builds to take advantage of multicore systems. 39 | * Asset bundling and versioning, for separating cacheable content. 40 | * Tree-shaking on ES6 modules, resulting in smaller bundles. 41 | * Auto-refreshing web server for development. 42 | * Support for statically injecting JSON data files into templates, *e.g.*, `some-data.json` in the example above will be injected into `template.pug` to produce `index.html`. 43 | * Extra support for select modern web frameworks. 44 | * [Single-file VueJs components](https://vuejs.org/v2/guide/single-file-components.html) (VueJs-2). 45 | * [Angular template bundler](https://github.com/TheLarkInn/angular2-template-loader) (Angular-2 and higher). 46 | * Incremental deployment to AWS S3 with the appropriate cache headers for versioned files. 47 | 48 | After running `martinet build -o /tmp/output`, the `/tmp/output` directory will resemble the following directory listing (with different version identifiers). 49 | 50 | index.html 51 | about/index.html 52 | __ver__/js.a9034893.js 53 | __ver__/style.b8932a83.css 54 | 55 | You can now open the HTML files in a browser, or run `martinet deploy` to upload the web distribution to an AWS S3 bucket. 56 | 57 | In production mode (the default for `build`), the HTML, Javascript, and CSS files will be optimized and minified, and a content-based hexadecimal identifier added to the filename. Any references to the file will be updated to point to this new filename. The `__ver__` directory will contain versioned copies of all asset files that have been found in your project, and can be cached indefinitely for client-side performance. 58 | 59 | The design of Martinet favors **convention-over-configuration**. The target use case is to build small-to-medium static sites and single-page web applications, as well as combinations of the two. 60 | 61 | ### Install 62 | 63 | npm install -g @iceroad/martinet 64 | martinet --help 65 | 66 | ## Conventions 67 | 68 | **martinet**, *n.* a person who stresses a rigid adherence to the details of forms and methods. 69 | 70 | Martinet's conventions are put in place so that you do not have to configure 71 | Webpack yourself 72 | 73 | 1. All style and script dependencies should be included in either the build specification, or 74 | using language-specific import commands. Do not include internal dependencies via ``; 32 | 33 | class DevServer extends EventEmitter { 34 | constructor(options) { 35 | super(); 36 | this.opt_ = options; 37 | 38 | // 39 | // Get "prod" or "dev" build configuration. 40 | // 41 | assert( 42 | this.opt_.config === 'prod' || this.opt_.config === 'dev', 43 | `Invalid build config "${this.opt_.config}".`.red); 44 | this.buildConfig_ = this.opt_.config; 45 | 46 | // 47 | // Ensure build specification exists in project root directory 48 | // (but may be invalid/malformed, that's OK). 49 | // 50 | this.projectRoot_ = path.resolve(this.opt_.root); 51 | this.buildSpecPath_ = path.resolve(this.opt_.root, 'martinet.json'); 52 | try { 53 | fs.accessSync(this.buildSpecPath_, fs.constants.R_OK); 54 | } catch (e) { 55 | throw new Error( 56 | `Unable to read build specification "${this.buildSpecPath_}": ` + 57 | `${e.message}`); 58 | } 59 | 60 | this.buildState_ = BUILD_STATES.init; 61 | 62 | // 63 | // WebPack can "finish" a build and then restart it and then finish again 64 | // sometimes, causing havoc with UI. Debounce build success notifications. 65 | // 66 | this.succeedBuild = _.debounce(this.succeedBuild.bind(this), 1000); 67 | this.failBuild = _.debounce(this.failBuild.bind(this), 1000); 68 | } 69 | 70 | start() { 71 | log.info(`Build configuration: ${col.bgYellow.black(this.buildConfig_)}`); 72 | 73 | // 74 | // Create and start a server instance. 75 | // 76 | this.createWebServer(); 77 | const port = this.opt_.port; 78 | this.httpServer_.listen(port, '0.0.0.0', (err) => { 79 | if (err) { 80 | log.error(err); 81 | return process.exit(1); 82 | } 83 | log.info(`${col.bold('Open the following URL in a browser:')} 84 | ┃ 85 | ┃ ${col.bold(`http://localhost:${port}/`).green} 86 | ┃`); 87 | 88 | // 89 | // Create and start a build file watcher to bootstrap the engine 90 | // creation process. 91 | // 92 | this.watchBuildSpecification(); 93 | }); 94 | } 95 | 96 | watchBuildSpecification() { 97 | const state = {}; 98 | 99 | const ReadSpec = () => { 100 | // Read raw JSON. 101 | try { 102 | state.curSpec = readBuildSpec(this.projectRoot_); 103 | } catch (e) { 104 | this.failBuild({ 105 | reason: 'bad_spec', 106 | text: `Cannot read build specification: ${e.message}`, 107 | }); 108 | delete state.oldSpec; 109 | return _.delay(ReadSpec, 2000); 110 | } 111 | 112 | // Compare to previous spec, kickstart rebuild if needed. 113 | const currentStableStr = stablejson(state.curSpec); 114 | if (currentStableStr !== state.oldSpec) { 115 | state.oldSpec = currentStableStr; 116 | this.startNewBuild(state.curSpec); 117 | } 118 | 119 | return _.delay(ReadSpec, 2000); 120 | }; 121 | 122 | ReadSpec(); 123 | } 124 | 125 | failBuild(errorDetail) { 126 | this.buildState_ = BUILD_STATES.error; 127 | this.errorDetails_ = errorDetail; 128 | this.broadcast(this.getBuildState()); 129 | } 130 | 131 | succeedBuild() { 132 | this.buildState_ = BUILD_STATES.done; 133 | delete this.errorDetails_; 134 | this.broadcast(this.getBuildState()); 135 | } 136 | 137 | createWebServer() { 138 | // 139 | // Create HTTP server and bind HTTP request listener. 140 | // 141 | const app = this.expressApp_ = express(); 142 | app.use( 143 | '/__martinet__/status', 144 | express.static(path.join(__dirname, 'statuspage'))); 145 | app.use(this.onHttpRequest.bind(this)); 146 | app.use(this.onHttp404.bind(this)); 147 | this.httpServer_ = http.createServer(app); 148 | 149 | // 150 | // Create Websocket server. 151 | // 152 | const wsOptions = { 153 | path: '/__martinet__/ws', 154 | perMessageDeflate: true, 155 | server: this.httpServer_, 156 | }; 157 | this.wsServer_ = new ws.Server(wsOptions); 158 | this.wsServer_.on('connection', this.onWsConnectionStart.bind(this)); 159 | this.wsServer_.on('error', err => log.error(err)); 160 | } 161 | 162 | onHttp404(req, res) { 163 | res.writeHead(404, { 164 | 'Content-Type': 'text/plain', 165 | }); 166 | res.end('404 Not Found'); 167 | } 168 | 169 | onWsConnectionStart(websocket) { 170 | websocket.send(json(this.getBuildState())); 171 | } 172 | 173 | getBuildState() { 174 | return { 175 | specPath: this.buildSpecPath_, 176 | projectRoot: this.projectRoot_, 177 | status: this.buildState_, 178 | error: this.errorDetails_, 179 | build: this.pageBuildStatus_, 180 | buildId: this.buildId_, 181 | }; 182 | } 183 | 184 | broadcast(data) { 185 | const dataStr = stablejson(data); 186 | if (dataStr !== this.lastBroadcast_) { 187 | log.debug(`Websocket broadcast: ${json(data).substr(0, 140)}`); 188 | this.wsServer_.clients.forEach((client) => { 189 | if (client.readyState === ws.OPEN) { 190 | client.send(dataStr); 191 | } 192 | }); 193 | this.lastBroadcast_ = dataStr; 194 | } 195 | } 196 | 197 | onHttpRequest(req, res, next) { 198 | switch (this.buildState_) { 199 | case BUILD_STATES.init: 200 | case BUILD_STATES.building: 201 | case BUILD_STATES.error: { 202 | // Redirect to status page. 203 | const statusUrl = 204 | `/__martinet__/status?bounceBack=${encodeURIComponent(req.url)}`; 205 | res.writeHead(302, { 206 | Location: statusUrl, 207 | }); 208 | res.end(); 209 | break; 210 | } 211 | 212 | case BUILD_STATES.done: { 213 | // Serve file from project build directory. 214 | let urlPath = decodeURIComponent(url.parse(req.url).pathname); 215 | if (urlPath[urlPath.length - 1] === '/') urlPath += 'index.html'; 216 | let clientPath = path.join(this.buildDir_, path.normalize(urlPath).substr(1)); 217 | if (fs.existsSync(clientPath) && fs.statSync(clientPath).isDirectory()) { 218 | clientPath += '/index.html'; 219 | } 220 | let fileContents, mimeType; 221 | try { 222 | fileContents = fs.readFileSync(clientPath); 223 | mimeType = mime.lookup(clientPath); 224 | } catch (e) { 225 | log.debug(`File not found: ${clientPath}: ${e}`); 226 | return next(); 227 | } 228 | 229 | // 230 | // Inject auto-reloader if the config flag is set and MIME is HTML. 231 | // 232 | if (mimeType === 'text/html') { 233 | fileContents = Buffer.from( 234 | fileContents.toString('utf-8').replace(/<\/body>/mi, RELOADER_JS), 235 | 'utf-8'); 236 | } 237 | res.writeHead(200, { 238 | 'Content-Type': mimeType, 239 | 'Content-Length': fileContents.length, 240 | }); 241 | res.end(fileContents); 242 | break; 243 | } 244 | 245 | default: { 246 | log.error(`Unknown build state: ${this.buildState_}`); 247 | return process.exit(1); 248 | } 249 | } 250 | } 251 | 252 | startNewBuild(buildSpec) { 253 | log.info(`Starting new build...`); 254 | delete this.errorDetails_; 255 | this.buildState_ = BUILD_STATES.building; 256 | this.broadcast(this.getBuildState()); 257 | 258 | // 259 | // Create new temporary directory for each build. 260 | // 261 | const buildDir = this.buildDir_ = temp.mkdirSync(); 262 | log.verbose(`Temporary build directory: ${buildDir}`); 263 | 264 | // 265 | // Destroy existing build engine. 266 | // 267 | if (this.engine_) { 268 | log.debug(`Destroying existing build engine...`); 269 | this.engine_.stopWorkers(); 270 | this.engine_.removeAllListeners(); 271 | temp.cleanupSync(); 272 | delete this.engine_; 273 | } 274 | 275 | // 276 | // Create build engine and start in watch mode. 277 | // 278 | const engine = this.engine_ = new Engine( 279 | buildSpec, 280 | buildDir, 281 | this.buildConfig_, 282 | this.projectRoot_, 283 | this.opt_); 284 | engine.startWorkers((err) => { 285 | if (err) { 286 | this.failBuild({ 287 | reason: 'worker_error', 288 | text: err.message, 289 | }); 290 | engine.stopWorkers(); 291 | return; 292 | } 293 | log.debug(`All worker processes ready.`); 294 | engine.watch(); 295 | }); 296 | 297 | engine.on('build_status', (pageBuildStatus) => { 298 | this.pageBuildStatus_ = pageBuildStatus; 299 | this.broadcast(this.getBuildState()); 300 | if (pageBuildStatus.overall === 'done') { 301 | buildDigest(this.buildDir_, (err, buildId) => { 302 | if (err) { 303 | log.error(err); 304 | } 305 | this.buildId_ = buildId; 306 | this.succeedBuild(); 307 | }); 308 | } 309 | if (pageBuildStatus.overall === 'fail') { 310 | this.failBuild({ 311 | reason: 'build_failed', 312 | }); 313 | } 314 | }); 315 | } 316 | } 317 | 318 | module.exports = DevServer; 319 | -------------------------------------------------------------------------------- /lib/server/buildDigest.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | fstate = require('../util/fstate'), 3 | digest = require('../util/digest'), 4 | hashcache = require('../util/hashcache'), 5 | log = require('../util/log') 6 | ; 7 | 8 | 9 | function buildDigest(buildDir, cb) { 10 | fstate(buildDir, (err, files) => { 11 | if (err) return cb(err); 12 | 13 | const buildId = digest(_.map( 14 | _.sortBy(files, 'relPath'), 15 | fileInfo => hashcache(fileInfo.absPath)).join(' '), 'base64'); 16 | 17 | log.debug(`Current build-digest for ${buildDir} is "${buildId}"`); 18 | 19 | return cb(null, buildId); 20 | }); 21 | } 22 | 23 | module.exports = _.debounce(buildDigest, 300); 24 | -------------------------------------------------------------------------------- /lib/server/reloader.js: -------------------------------------------------------------------------------- 1 | // ============================================================================= 2 | // The script below is dynamically injected by Martinet in order to 3 | // automatically trigger a page refresh when your source project is rebuilt. 4 | // 5 | // Your source files have not been modified on disk. 6 | // The code below will not be present when Martinet is run in "prod" configuration. 7 | // ============================================================================= 8 | /* eslint-disable */ 9 | function __Martinet__Reloader() { 10 | var lastBuildId, curtainElem; 11 | (function reconnect() { 12 | var location = window.location; 13 | var url = location.origin.replace(/^http/, 'ws') + '/__martinet__/ws'; 14 | var websocket = new WebSocket(url); 15 | websocket.onmessage = function(message) { 16 | const buildState = JSON.parse(message.data); 17 | 18 | if (buildState.status === 'error') { 19 | const newLocation = ( 20 | '/__martinet__/status/?bounceBack=' + 21 | encodeURIComponent(location.href)); 22 | location.href = location.origin + newLocation; 23 | return; 24 | } 25 | 26 | if (buildState.status === 'building') { 27 | if (!curtainElem) { 28 | curtainElem = document.createElement('div'); 29 | curtainElem.style.position = 'fixed'; 30 | curtainElem.style.left = 0; 31 | curtainElem.style.top = 0; 32 | curtainElem.style.width = '100%'; 33 | curtainElem.style.height = '100vh'; 34 | curtainElem.style['font-size'] = '22px'; 35 | curtainElem.style['background-color'] = 'white'; 36 | curtainElem.style['opacity'] = 0.9; 37 | curtainElem.style['text-align'] = 'center'; 38 | curtainElem.style['font-weight'] = 700; 39 | curtainElem.style['padding-top'] = '25%'; 40 | curtainElem.style['z-index'] = 16777271; 41 | curtainElem.textContent = 'Building...'; 42 | document.body.appendChild(curtainElem); 43 | } 44 | return; 45 | } 46 | 47 | if (buildState.buildId) { 48 | if (curtainElem) { 49 | curtainElem.remove(); 50 | curtainElem = null; 51 | } 52 | if (!lastBuildId) { 53 | lastBuildId = buildState.buildId; 54 | console.log('Martinet: current build ID is', lastBuildId); 55 | } else { 56 | if (lastBuildId !== buildState.buildId) { 57 | return location.reload(true); 58 | } 59 | } 60 | } 61 | }; 62 | websocket.onclose = function(error) { 63 | setTimeout(reconnect, 750); 64 | }; 65 | websocket.addEventListener('open', function() { 66 | console.log( 67 | 'Martinet: auto-refreshing this window on source changes.'); 68 | try { 69 | var saved = JSON.parse( 70 | window.localStorage.getItem('__martinet__')); 71 | window.scrollTo(saved.scrollX, saved.scrollY); 72 | console.log('Martinet: restored scroll position to', 73 | '(' + saved.scrollX + ', ' + saved.scrollY + ')'); 74 | } catch(e) { } 75 | }); 76 | window.onbeforeunload = function() { 77 | var saved = JSON.stringify({ 78 | scrollX: window.scrollX || 0, 79 | scrollY: window.scrollY || 0 80 | }); 81 | try { 82 | window.localStorage.setItem('__martinet__', saved); 83 | } catch(e) { 84 | console.error('Martinet:', e); 85 | } 86 | }; 87 | })(); 88 | } 89 | document.addEventListener('DOMContentLoaded', __Martinet__Reloader); 90 | // ============================================================================= 91 | // The script above is dynamically injected by Martinet. 92 | // ============================================================================= 93 | -------------------------------------------------------------------------------- /lib/server/statuspage/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var STATUS; 3 | 4 | 5 | function CreateNewSocket($scope, $location) { 6 | const wsUrl = window.location.origin.replace(/^http/, 'ws') + '/__martinet__/ws'; 7 | const bounceBackUrl = $location.search().bounceBack; 8 | 9 | if ($scope.$ws) { 10 | try { 11 | $scope.$ws.close(); 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | } 16 | var ws = $scope.$ws = new WebSocket(wsUrl); 17 | 18 | ws.addEventListener('open', function() { 19 | CreateNewSocket.backoffMs = 150; 20 | }); 21 | 22 | ws.addEventListener('message', function(msgEvt) { 23 | const msgData = JSON.parse(msgEvt.data); 24 | 25 | if (msgData.status === 'done' && bounceBackUrl) { 26 | window.location.href = decodeURIComponent(bounceBackUrl); 27 | return; 28 | } 29 | 30 | setTimeout(function() { 31 | $scope.$apply(function() { 32 | $scope.state = msgData; 33 | STATUS = msgData; 34 | }); 35 | }, 50); 36 | }); 37 | 38 | ws.addEventListener('close', function() { 39 | var nbf = CreateNewSocket.backoffMs; 40 | CreateNewSocket.backoffMs = nbf = Math.min(nbf * 2, 5000); 41 | console.log('Connection dropped, trying again in', nbf); 42 | setTimeout(function() { CreateNewSocket($scope); }, nbf); 43 | }); 44 | } 45 | 46 | 47 | function MainCtrl($scope, $location) { 48 | CreateNewSocket.backoffMs = 150; 49 | CreateNewSocket($scope, $location); 50 | } 51 | 52 | 53 | MainCtrl.prototype.setPageDetail = function(pageDist) { 54 | try { 55 | this.pageDetails = STATUS.build.pageStates[pageDist]; 56 | } catch (e) { 57 | console.warn(e); 58 | return; 59 | } 60 | }; 61 | 62 | 63 | angular 64 | .module('app', []) 65 | .config(function($locationProvider) { 66 | $locationProvider.html5Mode({ 67 | enabled: true, 68 | requireBase: false 69 | }); 70 | }) 71 | .controller('MainCtrl', MainCtrl); 72 | -------------------------------------------------------------------------------- /lib/server/statuspage/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/lib/server/statuspage/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /lib/server/statuspage/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/lib/server/statuspage/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /lib/server/statuspage/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/lib/server/statuspage/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /lib/server/statuspage/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/lib/server/statuspage/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /lib/server/statuspage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Martinet Build Status 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 |
16 | 17 | 18 |
19 |

Initializing...

20 |
21 | 22 | 23 |
24 |

Building...

25 |
26 | 27 | 28 |
29 |

Martinet: build error

30 |
31 | 32 | 33 |
34 |

Error parsing martinet.json

35 |
{{state.error.text}}
36 |
37 | 38 | 39 |
40 |

Error creating build worker child processes.

41 |
{{state.error.text}}
42 |
43 | 44 | 45 |
46 |
47 | 48 |
49 |
50 | 51 | 52 |
53 |

Martinet: build OK

54 |
55 | 56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 73 | 76 | 85 | 86 | 87 | 88 |
PageStatusErrorsWarnings
74 | {{pageOut}} 75 | 77 | 82 | {{pageState.status}} 83 | 84 | {{pageState.errors.length}}{{pageState.warnings.length}}
89 |
90 | 91 | 92 |
93 |
94 |

Errors

95 |
{{err}}
96 |
97 |
98 |

Errors

99 |
{{warn}}
100 |
101 | 102 |
103 |

Assets

104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
NameSizeChunks
{{asset.name}}{{asset.size | number}}{{asset.chunkNames.join(', ')}}
116 |
117 |
118 |
119 | 120 |
121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /lib/server/statuspage/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/lib/server/statuspage/spinner.gif -------------------------------------------------------------------------------- /lib/server/statuspage/style.css: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | } 4 | 5 | code { 6 | color: #333; 7 | } 8 | 9 | .panel { 10 | margin-top: 5px; 11 | margin-bottom: 5px; 12 | border-radius: 4px; 13 | } 14 | 15 | .panel-heading { 16 | cursor: pointer; 17 | } 18 | 19 | .metadata { 20 | font-size: 12px; 21 | } 22 | 23 | pre code { 24 | font-size: 14px; 25 | line-height: 105%; 26 | } 27 | 28 | .build-error { 29 | background-color: red; 30 | color: white; 31 | } 32 | 33 | .build-error a { 34 | color: white; 35 | } 36 | 37 | .build-warning { 38 | background-color: #ffa6af; 39 | } 40 | 41 | .fixed-cell { 42 | width: 75px; 43 | text-align: center; 44 | } 45 | -------------------------------------------------------------------------------- /lib/util/arghelp.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | col = require('colors'), 3 | pkgVer = require('../../package.json').version, 4 | spawnSync = require('child_process').spawnSync, 5 | Commands = require('../commands') 6 | ; 7 | 8 | 9 | function ShowCLIUsage() { 10 | const maxCmdLen = Math.max(10, _.max(_.map(Commands, (cmdDef, cmdName) => cmdName.length))); 11 | const cmdGroups = _.orderBy(_.toPairs(_.groupBy(Commands, 'helpGroup')), 12 | (pair) => { 13 | return _.min(_.map(pair[1], 'helpPriority')); 14 | }); 15 | 16 | const cmdGroupHelp = _.map(cmdGroups, (pair) => { 17 | const groupName = pair[0]; 18 | const cmdList = pair[1]; 19 | return [ 20 | ` * ${col.yellow(groupName)}`, 21 | _.map( 22 | _.orderBy(_.values(cmdList), 'helpPriority'), 23 | cmdDef => (`${col.bold(_.padStart(cmdDef.name, maxCmdLen))}: ` + 24 | `${col.dim(cmdDef.desc)}`)).join('\n'), 25 | '', 26 | ].join('\n'); 27 | }).join('\n'); 28 | 29 | console.error([ 30 | `${col.yellow.bold(`Martinet ${pkgVer}`)}: Webpack based static site generator`, 31 | `Usage: martinet ${col.bold('')} [options]`, 32 | 'Commands:', 33 | '', 34 | cmdGroupHelp, 35 | `Use ${'martinet -h'.bold}${' to see command-specific help.'.dim}`, 36 | ].join('\n')); 37 | 38 | return 1; 39 | } 40 | 41 | 42 | function ShowVersion() { 43 | let latestVer = spawnSync('npm show @iceroad/martinet version', { 44 | shell: true, 45 | stdio: 'pipe', 46 | }).stdout; 47 | let latestVerStr; 48 | if (latestVer) { 49 | latestVer = latestVer.toString('utf-8').replace(/\s/gm, ''); 50 | latestVerStr = `, latest version available: ${col.bold(latestVer)}`; 51 | } 52 | console.error(`@iceroad/martinet ${pkgVer}${latestVerStr}`); 53 | return 0; 54 | } 55 | 56 | 57 | function ShowCommandHelp(argspec, argname) { 58 | const publicFlags = _.filter(argspec, (aspec) => { 59 | return !aspec.private; 60 | }); 61 | 62 | // 63 | // Help message header. 64 | // 65 | const header = _.filter([ 66 | 'Usage: '.dim + `martinet ${argname}`.bold, 67 | publicFlags.length ? ' '.yellow : null, 68 | publicFlags.length ? `\n${'Options:\n'.dim}` : null, 69 | ]).join(''); 70 | 71 | // 72 | // Flag details. 73 | // 74 | const flagAliases = _.map(publicFlags, (aspec) => { 75 | return _.map(aspec.flags, (alias) => { 76 | if (!alias) return alias; 77 | return `-${alias.length > 1 ? '-' : ''}${alias}`; 78 | }).join(', '); 79 | }); 80 | const flagDescriptions = _.map(publicFlags, 'desc'); 81 | const longestAliasStr = _.max([10, _.max(_.map(flagAliases, _.size))]); 82 | const flagDetails = _.map(_.zip( 83 | flagAliases, flagDescriptions, publicFlags), (ftriple) => { 84 | let flagDefVal = ftriple[2].defVal; 85 | 86 | // Indent multi-line descriptions 87 | const flagDesc = ftriple[2].desc; 88 | let descLines = _.map(flagDesc.split('\n'), (lineStr, idx) => { 89 | return ((idx ? _.padEnd('', longestAliasStr + 5) : '') + lineStr); 90 | }); 91 | const longestDescStr = _.max(_.map(descLines, 'length')); 92 | descLines = descLines.join('\n'); 93 | 94 | if (_.isObject(flagDefVal)) { 95 | flagDefVal = flagDefVal.value; 96 | } 97 | 98 | return `${[ 99 | ' ', 100 | _.padEnd(ftriple[0], longestAliasStr).yellow, 101 | ' ', 102 | descLines, 103 | ].join(' ')}\n${[ 104 | ' ', 105 | _.padEnd('', longestAliasStr), 106 | 'Default: '.gray + ( 107 | (!_.isUndefined(flagDefVal)) ? 108 | flagDefVal.toString().yellow.dim : 109 | ' (must specify)'.dim.yellow), 110 | ].join(' ')}\n ${ 111 | _.padEnd('', longestAliasStr)}${_.padEnd('', longestDescStr, '─')}\n`; 112 | }); 113 | 114 | console.error(`${_.concat(header, flagDetails).join('\n')}`); 115 | return 1; 116 | } 117 | 118 | 119 | module.exports = { 120 | ShowCommandHelp, 121 | ShowCLIUsage, 122 | ShowVersion, 123 | }; 124 | -------------------------------------------------------------------------------- /lib/util/argparse.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | 4 | module.exports = function argparse(argspec, args) { 5 | if (!_.isObject(argspec)) { 6 | throw new Error('require an object argument for "argspec".'); 7 | } 8 | if (!_.isObject(args)) { 9 | throw new Error('require an object argument for "args".'); 10 | } 11 | 12 | // 13 | // Build a map of all flag aliases. 14 | // 15 | const allAliases = _.fromPairs(_.map(_.flatten(_.map(argspec, 'flags')), (flagAlias) => { 16 | return [flagAlias, true]; 17 | })); 18 | 19 | // 20 | // Make sure all provided flags are specified in the argspec. 21 | // 22 | _.forEach(args, (argVal, argKey) => { 23 | if (argKey === '_') return; // ignore positional arguments 24 | if (!(argKey in allAliases)) { 25 | throw new Error(`Unknown command-line argument "${argKey}" specified.`); 26 | } 27 | }); 28 | 29 | // 30 | // Handle boolean flags specified as string values on the command line. 31 | // 32 | args = _.mapValues(args, (flagVal) => { 33 | if (_.isString(flagVal)) { 34 | if (flagVal === 'true') { 35 | return true; 36 | } 37 | if (flagVal === 'false') { 38 | return false; 39 | } 40 | } 41 | return flagVal; 42 | }); 43 | 44 | // 45 | // For each flag in the argspec, see if any alias of the flag is 46 | // present in `args`. If it is, then assign that value to *all* 47 | // aliases of the flag. If it is not present, then assign the 48 | // default value. Throw on unspecified required flags. 49 | // 50 | const finalFlags = {}; 51 | _.forEach(argspec, (aspec) => { 52 | // Find the first alias of this flag that is specified in args, if at all. 53 | const foundAlias = _.find(aspec.flags, (flagAlias) => { 54 | return flagAlias && (flagAlias in args); 55 | }); 56 | let assignValue = foundAlias ? args[foundAlias] : aspec.defVal; 57 | 58 | // If defVal is an object, then we need to execute a function to get the 59 | // default value. 60 | if (_.isObject(assignValue)) { 61 | if (!_.isFunction(assignValue.fn) || !assignValue.desc) { 62 | throw new Error('Specification has an unrecognized "defVal" entry.'); 63 | } 64 | assignValue = assignValue.fn.call(argspec); 65 | } 66 | 67 | // Assign value to all aliases of flag. 68 | _.forEach(aspec.flags, (flagAlias) => { 69 | finalFlags[flagAlias] = assignValue; 70 | }); 71 | }); 72 | 73 | // Copy positional arguments to finalFlags. 74 | finalFlags._ = args._; 75 | 76 | return finalFlags; 77 | }; 78 | -------------------------------------------------------------------------------- /lib/util/callsite.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | fmt = require('util').format, 3 | path = require('path') 4 | ; 5 | 6 | 7 | const PROJ_ROOT = path.resolve(__dirname, '..', '..', 'lib'); 8 | const STACK_LINE_RE_1 = /^\s+at (.+) \((\S+):(\d+):(\d+)\)/; 9 | const STACK_LINE_RE_2 = /^\s+at (.+):(\d+):(\d+)/; 10 | 11 | module.exports = function getCallsite(stack) { 12 | // 13 | // This is a horrible, ugly, necessary way to extract information. 14 | // 15 | if (!stack) { 16 | stack = (new Error()).stack.toString().split('\n'); 17 | } else { 18 | stack = stack.toString().split('\n'); 19 | } 20 | 21 | // 22 | // Extract callsites from stack lines. 23 | // 24 | let cleanStack = _.filter(_.map(stack, (stackLine) => { 25 | stackLine = stackLine.replace(/\[as .*?\]\s*/, ''); 26 | 27 | // 28 | // Try pattern 1. 29 | // 30 | let parts = STACK_LINE_RE_1.exec(stackLine); 31 | if (parts && parts.length) { 32 | return { 33 | symbol: parts[1], 34 | absPath: parts[2], 35 | line: _.toInteger(parts[3]), 36 | column: _.toInteger(parts[4]), 37 | }; 38 | } 39 | 40 | // 41 | // Try pattern 2. 42 | // 43 | parts = STACK_LINE_RE_2.exec(stackLine); 44 | if (parts && parts.length) { 45 | return { 46 | absPath: parts[1], 47 | line: _.toInteger(parts[2]), 48 | column: _.toInteger(parts[3]), 49 | }; 50 | } 51 | })); 52 | 53 | // 54 | // Filter out files in our lib project. 55 | // 56 | cleanStack = _.filter(_.map(cleanStack, (stackLine) => { 57 | if (stackLine.absPath.substr(0, PROJ_ROOT.length) === PROJ_ROOT) { 58 | stackLine.path = path.relative(PROJ_ROOT, stackLine.absPath); 59 | delete stackLine.absPath; 60 | return stackLine; 61 | } 62 | return stackLine; 63 | })); 64 | 65 | // 66 | // Filter out syslog and callsite 67 | // 68 | cleanStack = _.filter(cleanStack, (stackLine) => { 69 | const symbol = stackLine.symbol || ''; 70 | const symPath = stackLine.path || ''; 71 | if (symbol === getCallsite.name) { 72 | return; 73 | } 74 | if (symbol.match(/^SysLog/)) { 75 | return; 76 | } 77 | if (symPath.match(/^log\//)) { 78 | return; 79 | } 80 | return true; 81 | }); 82 | 83 | const result = { 84 | clean: cleanStack, 85 | full: stack.toString(), 86 | }; 87 | if (cleanStack.length) { 88 | result.summary = fmt( 89 | '%s:%d', cleanStack[0].path, cleanStack[0].line); 90 | } 91 | 92 | return result; 93 | }; 94 | -------------------------------------------------------------------------------- /lib/util/digest.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | ; 3 | 4 | 5 | function sha256(str, encoding) { 6 | const hasher = crypto.createHash('sha256'); 7 | hasher.update(str); 8 | return hasher.digest(encoding); 9 | } 10 | 11 | 12 | module.exports = sha256; 13 | -------------------------------------------------------------------------------- /lib/util/fstate.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | path = require('path'), 3 | walk = require('walk') 4 | ; 5 | 6 | 7 | function fstate(dir, opt, cb) { 8 | if (_.isFunction(opt) && _.isUndefined(cb)) { 9 | cb = opt; 10 | opt = {}; 11 | } 12 | const files = []; 13 | const errors = []; 14 | const walker = walk.walk(dir, { 15 | followLinks: true, 16 | }); 17 | 18 | walker.on('directory', (root, dirStat, next) => { 19 | if (opt.includeDirectories) { 20 | const absPath = path.join(root, dirStat.name); 21 | const relPath = path.relative(dir, absPath); 22 | files.push({ 23 | absPath, 24 | relPath, 25 | mtime: dirStat.mtime, 26 | name: dirStat.name, 27 | type: 'dir', 28 | }); 29 | } 30 | return next(); 31 | }); 32 | 33 | walker.on('file', (root, fileStat, next) => { 34 | // Filter out hidden files and directories in the listing. 35 | if (fileStat.name[0] !== '.') { 36 | // Assemble return structure. 37 | const absPath = path.join(root, fileStat.name); 38 | const relPath = path.relative(dir, absPath); 39 | files.push({ 40 | absPath, 41 | relPath, 42 | size: fileStat.size, 43 | mtime: fileStat.mtime, 44 | name: fileStat.name, 45 | type: 'file', 46 | }); 47 | } 48 | return next(); 49 | }); 50 | 51 | walker.on('errors', (root, nodeStatsArray, next) => { 52 | const firstErr = nodeStatsArray[0]; 53 | errors.push(`File: ${firstErr.error.path}, error: "${firstErr.error.code}"`); 54 | return next(); 55 | }); 56 | 57 | walker.once('end', () => { 58 | return cb(null, _.sortBy(files, 'absPath'), errors); 59 | }); 60 | } 61 | 62 | 63 | module.exports = fstate; 64 | -------------------------------------------------------------------------------- /lib/util/hashcache.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require, import/no-dynamic-require */ 2 | const _ = require('lodash'), 3 | fs = require('fs'), 4 | digest = require('./digest'), 5 | os = require('os'), 6 | path = require('path') 7 | ; 8 | 9 | 10 | const CACHE_PATH = path.join(os.tmpdir(), '.hashcache.json'); 11 | const CACHE = (function loadCache() { 12 | if (fs.existsSync(CACHE_PATH)) { 13 | try { 14 | return require(CACHE_PATH); 15 | } catch (e) { 16 | console.warn(`Unable to read file hash cache from ${CACHE_PATH}: ${e}`); 17 | return process.exit(1); 18 | } 19 | } 20 | return {}; 21 | }()); 22 | 23 | 24 | process.once('exit', () => { 25 | if (!_.isEmpty(CACHE)) { 26 | fs.writeFileSync(CACHE_PATH, JSON.stringify(CACHE), 'utf-8'); 27 | } 28 | }); 29 | 30 | 31 | function hashcache(absPath) { 32 | const realPath = fs.realpathSync(absPath); 33 | const curMtime = fs.statSync(absPath).mtime.getTime(); 34 | 35 | const cacheItem = CACHE[realPath]; 36 | if (cacheItem) { 37 | if (cacheItem.mtime === curMtime) { 38 | return cacheItem.hash; 39 | } 40 | } 41 | 42 | const hash = digest(fs.readFileSync(absPath)).toString('base64'); 43 | 44 | CACHE[realPath] = { 45 | mtime: curMtime, 46 | hash, 47 | }; 48 | 49 | return hash; 50 | } 51 | 52 | module.exports = hashcache; 53 | -------------------------------------------------------------------------------- /lib/util/log.js: -------------------------------------------------------------------------------- 1 | const col = require('colors'), 2 | callsite = require('./callsite'), 3 | fmt = require('util').format 4 | ; 5 | 6 | const LEVELS = { 7 | NORMAL: 1, 8 | INFO: 2, 9 | VERBOSE: 3, 10 | DEBUG: 4, 11 | }; 12 | 13 | 14 | class SysLog { 15 | // 16 | // Base logging function. 17 | // 18 | baseLog(colFn, logStream, ...args) { 19 | console.log([ 20 | colFn(`${logStream}:`), 21 | col.blue(callsite().summary), 22 | colFn(fmt(...args)), 23 | ].join(' ')); 24 | } 25 | 26 | // 27 | // Stream aliases 28 | // 29 | info(...args) { 30 | if (process.env.LOGLEVEL >= LEVELS.INFO) { 31 | this.baseLog(col.green, 'info', ...args); 32 | } 33 | } 34 | 35 | verbose(...args) { 36 | if (process.env.LOGLEVEL >= LEVELS.VERBOSE) { 37 | this.baseLog(col.dim, 'verbose', ...args); 38 | } 39 | } 40 | 41 | 42 | debug(...args) { 43 | if (process.env.LOGLEVEL >= LEVELS.DEBUG) { 44 | this.baseLog(col.dim, 'debug', ...args); 45 | } 46 | } 47 | 48 | 49 | fatal(...args) { 50 | this.baseLog(col.red, 'fatal', ...args); 51 | } 52 | 53 | 54 | error(...args) { 55 | this.baseLog(col.red, 'error', ...args); 56 | } 57 | 58 | 59 | warn(...args) { 60 | this.baseLog(col.yellow, 'warning', ...args); 61 | } 62 | 63 | 64 | warning(...args) { 65 | this.baseLog(col.yellow, 'warning', ...args); 66 | } 67 | } 68 | 69 | 70 | SysLog.prototype.LEVELS = LEVELS; 71 | 72 | module.exports = new SysLog(); 73 | -------------------------------------------------------------------------------- /lib/util/syncDir.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'), 2 | assert = require('assert'), 3 | async = require('async'), 4 | col = require('colors'), 5 | fs = require('fs'), 6 | fse = require('fs-extra'), 7 | log = require('./log'), 8 | fstate = require('./fstate'), 9 | hashcache = require('./hashcache'), 10 | path = require('path') 11 | ; 12 | 13 | 14 | function ConditionalCopy(srcFile, destPath) { 15 | const newHash = hashcache(srcFile); 16 | let currentHash; 17 | if (fs.existsSync(destPath)) { 18 | currentHash = hashcache(destPath); 19 | } 20 | if (currentHash !== newHash) { 21 | fse.ensureDirSync(path.dirname(destPath)); 22 | fse.copySync(srcFile, destPath); 23 | log.verbose(`Copied ${col.bold(srcFile)} ⇒ ${col.bold(destPath)}`.yellow); 24 | return 1; 25 | } 26 | 27 | log.verbose(`Unchanged ${srcFile}`.gray); 28 | return 0; 29 | } 30 | 31 | function syncDir(srcDir, destDir, cb) { 32 | assert(fs.existsSync(srcDir), `Source directory ${srcDir} does not exist.`); 33 | assert(fs.existsSync(destDir), `Destination directory ${destDir} does not exist.`); 34 | 35 | async.auto({ 36 | srcListing: cb => fstate(srcDir, { includeDirectories: true }, cb), 37 | destListing: cb => fstate(destDir, { includeDirectories: true }, cb), 38 | sync: ['srcListing', 'destListing', (deps, cb) => { 39 | const srcFiles = _.filter(deps.srcListing[0], file => file.type === 'file'); 40 | const destFiles = _.filter(deps.destListing[0], file => file.type === 'file'); 41 | const srcFileRelPaths = _.sortBy(_.map(srcFiles, 'relPath')); 42 | const destFileRelPaths = _.sortBy(_.map(destFiles, 'relPath')); 43 | 44 | // Copy source files to output if contents do not match. 45 | const numCopied = _.sum(_.map(srcFiles, (srcFile) => { 46 | const destPath = path.join(destDir, srcFile.relPath); 47 | return ConditionalCopy(srcFile.absPath, destPath); 48 | })); 49 | 50 | if (numCopied) { 51 | log.info( 52 | `Copied ${col.bold(numCopied)} changed source files from ` + 53 | `${col.bold(srcDir)} to ${col.bold(destDir)}.`); 54 | } 55 | 56 | // Delete files in destination that are not in source. 57 | const filesToDelete = _.difference(destFileRelPaths, srcFileRelPaths); 58 | _.forEach(filesToDelete, (delFileRelPath) => { 59 | const delFile = path.join(destDir, delFileRelPath); 60 | fse.removeSync(delFile); 61 | log.verbose(`Deleted ${col.bold(delFile)}`.red); 62 | }); 63 | let numDeleted = filesToDelete.length; 64 | 65 | if (numDeleted) { 66 | log.info( 67 | `Deleted ${col.bold(numDeleted)} unneccessary files from ` + 68 | `${col.bold(destDir)}.`); 69 | } 70 | 71 | // Delete dangling directories. 72 | const srcDirs = _.filter(deps.srcListing[0], file => file.type === 'dir'); 73 | const destDirs = _.filter(deps.destListing[0], file => file.type === 'dir'); 74 | const srcDirRelPaths = _.sortBy(_.map(srcDirs, 'relPath')); 75 | const destDirRelPaths = _.sortBy(_.map(destDirs, 'relPath')); 76 | const dirsToDelete = _.difference(destDirRelPaths, srcDirRelPaths); 77 | _.forEach(dirsToDelete, (delDirRelPath) => { 78 | const delDir = path.join(destDir, delDirRelPath); 79 | fse.removeSync(delDir); 80 | log.verbose(`Deleted directory ${col.bold(delDir)}`.red); 81 | numDeleted++; 82 | }); 83 | 84 | if (!(numCopied || numDeleted)) { 85 | log.info('Build results unchanged from existing build.'); 86 | } 87 | 88 | return cb(); 89 | }], 90 | }, cb); 91 | } 92 | 93 | 94 | module.exports = syncDir; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@iceroad/martinet", 3 | "version": "1.1.1", 4 | "description": "A simplified build system for static sites and single-page webapps, based on Webpack 2.", 5 | "main": "lib/engine", 6 | "scripts": { 7 | "test": "node_modules/.bin/mocha --recursive test" 8 | }, 9 | "bin": { 10 | "martinet": "./lib/cli.js", 11 | "please": "./lib/cli.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:iceroad/martinet.git" 16 | }, 17 | "keywords": [ 18 | "build-system", 19 | "webpack", 20 | "static-site", 21 | "sitegen", 22 | "site-generator", 23 | "single-page-app", 24 | "esnext", 25 | "es6", 26 | "es2015", 27 | "react", 28 | "less", 29 | "pug-lang", 30 | "tree-shaking" 31 | ], 32 | "author": "Mayank Lahiri ", 33 | "license": "MIT", 34 | "dependencies": { 35 | "angular2-template-loader": "^0.6.2", 36 | "async": "^2.5.0", 37 | "awesome-typescript-loader": "^3.2.1", 38 | "aws-sdk": "^2.88.0", 39 | "babel-core": "^6.25.0", 40 | "babel-loader": "^7.1.1", 41 | "babel-preset-env": "^1.6.0", 42 | "babel-preset-react": "^6.24.1", 43 | "colors": "^1.1.2", 44 | "copy-webpack-plugin": "^4.0.1", 45 | "css-loader": "^0.28.4", 46 | "express": "^4.15.3", 47 | "extract-text-webpack-plugin": "^3.0.0", 48 | "file-loader": "^0.11.2", 49 | "fs-extra": "^4.0.0", 50 | "html-loader": "^0.4.5", 51 | "html-webpack-plugin": "^2.29.0", 52 | "json-loader": "^0.5.4", 53 | "json-stable-stringify": "^1.0.1", 54 | "jsonlint": "^1.6.2", 55 | "less": "^2.7.2", 56 | "less-loader": "^4.0.5", 57 | "lodash": "^4.17.4", 58 | "mime": "^1.3.6", 59 | "minimist": "^1.2.0", 60 | "moment": "^2.18.1", 61 | "pug-html-loader": "^1.1.5", 62 | "runtype": "^0.2.3", 63 | "style-loader": "^0.18.2", 64 | "temp": "^0.8.3", 65 | "typescript": "^2.4.2", 66 | "url-loader": "^0.5.9", 67 | "vue-loader": "^12.2.2", 68 | "vue-template-compiler": "^2.4.1", 69 | "walk": "^2.3.9", 70 | "webpack": "^3.3.0", 71 | "ws": "^3.0.0" 72 | }, 73 | "engines": { 74 | "node": ">=6.0.0" 75 | }, 76 | "devDependencies": { 77 | "chai": "^4.1.0", 78 | "istanbul": "^0.4.5", 79 | "mocha": "^3.4.2", 80 | "pre-commit": "^1.2.2" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/engine/compile-constants.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | spawnSync = require('child_process').spawnSync, 8 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 9 | temp = require('temp').track(), 10 | Engine = require('../../lib/engine/Engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: compile-time constants for conditional compilation', function() { 18 | this.slow(5000); 19 | this.timeout(10000); 20 | 21 | it('should build a page with with conditional compilation (prod)', 22 | (cb) => { 23 | const projRoot = path.join(TEST_DATA, 'compile-constants'); 24 | const buildSpec = readBuildSpec(projRoot); 25 | const outputRoot = temp.mkdirSync(); 26 | const engine = new Engine(buildSpec, outputRoot, 'prod', projRoot); 27 | engine.build((err, allStats) => { 28 | if (err) { 29 | console.error(allStats); 30 | return cb(err); 31 | } 32 | 33 | // Ensure compilation was successful. 34 | const stats = allStats[0]; // first page in build specification 35 | assert.isFalse(stats.hasErrors()); 36 | 37 | // Ensure output directory matches expectation. 38 | const outDirContents = fs.readdirSync(outputRoot); 39 | assert.deepEqual( 40 | _.sortBy(outDirContents), 41 | ['__ver__', 'index.html']); 42 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 43 | assert.strictEqual(versionedFiles.length, 1); 44 | 45 | // Read js file, ensure conditional compilation occurred. 46 | let jsPath = path.join(outputRoot, '__ver__', versionedFiles[0]); 47 | let js = fs.readFileSync(jsPath, 'utf-8'); 48 | assert.match(js, /config:prod/mi); 49 | assert.notMatch(js, /config:dev/mi); 50 | assert.notMatch(js, /config:unknown/mi); 51 | 52 | 53 | // Ensure that the Pug file had access to $martinet. 54 | const html = fs.readFileSync(path.join(outputRoot, 'index.html'), 'utf-8'); 55 | assert.notMatch(html, /pug_sentinel:dev/i); 56 | assert.notMatch(html, /pug_sentinel:unknown/i); 57 | assert.match(html, /pug_sentinel:prod/i); 58 | assert.match(html, /public_path::/i); 59 | 60 | return cb(); 61 | }); 62 | }); 63 | 64 | it('should build a page with with conditional compilation (dev)', 65 | (cb) => { 66 | const projRoot = path.join(TEST_DATA, 'compile-constants'); 67 | const buildSpec = readBuildSpec(projRoot); 68 | const outputRoot = temp.mkdirSync(); 69 | const engine = new Engine(buildSpec, outputRoot, 'dev', projRoot); 70 | engine.build((err, allStats) => { 71 | if (err) { 72 | console.error(allStats); 73 | return cb(err); 74 | } 75 | 76 | // Ensure compilation was successful. 77 | const stats = allStats[0]; // first page in build specification 78 | assert.isFalse(stats.hasErrors()); 79 | 80 | // Ensure output directory matches expectation. 81 | const outDirContents = fs.readdirSync(outputRoot); 82 | assert.deepEqual( 83 | _.sortBy(outDirContents), 84 | ['__ver__', 'index.html']); 85 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 86 | assert.strictEqual(versionedFiles.length, 1); 87 | 88 | // Read js file, all versions of conditional compilation should be 89 | // included (i.e., no code removal). 90 | let jsPath = path.join(outputRoot, '__ver__', versionedFiles[0]); 91 | let js = fs.readFileSync(jsPath, 'utf-8'); 92 | assert.match(js, /config:prod/mi); 93 | assert.match(js, /config:dev/mi); 94 | assert.match(js, /config:unknown/mi); 95 | 96 | // Finally execute bundle using node. 97 | const rv = spawnSync(process.execPath, [jsPath], { stdio: 'pipe' }); 98 | assert.strictEqual(0, rv.status); 99 | assert.match(rv.stdout.toString('utf-8'), /config:dev/mgi); 100 | assert.notMatch(rv.stdout.toString('utf-8'), /config:prod/mgi); 101 | assert.notMatch(rv.stdout.toString('utf-8'), /config:unknown/mgi); 102 | 103 | // Ensure that the Pug file had access to $martinet. 104 | const html = fs.readFileSync(path.join(outputRoot, 'index.html'), 'utf-8'); 105 | assert.match(html, /pug_sentinel:dev/i); 106 | assert.notMatch(html, /pug_sentinel:prod/i); 107 | assert.notMatch(html, /pug_sentinel:unknown/i); 108 | assert.match(html, /public_path::/i); 109 | 110 | return cb(); 111 | }); 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /test/engine/data-inject.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | spawnSync = require('child_process').spawnSync, 8 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 9 | temp = require('temp').track(), 10 | Engine = require('../../lib/engine/Engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: template+data only pages', function() { 18 | this.slow(5000); 19 | this.timeout(10000); 20 | 21 | 22 | it('should build a page with template and data, without styles or scripts (prod)', 23 | (cb) => { 24 | const projRoot = path.join(TEST_DATA, 'template-data-inject'); 25 | const buildSpec = readBuildSpec(projRoot); 26 | const outputRoot = temp.mkdirSync(); 27 | const engine = new Engine(buildSpec, outputRoot, 'prod', projRoot); 28 | engine.build((err, allStats) => { 29 | if (err) { 30 | console.error(allStats); 31 | return cb(err); 32 | } 33 | 34 | // Ensure compilation was successful. 35 | const stats = allStats[0]; // first page in build specification 36 | assert.isFalse(stats.hasErrors()); 37 | 38 | // Ensure output directory matches expectation. 39 | const outDirContents = fs.readdirSync(outputRoot); 40 | assert.deepEqual( 41 | _.sortBy(outDirContents), 42 | ['__ver__', 'index.html', 'no-data.html']); 43 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 44 | assert.strictEqual(versionedFiles.length, 1); 45 | 46 | // Read index.html, ensure sentinel from data file is present. 47 | let htmlPath = path.join(outputRoot, 'index.html'); 48 | let html = fs.readFileSync(htmlPath, 'utf-8'); 49 | assert.match(html, /12345678/mi); 50 | 51 | // Read no-data.html, ensure sentinel is not present. 52 | htmlPath = path.join(outputRoot, 'no-data.html'); 53 | html = fs.readFileSync(htmlPath, 'utf-8'); 54 | assert.notMatch(html, /12345678/mi); 55 | 56 | return cb(); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /test/engine/es-6.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 8 | spawnSync = require('child_process').spawnSync, 9 | temp = require('temp'), 10 | Engine = require('../../lib/engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: ES6 support via Babel', function() { 18 | this.slow(5000); 19 | this.timeout(10000); 20 | 21 | 22 | it('should compile a simple ES6 JS file (prod)', (cb) => { 23 | const projRoot = path.join(TEST_DATA, 'es6-js'); 24 | const outputRoot = temp.mkdirSync(); 25 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'prod', projRoot); 26 | engine.build((err, allStats) => { 27 | const stats = allStats[0]; 28 | 29 | // Ensure compilation was successful. 30 | assert.isNotOk(err); 31 | assert.isFalse(stats.hasErrors()); 32 | 33 | // Ensure no warnings compiling ES-latest code. 34 | assert.isFalse(stats.hasWarnings()); 35 | const warnings = _.get(stats, 'compilation.warnings', []); 36 | 37 | // Ensure output directory matches expectation. 38 | const outDirContents = fs.readdirSync(outputRoot); 39 | assert.deepEqual(_.sortBy(outDirContents), ['__ver__', 'index.html']); 40 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 41 | assert.strictEqual(versionedFiles.length, 1); 42 | 43 | // Read bundle, ensure both functions are present. 44 | const bundlePath = path.join(outputRoot, '__ver__', versionedFiles[0]); 45 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 46 | assert.match(bundle, /sentinel_1/mgi); 47 | assert.match(bundle, /sentinel_2/mgi); 48 | assert.notMatch(bundle, /\/\*\*/mgi); 49 | 50 | // Finally execute bundle using node. 51 | const rv = spawnSync(process.execPath, [bundlePath], { stdio: 'pipe' }); 52 | assert.strictEqual(0, rv.status); 53 | assert.match(rv.stdout.toString('utf-8'), /sentinel_1 1/mgi); 54 | assert.match(rv.stdout.toString('utf-8'), /sentinel_2 2/mgi); 55 | 56 | return cb(); 57 | }); 58 | }); 59 | 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /test/engine/import-static-json.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | spawnSync = require('child_process').spawnSync, 8 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 9 | temp = require('temp').track(), 10 | Engine = require('../../lib/engine/Engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: Javascript modules should be able to require static JSON', 18 | function() { 19 | this.slow(5000); 20 | this.timeout(10000); 21 | 22 | 23 | it('should include require()\' JSON inline (prod)', 24 | (cb) => { 25 | const projRoot = path.join(TEST_DATA, 'import-static-json'); 26 | const buildSpec = readBuildSpec(projRoot); 27 | const outputRoot = temp.mkdirSync(); 28 | const engine = new Engine(buildSpec, outputRoot, 'prod', projRoot); 29 | engine.build((err, allStats) => { 30 | if (err) { 31 | console.error(allStats); 32 | return cb(err); 33 | } 34 | 35 | // Ensure compilation was successful. 36 | const stats = allStats[0]; // first page in build specification 37 | assert.isFalse(stats.hasErrors()); 38 | 39 | // Ensure output directory matches expectation. 40 | const outDirContents = fs.readdirSync(outputRoot); 41 | assert.deepEqual( 42 | _.sortBy(outDirContents), 43 | ['__ver__', 'index.html']); 44 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 45 | assert.strictEqual(versionedFiles.length, 1); 46 | 47 | // Read js file, ensure conditional compilation occurred. 48 | let jsPath = path.join(outputRoot, '__ver__', versionedFiles[0]); 49 | let js = fs.readFileSync(jsPath, 'utf-8'); 50 | assert.match(js, /data_sentinel:{nested:12345}/i); 51 | 52 | return cb(); 53 | }); 54 | }); 55 | 56 | 57 | it('should include require()\'d JSON inline (dev)', 58 | (cb) => { 59 | const projRoot = path.join(TEST_DATA, 'import-static-json'); 60 | const buildSpec = readBuildSpec(projRoot); 61 | const outputRoot = temp.mkdirSync(); 62 | const engine = new Engine(buildSpec, outputRoot, 'dev', projRoot); 63 | engine.build((err, allStats) => { 64 | if (err) { 65 | console.error(allStats); 66 | return cb(err); 67 | } 68 | 69 | // Ensure compilation was successful. 70 | const stats = allStats[0]; // first page in build specification 71 | assert.isFalse(stats.hasErrors()); 72 | 73 | // Ensure output directory matches expectation. 74 | const outDirContents = fs.readdirSync(outputRoot); 75 | assert.deepEqual( 76 | _.sortBy(outDirContents), 77 | ['__ver__', 'index.html']); 78 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 79 | assert.strictEqual(versionedFiles.length, 1); 80 | 81 | // Read js file, ensure conditional compilation occurred. 82 | let jsPath = path.join(outputRoot, '__ver__', versionedFiles[0]); 83 | let js = fs.readFileSync(jsPath, 'utf-8'); 84 | assert.match(js, /data_sentinel/i); 85 | 86 | return cb(); 87 | }); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /test/engine/simple-static.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | fstate = require('../../lib/util/fstate'), 6 | log = require('../../lib/util/log'), 7 | json = JSON.stringify, 8 | path = require('path'), 9 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 10 | temp = require('temp'), 11 | Engine = require('../../lib/engine/Engine') 12 | ; 13 | 14 | 15 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 16 | 17 | 18 | describe('Engine: simple static site generation', function() { 19 | this.slow(5000); 20 | this.timeout(10000); 21 | 22 | 23 | it('should build a simple static site with assets (prod)', (cb) => { 24 | const projRoot = path.join(TEST_DATA, 'simple-static'); 25 | const outputRoot = temp.mkdirSync(); 26 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'prod', projRoot); 27 | engine.build((err, allStats) => { 28 | if (err) { 29 | console.error(allStats); 30 | return cb(err); 31 | } 32 | 33 | // 34 | // Examine build. 35 | // 36 | fstate(outputRoot, (err, files) => { 37 | if (err) return cb(err); 38 | 39 | // 40 | // Raw contents: 41 | // 42 | // * .map: 2 sourcemaps 43 | // * .html: 3: /index.html and /nested/inner/index.html /nested/inned/about.html 44 | // * .js: 2: one bundle each for /nested/inner/index and /index 45 | // * .jpg: /__ver__/test.*.jpg 46 | // * .css: /__ver__/style.*.css 47 | // 48 | const byExt = _.groupBy(files, fileInfo => path.extname(fileInfo.name).substr(1)); 49 | log.debug('Build output files:', json(byExt, null, 2)); 50 | //assert.strictEqual(byExt.ico.length, 1); 51 | assert.strictEqual(byExt.jpg.length, 1); 52 | assert.strictEqual(byExt.css.length, 1); 53 | assert.strictEqual(byExt.html.length, 3); 54 | assert.strictEqual(byExt.map.length, 2); 55 | assert.strictEqual(byExt.woff2.length, 1); 56 | 57 | // 58 | // Validate that URLs in generated HTML point to the right asset paths. 59 | // 60 | const htmlFiles = _.sortBy(byExt.html, 'relPath'); 61 | assert.deepEqual(_.map(htmlFiles, 'relPath'), [ 62 | 'index.html', 63 | 'nested/inner/about.html', 64 | 'nested/inner/index.html', 65 | ]); 66 | 67 | // Validate relative URLs: /index.html 68 | const indexHtml = fs.readFileSync(htmlFiles[0].absPath, 'utf-8'); 69 | assert.match(indexHtml, /link href="__ver__\/style.[a-f0-9]+\.css/mi); 70 | assert.match(indexHtml, /src="__ver__\/js.[a-f0-9]+\.bundle\.js/mi); 71 | assert.match(indexHtml, /img src=__ver__\/test\.[a-f0-9]+\.jpg/mi); 72 | 73 | // Validate relative URLs: /nested/inner/about.html 74 | const innerAbout = fs.readFileSync(htmlFiles[1].absPath, 'utf-8'); 75 | assert.match(innerAbout, /link href="..\/..\/__ver__\/style.[a-f0-9]+\.css/mi); 76 | assert.match(innerAbout, /src="..\/..\/__ver__\/js.[a-f0-9]+\.bundle\.js/mi); 77 | assert.match(innerAbout, /img src=..\/..\/__ver__\/test\.[a-f0-9]+\.jpg/mi); 78 | 79 | // Validate relative URLs: /nested/inner/index.html 80 | const innerIndex = fs.readFileSync(htmlFiles[2].absPath, 'utf-8'); 81 | assert.match(innerAbout, /link href="..\/..\/__ver__\/style.[a-f0-9]+\.css/mi); 82 | assert.match(innerAbout, /src="..\/..\/__ver__\/js.[a-f0-9]+\.bundle\.js/mi); 83 | assert.match(innerAbout, /img src=..\/..\/__ver__\/test\.[a-f0-9]+\.jpg/mi); 84 | 85 | // Validate that Pug mixins have been mixed in. 86 | assert.match(innerAbout, /mixin-sentinel/i); 87 | assert.match(innerAbout, /

456789<\/h2>/i); // from sample.json 88 | 89 | // 90 | // Validate that URLs in generated CSS point to the right asset paths. 91 | // 92 | const cssFiles = _.sortBy(byExt.css, 'relPath'); 93 | assert.match(byExt.css[0].relPath, /^__ver__\/style\.[0-9a-f]+\.css/mi); 94 | 95 | // Validate relative URLs: /__ver__/style.NNN.css 96 | const indexCss = fs.readFileSync(cssFiles[0].absPath, 'utf-8'); 97 | assert.match(indexCss, /url\(\.\.\/__ver__\/glyphicons.[a-f0-9]+\.woff2/mi); 98 | assert.match(indexCss, /url\(\.\.\/__ver__\/test.[a-f0-9]+\.jpg/mi); 99 | 100 | // Validate that the HTML files all include the CSS. 101 | const reInner = new RegExp(`link href="../../__ver__/${cssFiles[0].name}`, 'mi'); 102 | const reTop = new RegExp(`link href="__ver__/${cssFiles[0].name}`, 'mi'); 103 | assert.match(innerAbout, reInner); 104 | assert.match(innerIndex, reInner); 105 | assert.match(indexHtml, reTop); 106 | 107 | // Validate that Less has been compiled down into CSS. 108 | assert.match(indexCss, /\.nested-style \.more-div/mi); 109 | 110 | return cb(); 111 | }); 112 | }); 113 | }); 114 | 115 | 116 | }); 117 | -------------------------------------------------------------------------------- /test/engine/subpaths.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | path = require('path'), 6 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 7 | temp = require('temp').track(), 8 | Engine = require('../../lib/engine/Engine') 9 | ; 10 | 11 | 12 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 13 | 14 | 15 | describe('Engine: projects that specify sub-paths', function() { 16 | this.slow(5000); 17 | this.timeout(10000); 18 | 19 | 20 | it('should build a project in a subdirectory to the correct output path (prod)', 21 | (cb) => { 22 | const projRoot = path.join(TEST_DATA, 'sub-paths'); 23 | const buildSpec = readBuildSpec(projRoot); 24 | const outputRoot = temp.mkdirSync(); 25 | const engine = new Engine(buildSpec, outputRoot, 'prod', projRoot); 26 | engine.build((err, allStats) => { 27 | if (err) { 28 | console.error(allStats); 29 | return cb(err); 30 | } 31 | 32 | // Ensure compilation was successful. 33 | const stats = allStats[0]; // first page in build specification 34 | assert.isFalse(stats.hasErrors()); 35 | 36 | // Ensure output directory matches expectation. 37 | const outDirContents = fs.readdirSync(outputRoot); 38 | assert.deepEqual(_.sortBy(outDirContents), ['__ver__', 'index.html']); 39 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 40 | assert.strictEqual(versionedFiles.length, 2); 41 | 42 | // Read JS bundle, ensure Typescript was compiled. 43 | const bundlePath = path.join( 44 | outputRoot, '__ver__', _.find(versionedFiles, f => f.match(/\.js$/))); 45 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 46 | assert.match(bundle, /Hello, world/mi, 'TypeScript not compiled.'); 47 | assert.notMatch(bundle, /public greeting:/mi, 'TypeScript not compiled.'); 48 | 49 | // Read HTML bundle, ensure image was correctly referenced. 50 | const htmlPath = path.join(outputRoot, 'index.html'); 51 | const html = fs.readFileSync(htmlPath, 'utf-8'); 52 | assert.match(html, /img src=__ver__\/sample\.[a-f0-9]+\.jpg/mi); 53 | assert.match(html, /pug_sentinel/mi); 54 | 55 | return cb(); 56 | }); 57 | }); 58 | 59 | }); 60 | -------------------------------------------------------------------------------- /test/engine/tree-shaking.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | spawnSync = require('child_process').spawnSync, 8 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 9 | temp = require('temp'), 10 | Engine = require('../../lib/engine/Engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: Webpack tree-shaking', function() { 18 | this.slow(5000); 19 | this.timeout(10000); 20 | 21 | 22 | it('should compile a simple JS file with tree-shaking (prod)', (cb) => { 23 | const projRoot = path.join(TEST_DATA, 'single-js'); 24 | const outputRoot = temp.mkdirSync(); 25 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'prod', projRoot); 26 | engine.build((err, allStats) => { 27 | if (err) { 28 | console.error(allStats); 29 | return cb(err); 30 | } 31 | const stats = allStats[0]; 32 | 33 | // Ensure compilation was successful. 34 | assert.isFalse(stats.hasErrors()); 35 | 36 | // Ensure that removing the unused function lead to a warning. 37 | assert.isTrue(stats.hasWarnings()); // for unused function removal 38 | const warnings = _.get(stats, 'warnings', []); 39 | assert.strictEqual(warnings.length, 1); 40 | 41 | // Ensure output directory matches expectation. 42 | const outDirContents = fs.readdirSync(outputRoot); 43 | assert.deepEqual(_.sortBy(outDirContents), ['__ver__', 'index.html']); 44 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 45 | assert.strictEqual(versionedFiles.length, 1); 46 | 47 | // Read bundle, ensure both functions are present. 48 | const bundlePath = path.join(outputRoot, '__ver__', versionedFiles[0]); 49 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 50 | assert.match(bundle, /sentinel_1/mgi); 51 | assert.notMatch(bundle, /sentinel_2/mgi); 52 | assert.notMatch(bundle, /\/\*\*/mgi); 53 | 54 | // Finally execute bundle using node. 55 | const rv = spawnSync(process.execPath, [bundlePath], { stdio: 'pipe' }); 56 | assert.strictEqual(0, rv.status); 57 | assert.match(rv.stdout.toString('utf-8'), /sentinel_1/mgi); 58 | assert.notMatch(rv.stdout.toString('utf-8'), /sentinel_2/mgi); 59 | 60 | return cb(); 61 | }); 62 | }); 63 | 64 | 65 | it('should compile a simple JS file without tree-shaking (dev)', (cb) => { 66 | const projRoot = path.join(TEST_DATA, 'single-js'); 67 | const outputRoot = temp.mkdirSync(); 68 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'dev', projRoot); 69 | engine.build((err, allStats) => { 70 | if (err) { 71 | console.error(allStats); 72 | return cb(err); 73 | } 74 | const stats = allStats[0]; 75 | 76 | // Ensure compilation was successful. 77 | assert.isNotOk(err); 78 | assert.isFalse(stats.hasErrors()); 79 | assert.isFalse(stats.hasWarnings()); 80 | 81 | // Ensure output directory matches expectation. 82 | const outDirContents = fs.readdirSync(outputRoot); 83 | assert.deepEqual(_.sortBy(outDirContents), ['__ver__', 'index.html']); 84 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 85 | assert.strictEqual(versionedFiles.length, 1); 86 | 87 | // Read bundle, ensure both functions are present. 88 | const bundlePath = path.join(outputRoot, '__ver__', versionedFiles[0]); 89 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 90 | assert.match(bundle, /sentinel_1/mgi); 91 | assert.match(bundle, /sentinel_2/mgi); 92 | 93 | return cb(); 94 | }); 95 | }); 96 | 97 | 98 | it('should remove unused library dependencies using import (prod)', (cb) => { 99 | const projRoot = path.join(TEST_DATA, 'js-with-deps'); 100 | const outputRoot = temp.mkdirSync(); 101 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'prod', projRoot); 102 | engine.build((err, allStats) => { 103 | if (err) { 104 | console.error(allStats); 105 | return cb(err); 106 | } 107 | const stats = allStats[0]; 108 | 109 | // Ensure compilation was successful. 110 | assert.isNotOk(err); 111 | assert.isFalse(stats.hasErrors()); 112 | 113 | // Ensure output directory matches expectation. 114 | const outDirContents = fs.readdirSync(outputRoot); 115 | assert.deepEqual(_.sortBy(outDirContents), ['__ver__', 'index.html']); 116 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 117 | assert.strictEqual(versionedFiles.length, 1); 118 | 119 | // Read bundle, ensure both functions are present. 120 | const bundlePath = path.join(outputRoot, '__ver__', versionedFiles[0]); 121 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 122 | assert.match(bundle, /sentinel_1/mgi); 123 | assert.notMatch(bundle, /sentinel_2/mgi); 124 | assert.notMatch(bundle, /\/\*\*/mgi); 125 | assert.isBelow(bundle.length, 25 * 1024, 'lodash not tree-shaken'); 126 | assert.isAbove(bundle.length, 15 * 1024, 'lodash not tree-shaken'); 127 | 128 | return cb(); 129 | }); 130 | }); 131 | 132 | }); 133 | -------------------------------------------------------------------------------- /test/engine/vue.mocha.js: -------------------------------------------------------------------------------- 1 | /* eslint no-undef: "ignore" */ 2 | const _ = require('lodash'), 3 | assert = require('chai').assert, 4 | fs = require('fs'), 5 | json = JSON.stringify, 6 | path = require('path'), 7 | spawnSync = require('child_process').spawnSync, 8 | readBuildSpec = require('../../lib/engine/readBuildSpec'), 9 | temp = require('temp'), 10 | Engine = require('../../lib/engine') 11 | ; 12 | 13 | 14 | const TEST_DATA = path.resolve(__dirname, '../../test_data'); 15 | 16 | 17 | describe('Engine: Vue single-file components support', function() { 18 | this.slow(5000); 19 | this.timeout(10000); 20 | 21 | 22 | it('should compile a single-file .vue component (prod)', (cb) => { 23 | const projRoot = path.join(TEST_DATA, 'vue-components'); 24 | const outputRoot = temp.mkdirSync(); 25 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'prod', projRoot); 26 | engine.build((err, allStats) => { 27 | const stats = allStats[0]; 28 | 29 | // Ensure compilation was successful. 30 | assert.isNotOk(err); 31 | assert.isFalse(stats.hasErrors()); 32 | 33 | // Ensure output directory matches expectation. 34 | const outputRootContents = fs.readdirSync(outputRoot); 35 | assert.deepEqual(_.sortBy(outputRootContents), ['__ver__', 'index.html']); 36 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 37 | assert.strictEqual(versionedFiles.length, 4); // .map, .bundle.js, .css, and .jpg 38 | 39 | // Read JS bundle, ensure sentinels are present. 40 | const bundleFilename = _.find(versionedFiles, fileInfo => fileInfo.match(/\.js$/)); 41 | const bundlePath = path.join(outputRoot, '__ver__', bundleFilename); 42 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 43 | assert.match(bundle, /vue-script-sentinel/mgi); 44 | assert.match(bundle, /vue-template-sentinel/mgi); 45 | assert.notMatch(bundle, /vue-style-sentinel/mgi); 46 | 47 | // Read CSS bundle, ensure sentinel is present. 48 | const styleFilename = _.find(versionedFiles, fileInfo => fileInfo.match(/\.css$/)); 49 | const stylePath = path.join(outputRoot, '__ver__', styleFilename); 50 | const style = fs.readFileSync(stylePath, 'utf-8'); 51 | assert.match(style, /vue-style-sentinel/mgi); 52 | assert.notMatch(style, /vue-script-sentinel/mgi); 53 | assert.notMatch(style, /vue-template-sentinel/mgi); 54 | 55 | // Finally execute bundle using node. 56 | const rv = spawnSync(process.execPath, [bundlePath], { stdio: 'pipe' }); 57 | assert.strictEqual(0, rv.status); 58 | const stdout = rv.stdout.toString('utf-8'); 59 | assert.match(stdout, /vue-script-sentinel/mgi); 60 | assert.notMatch(stdout, /vue-style-sentinel/mgi); 61 | assert.notMatch(stdout, /vue-template-sentinel/mgi); 62 | 63 | return cb(); 64 | }); 65 | }); 66 | 67 | 68 | it('should compile a single-file .vue component (dev)', (cb) => { 69 | const projRoot = path.join(TEST_DATA, 'vue-components'); 70 | const outputRoot = temp.mkdirSync(); 71 | const engine = new Engine(readBuildSpec(projRoot), outputRoot, 'dev', projRoot); 72 | engine.build((err, allStats) => { 73 | const stats = allStats[0]; 74 | 75 | // Ensure compilation was successful. 76 | assert.isNotOk(err); 77 | assert.isFalse(stats.hasErrors()); 78 | 79 | // Ensure output directory matches expectation. 80 | const outputRootContents = fs.readdirSync(outputRoot); 81 | assert.deepEqual(_.sortBy(outputRootContents), ['__ver__', 'index.html']); 82 | const versionedFiles = fs.readdirSync(path.join(outputRoot, '__ver__')); 83 | assert.strictEqual(versionedFiles.length, 3); // .bundle.js, .css, and .jpg 84 | 85 | // Read JS bundle, ensure sentinels are present. 86 | const bundleFilename = _.find(versionedFiles, fileInfo => fileInfo.match(/\.js$/)); 87 | const bundlePath = path.join(outputRoot, '__ver__', bundleFilename); 88 | const bundle = fs.readFileSync(bundlePath, 'utf-8'); 89 | assert.match(bundle, /vue-script-sentinel/mgi); 90 | assert.match(bundle, /vue-template-sentinel/mgi); 91 | assert.notMatch(bundle, /vue-style-sentinel/mgi); 92 | 93 | // Read CSS bundle, ensure sentinel is present. 94 | const styleFilename = _.find(versionedFiles, fileInfo => fileInfo.match(/\.css$/)); 95 | const stylePath = path.join(outputRoot, '__ver__', styleFilename); 96 | const style = fs.readFileSync(stylePath, 'utf-8'); 97 | assert.match(style, /vue-style-sentinel/mgi); 98 | assert.notMatch(style, /vue-script-sentinel/mgi); 99 | assert.notMatch(style, /vue-template-sentinel/mgi); 100 | 101 | // Finally execute bundle using node. 102 | const rv = spawnSync(process.execPath, [bundlePath], { stdio: 'pipe' }); 103 | assert.strictEqual(0, rv.status); 104 | const stdout = rv.stdout.toString('utf-8'); 105 | assert.match(stdout, /vue-script-sentinel/mgi); 106 | assert.notMatch(stdout, /vue-style-sentinel/mgi); 107 | assert.notMatch(stdout, /vue-template-sentinel/mgi); 108 | 109 | return cb(); 110 | }); 111 | }); 112 | 113 | }); 114 | -------------------------------------------------------------------------------- /test_data/compile-constants/app.js: -------------------------------------------------------------------------------- 1 | if (process.env.CONFIG === 'dev') { 2 | console.log('config:dev'); 3 | } else { 4 | if (process.env.CONFIG === 'prod') { 5 | console.log('config:prod'); 6 | } else { 7 | console.log('config:unknown'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test_data/compile-constants/index.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | body 4 | h1= 'pug_sentinel:' + $martinet.CONFIG 5 | h2= 'public_path:' + $martinet.PUBLIC_PATH + ':' 6 | -------------------------------------------------------------------------------- /test_data/compile-constants/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.pug", 5 | "dist": "index.html", 6 | "scripts": ["app.js"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/es6-js/entry.js: -------------------------------------------------------------------------------- 1 | const value = 123; 2 | 3 | class SomeClass { 4 | constructor(...args) { 5 | this.test = args; 6 | } 7 | 8 | beep() { 9 | setTimeout(() => { 10 | let j = this.test.length; 11 | console.log('sentinel_1', j); 12 | j++; 13 | console.log('sentinel_2', j); 14 | }, 0); 15 | } 16 | } 17 | 18 | function main(...args) { 19 | const i = new SomeClass(args); 20 | i.beep(); 21 | } 22 | 23 | main(value); 24 | -------------------------------------------------------------------------------- /test_data/es6-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test_data/es6-js/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.html", 5 | "dist": "index.html", 6 | "scripts": ["entry.js"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/import-static-json/entry.js: -------------------------------------------------------------------------------- 1 | const staticData = require('./static.json'); 2 | 3 | console.log(JSON.stringify(staticData)); 4 | -------------------------------------------------------------------------------- /test_data/import-static-json/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test_data/import-static-json/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.html", 5 | "scripts": ["entry.js"], 6 | "dist": "index.html" 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/import-static-json/static.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_sentinel": { 3 | "nested": 12345 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test_data/js-with-deps/entry.js: -------------------------------------------------------------------------------- 1 | import { map } from 'lodash/map'; 2 | import { range } from 'lodash/range'; 3 | import { random } from 'lodash/random'; 4 | 5 | console.log('sentinel_1', map(range(0, 10), random)); 6 | -------------------------------------------------------------------------------- /test_data/js-with-deps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test_data/js-with-deps/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.html", 5 | "dist": "index.html", 6 | "scripts": ["entry.js"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/js-with-deps/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-with-deps", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "lodash": "^4.17.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test_data/simple-static/css/more-style.less: -------------------------------------------------------------------------------- 1 | @import './sub-dir/sub-include.less'; 2 | 3 | -------------------------------------------------------------------------------- /test_data/simple-static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: yellow; 3 | } 4 | 5 | @font-face { 6 | font-family: 'jambalaya'; 7 | src: url(../fonts/glyphicons.woff2); 8 | } 9 | 10 | h2 { 11 | color: red; 12 | } 13 | -------------------------------------------------------------------------------- /test_data/simple-static/css/sub-dir/sub-include.less: -------------------------------------------------------------------------------- 1 | .nested-style { 2 | .more-div { 3 | background-color: pink; 4 | background-image: url('../../img/test.jpg'); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test_data/simple-static/data/sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentinel": 456789 3 | } 4 | -------------------------------------------------------------------------------- /test_data/simple-static/fonts/glyphicons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/simple-static/fonts/glyphicons.woff2 -------------------------------------------------------------------------------- /test_data/simple-static/img/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/simple-static/img/test.jpg -------------------------------------------------------------------------------- /test_data/simple-static/index.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | head 4 | title Test Project 5 | body 6 | h1 First Heading 7 | img(src='img/test.jpg') 8 | h2= sentinel 9 | a(href='nested/inner/index.html') Inner link 10 | a(href='https://www.baresoil.com/') External link 11 | -------------------------------------------------------------------------------- /test_data/simple-static/js/app.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | window.onload = () => { 3 | console.log('loaded!'); 4 | throw new Error('a sample error'); 5 | }; 6 | -------------------------------------------------------------------------------- /test_data/simple-static/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.pug", 5 | "styles": ["css/style.css", "css/more-style.less"], 6 | "scripts": ["js/app.js"], 7 | "data": ["data/sample.json"], 8 | "dist": "nested/inner/index.html" 9 | }, 10 | { 11 | "src": "pages/about.pug", 12 | "styles": ["css/style.css", "css/more-style.less"], 13 | "scripts": ["js/app.js"], 14 | "data": ["data/sample.json"], 15 | "dist": "nested/inner/about.html" 16 | }, 17 | { 18 | "src": "index.pug", 19 | "styles": ["css/style.css", "css/more-style.less"], 20 | "scripts": ["js/app.js"], 21 | "data": ["data/sample.json"], 22 | "dist": "index.html" 23 | } 24 | ], 25 | "verbatim": [ 26 | "static", 27 | {"static/favicon.ico": "favicon.ico"} 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test_data/simple-static/pages/about.pug: -------------------------------------------------------------------------------- 1 | include ./partials/mixins.pug 2 | 3 | doctype 4 | html 5 | head 6 | title Test Project 7 | body 8 | h1 About Page 9 | img(src='../img/test.jpg') 10 | h2= sentinel 11 | a(href='../../index.html') Home 12 | span  ·  13 | a(href='https://www.baresoil.com/') External link 14 | +MixinSentinel() 15 | -------------------------------------------------------------------------------- /test_data/simple-static/pages/partials/mixins.pug: -------------------------------------------------------------------------------- 1 | mixin MixinSentinel 2 | div(class='nested-style') 3 | div(class='more-div') mixin-sentinel 4 | 5 | -------------------------------------------------------------------------------- /test_data/simple-static/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/simple-static/static/favicon.ico -------------------------------------------------------------------------------- /test_data/simple-static/static/images/verb-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/simple-static/static/images/verb-1.jpg -------------------------------------------------------------------------------- /test_data/single-js/entry.js: -------------------------------------------------------------------------------- 1 | import { func1 } from './library'; 2 | 3 | console.log(func1('testing')); 4 | -------------------------------------------------------------------------------- /test_data/single-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test_data/single-js/library.js: -------------------------------------------------------------------------------- 1 | function func1(arg) { 2 | console.log('sentinel_1'); 3 | return arg.toLowerCase(); 4 | } 5 | 6 | function func2(arg) { 7 | console.log('sentinel_2'); 8 | return Math.floor(arg); 9 | } 10 | 11 | export { func1, func2 }; 12 | -------------------------------------------------------------------------------- /test_data/single-js/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.html", 5 | "dist": "index.html", 6 | "scripts": ["entry.js"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/sub-paths/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "paths": { 3 | "src": "src" 4 | }, 5 | "pages": [ 6 | { 7 | "src": "templates/index.pug", 8 | "dist": "index.html", 9 | "scripts": ["app/greeter.ts"] 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test_data/sub-paths/src/app/greeter.ts: -------------------------------------------------------------------------------- 1 | class Greeter { 2 | constructor(public greeting: string) { } 3 | greet() { 4 | return "

" + this.greeting + "

"; 5 | } 6 | }; 7 | 8 | const greeter = new Greeter("Hello, world!"); 9 | 10 | console.log(greeter.greet()); 11 | -------------------------------------------------------------------------------- /test_data/sub-paths/src/assets/img/jpg/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/sub-paths/src/assets/img/jpg/sample.jpg -------------------------------------------------------------------------------- /test_data/sub-paths/src/templates/index.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | body 4 | h1 pug_sentinel 5 | img(src='../assets/img/jpg/sample.jpg') 6 | -------------------------------------------------------------------------------- /test_data/template-data-inject/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "data_sentinel": 12345678 3 | } 4 | -------------------------------------------------------------------------------- /test_data/template-data-inject/index.pug: -------------------------------------------------------------------------------- 1 | doctype 2 | html 3 | body 4 | h1= data_sentinel 5 | -------------------------------------------------------------------------------- /test_data/template-data-inject/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.pug", 5 | "data": ["data.json"], 6 | "dist": "index.html" 7 | }, 8 | { 9 | "src": "index.pug", 10 | "dist": "no-data.html" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test_data/vue-components/entry.js: -------------------------------------------------------------------------------- 1 | import VueComp from './vue-component.vue'; 2 | 3 | function main() { 4 | VueComp.talk(); 5 | } 6 | 7 | main(); 8 | -------------------------------------------------------------------------------- /test_data/vue-components/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test_data/vue-components/inner/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceroad/martinet/8dfb1fc1bb975c8863839b99b4c8209e6008d984/test_data/vue-components/inner/test.jpg -------------------------------------------------------------------------------- /test_data/vue-components/martinet.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | { 4 | "src": "index.html", 5 | "dist": "index.html", 6 | "scripts": ["entry.js"] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /test_data/vue-components/vue-component.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 21 | --------------------------------------------------------------------------------