1 | #!/usr/bin/env node
|
2 |
|
3 | 'use strict';
|
4 |
|
5 | const commandLineArgs = require('command-line-args');
|
6 | const carmi = require('../index');
|
7 | const path = require('path');
|
8 | const fs = require('fs-extra');
|
9 | const {isUpToDate, getDependenciesHashes, analyzeDependencies} = require('../src/analyze-dependencies');
|
10 | const getCacheFilePath = require('../src/get-cache-file-path');
|
11 | const wrapModule = require('../src/wrap-module');
|
12 | const base64ArrayBuffer = require('../bytecode/base64-arraybuffer');
|
13 |
|
14 | const CACHE_SCENARIOS = {
|
15 | mtime: 'mtime',
|
16 | gitHash: 'git-hash'
|
17 | };
|
18 |
|
19 | const optionDefinitions = [
|
20 | {name: 'source', type: String, defaultOption: true, description: 'source filename, which exports a carmi model'},
|
21 | {name: 'output', type: String, defaultValue: '', description: 'output filename'},
|
22 | {name: 'cwd', type: String, defaultValue: process.cwd(), description: 'root directory for carmi files. Needed for babel config'},
|
23 | {
|
24 | name: 'compiler',
|
25 | type: String,
|
26 | defaultValue: 'optimizing',
|
27 | description: 'compiler version to use naive/simple/optimizing/bytecode'
|
28 | },
|
29 | {name: 'debug', type: Boolean, defaultValue: false, description: 'add debug functions to the generated code'},
|
30 | {name: 'type-check', type: Boolean, defaultValue: false, description: 'add static type checking to runtime code'},
|
31 | {name: 'format', type: String, defaultValue: 'iife', description: 'output format - iife,cjs... or binary'},
|
32 | {name: 'name', type: String, defaultValue: 'model', description: 'name of the output module/function'},
|
33 | {name: 'prettier', type: Boolean, defaultValue: false, description: 'run prettier on the output'},
|
34 | {name: 'no-cache', type: Boolean, defaultValue: false, description: 'ignore cache'},
|
35 | {name: 'cache-scenario', type: String, defaultValue: CACHE_SCENARIOS.mtime, description: `cache scenario to use (${Object.values(CACHE_SCENARIOS).join(' | ')})`},
|
36 | {name: 'no-coverage', type: Boolean, defaultValue: false, description: 'generate header to disable coverage'},
|
37 | {name: 'stats', type: String, defaultValue: '', description: 'generate stats file'},
|
38 | {name: 'help', type: Boolean, defaultValue: false, description: 'shows this very help message and quits'},
|
39 | {name: 'ast', type: Boolean, defaultValue: false}
|
40 | ];
|
41 |
|
42 | const HELP = `carmi [<options>] <src>
|
43 | ${optionDefinitions
|
44 | .filter(v => v.description)
|
45 | .map(opt => `--${opt.name} - ${opt.description}`)
|
46 | .join('\n')}
|
47 | `;
|
48 |
|
49 | const options = commandLineArgs(optionDefinitions);
|
50 |
|
51 | async function run() {
|
52 | if (options.help) {
|
53 | return console.log(HELP);
|
54 | }
|
55 |
|
56 | const absPath = path.resolve(process.cwd(), options.source);
|
57 |
|
58 | require('@babel/register')({
|
59 | rootMode: 'upward',
|
60 | extensions: ['.ts', '.js'],
|
61 | ignore: [/node_modules/],
|
62 | envName: 'carmi'
|
63 | })
|
64 |
|
65 | const statsFilePath = path.resolve(options.stats);
|
66 | const dependencies = analyzeDependencies(absPath, statsFilePath);
|
67 | const encoding = options.compiler === 'bytecode' ? null : 'utf-8';
|
68 |
|
69 | const dependenciesHashes = options['cache-scenario'] === CACHE_SCENARIOS.gitHash ?
|
70 | getDependenciesHashes(dependencies) :
|
71 | null;
|
72 |
|
73 | const cacheFilePath = getCacheFilePath({
|
74 | path: absPath,
|
75 | debug: options.debug,
|
76 | format: options.format,
|
77 | prettier: options.prettier,
|
78 | dependenciesHashes,
|
79 | name: options.name
|
80 | });
|
81 |
|
82 | if (options.stats) {
|
83 | await fs.outputJSON(statsFilePath, dependencies);
|
84 | }
|
85 |
|
86 | let code;
|
87 |
|
88 | // We are using fallback to mtime check if scenario is `git-hash`, but getting hashes was resulted in error.
|
89 | const upToDate = Boolean(dependenciesHashes) || isUpToDate(dependencies, cacheFilePath);
|
90 | const useCache = !options['no-cache'] && fs.existsSync(cacheFilePath) && upToDate;
|
91 | if (useCache) {
|
92 | // return from cache
|
93 | code = await fs.readFile(cacheFilePath, encoding);
|
94 | } else {
|
95 | // run carmi and generate cache
|
96 | let model;
|
97 |
|
98 | try {
|
99 | model = require(absPath);
|
100 | } catch (e) {
|
101 | console.error(`failed to require ${options.source} ${e.stack}`);
|
102 | throw e;
|
103 | }
|
104 |
|
105 | code = carmi.compile(model, options);
|
106 | }
|
107 |
|
108 | if (options['no-coverage'] && typeof code === 'string') {
|
109 | code = `/* istanbul ignore file */
|
110 | ${code}`
|
111 | }
|
112 |
|
113 | if (!options['no-cache']) {
|
114 | await fs.outputFile(cacheFilePath, code, encoding)
|
115 | }
|
116 | if (typeof code !== 'string' && options.format !== 'binary') {
|
117 | code = wrapModule(options.format, `require('carmi/bytecode/carmi-instance')('${base64ArrayBuffer.encode(code)}')`, options.name);
|
118 | }
|
119 | if (options['no-coverage'] && typeof code === 'string') {
|
120 | code = `/* istanbul ignore file */
|
121 | ${code}`
|
122 | }
|
123 | if (options.output) {
|
124 | await fs.outputFile(options.output, code);
|
125 | } else {
|
126 | console.log(code);
|
127 | }
|
128 | }
|
129 |
|
130 | run().catch(e => {
|
131 | console.log(`error ${e}`)
|
132 | process.exit(1)
|
133 | });
|