├── .eslintrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── compiler.js ├── constexpr.js ├── injections ├── compile_finish_hooks.js ├── index.js └── new_page.js ├── package-lock.json ├── package.json ├── test ├── generator.html └── index.html └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "browser": true, 5 | "node": true 6 | }, 7 | "extends" : "standard", 8 | "rules": {} 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Created by https://www.gitignore.io 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | .env*.local 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | 87 | # Nuxt.js build / generate output 88 | .nuxt 89 | dist 90 | 91 | # Gatsby files 92 | .cache/ 93 | # Comment in the public line in if your project uses Gatsby and not Next.js 94 | # https://nextjs.org/blog/next-9-1#public-directory-support 95 | # public 96 | 97 | # vuepress build output 98 | .vuepress/dist 99 | 100 | # Serverless directories 101 | .serverless/ 102 | 103 | # FuseBox cache 104 | .fusebox/ 105 | 106 | # DynamoDB Local files 107 | .dynamodb/ 108 | 109 | # TernJS port file 110 | .tern-port 111 | 112 | # Stores VSCode versions used for testing VSCode extensions 113 | .vscode-test 114 | 115 | 116 | test/_out 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.4.1 2 | 3 | * Allow users to specify HTML tags whose content shouldn't be formatted 4 | 5 | # 1.4.0 6 | 7 | * Format output HTML 8 | * Use only static analysis for finding resource dependencies 9 | 10 | # 1.3.{0,1} 11 | 12 | * Dependency resolution bugfix 13 | 14 | # 1.2.2 15 | 16 | * depfile not generating bugfix 17 | * some resources not getting detected bugfix 18 | 19 | # 1.2.{0,1} 20 | 21 | * code cleanup 22 | * replace --noheadless with --headless 23 | * linting 24 | 25 | # 1.1.1 26 | 27 | * bugfixes 28 | 29 | # 1.1.0 30 | 31 | * forward console logs and uncaught exceptions to stdout 32 | 33 | # 1.0.0 34 | 35 | * bugfixes 36 | 37 | # 0.8.{1,2,3} 38 | 39 | * bugfixes 40 | 41 | # 0.8.0 42 | 43 | * guess jobscount 44 | 45 | * implementation changes 46 | 47 | * Binary name changed to `constexprjs` (`constexpr.js` is incompatible with windows) 48 | 49 | # 0.7.4 50 | 51 | * perf improvement 52 | 53 | # 0.7.3 54 | 55 | * bugfix 56 | 57 | # 0.7.2 58 | 59 | * removed restriction on input/output directories 60 | 61 | * logging fix 62 | 63 | # 0.7.1 64 | 65 | * Minor fixes 66 | 67 | # 0.7.0 68 | 69 | * Removed automatic HTML discovery 70 | 71 | * Added `addDependency` hook ([Guide](https://amokfa.github.io/posts/constexprjs_dependency_resolution.html)) 72 | 73 | * Replaced `addPaths` with `addPath` 74 | 75 | * Replaced `addExclusions` with `addExclusion` 76 | 77 | * Removed `--exclusion` 78 | 79 | # 0.6.0 80 | 81 | * `--entry` option ([Guide](https://amokfa.github.io/posts/constexprjs_entry_points.html)) 82 | 83 | * `log` hook 84 | 85 | * `--jobs` is changehd to `--jobcount` 86 | 87 | * `--exclusions` is replaced by `--exclusion` (can be used multiple times) 88 | 89 | * Better cli help 90 | 91 | * Better depfiles 92 | 93 | # 0.5.1 94 | 95 | * `addExclusions` 96 | 97 | # 0.5.0 98 | 99 | * Generator pages. ([Guide](https://amokfa.github.io/posts/constexprjs_generator_pages.html)) 100 | 101 | # 0.4.1 102 | 103 | * Colored log output 104 | 105 | * Added `--depfile` option 106 | 107 | # 0.4.0 108 | 109 | * Added `--jobtimeout` option 110 | 111 | * Added `window._ConstexprJS_.abort(message)` callback. 112 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Sagar Tiwari 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 6 | persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ConstexprJS

2 |

3 | 4 | 5 | npm version 6 | 7 |

8 | 9 | Evaluate and strip JS in your website ahead of time. 10 | 11 | ## Demo 12 | 13 | [This](https://amokfa.github.io) site. ([Sources](https://github.com/amokfa/knmw.link.src)) 14 | 15 | ### Installation 16 | 17 | npm i -g constexprjs@latest 18 | 19 | ### Documentation 20 | 21 | * Introduction: https://amokfa.github.io/posts/constexprjs.html 22 | * Hello world: https://amokfa.github.io/posts/constexprjs_hello_world.html 23 | * Guides: https://amokfa.github.io/tags/constexprjs.html -------------------------------------------------------------------------------- /compiler.js: -------------------------------------------------------------------------------- 1 | const urljoin = require('url-join') 2 | const fs = require('fs').promises 3 | const path = require('path') 4 | const hp = require('node-html-parser') 5 | const injections = require('./injections') 6 | const { fileExists, clog, log, warn, error, align, randomColor } = require('./utils') 7 | const _ = require('lodash') 8 | const formatHtml = require('js-beautify').html 9 | // eslint-disable-next-line 10 | const { trace, thread } = require('./utils') 11 | 12 | async function compileFile (browser, httpBase, jobTimeout, generator, output, idx) { 13 | const { targetId } = await browser.send('Target.createTarget', { 14 | url: 'about:blank' 15 | }) 16 | const page = await browser.attachToTarget(targetId) 17 | await page.send('Page.enable') 18 | await page.send('Network.enable') 19 | await page.send('Runtime.enable') 20 | await page.send('Network.clearBrowserCache') 21 | 22 | const killSwitches = [ 23 | thread(async () => { 24 | const resp = await page.until('Runtime.consoleAPICalled') 25 | console[resp.type].apply(null, _.concat(generator, ':', resp.args.map((e) => e.value))) 26 | }), 27 | thread(async () => { 28 | const resp = await page.until('Runtime.exceptionThrown') 29 | resp.exceptionDetails.exception.description.split('\n') 30 | .forEach((l) => console.log(generator, ':', l)) 31 | }) 32 | ] 33 | 34 | await page.send('Page.addScriptToEvaluateOnNewDocument', { 35 | source: injections.newPageScript 36 | }) 37 | 38 | await page.send('Page.navigate', { 39 | url: urljoin(httpBase, generator) 40 | }) 41 | 42 | const { 43 | result: { 44 | value: result 45 | } 46 | } = await page.send('Runtime.evaluate', { 47 | expression: injections.compileFinishHooks.replace('$[jobTimeout]', jobTimeout), 48 | awaitPromise: true, 49 | returnByValue: true 50 | }) 51 | result.idx = idx 52 | result.output = output 53 | result.generator = generator 54 | 55 | if (result.status === 'abort' || result.status === 'timeout') { 56 | return result 57 | } 58 | 59 | result.addedPaths.forEach(p => log(`${generator} added extra path ${p.output} to be generated using ${p.generator}`)) 60 | 61 | const html = (await page.send('DOM.getOuterHTML', { 62 | nodeId: (await page.send('DOM.getDocument')).root.nodeId 63 | })).outerHTML 64 | await browser.send('Target.closeTarget', { targetId }) 65 | await Promise.all(killSwitches.map(s => s())) 66 | 67 | const finalDeps = result.deducedDependencies 68 | .filter(e => e.startsWith(httpBase)) 69 | .map(e => e.replace(httpBase, '')) 70 | finalDeps.push(...result.addedDependencies) 71 | finalDeps.sort() 72 | 73 | return _.assign(result, { 74 | html, 75 | deps: finalDeps 76 | }) 77 | } 78 | 79 | const { range } = require('lodash') 80 | 81 | async function compilePaths (config, browser) { 82 | const entryPoints = config.paths.map(p => ({ generator: p, output: p })) 83 | const COLORS = range(entryPoints.length).map((i) => randomColor(i)) 84 | 85 | const allResults = [] 86 | const results = [] 87 | const linkMapping = {} 88 | const taskQueue = {} 89 | let next = 0 90 | let done = 0 91 | while (true) { 92 | const tasks = Object.values(taskQueue) 93 | if (next === entryPoints.length && tasks.length === 0) { 94 | break 95 | } 96 | if (tasks.length < config.jobCount && next < entryPoints.length) { 97 | const col = COLORS[next] 98 | taskQueue[next] = compileFile(browser, `http://localhost:${config.port}`, config.jobTimeout, entryPoints[next].generator, entryPoints[next].output, next) 99 | next++ 100 | clog(col, align(`Queued file #${next}:`), `${entryPoints[next - 1].output}`) 101 | } else { 102 | let result 103 | try { 104 | result = await Promise.race(tasks) 105 | } catch (e) { 106 | console.log(e) 107 | error('Unrecoverable error, aborting') 108 | process.exit(1) 109 | } 110 | 111 | if (result.status === 'abort') { 112 | warn(align(`Page ${result.generator} signalled an abortion, message:`), `"${result.message}"`) 113 | } else if (result.status === 'timeout') { 114 | error(align('Timeout reached when processing file:'), `${result.generator}`) 115 | } 116 | 117 | allResults.push(result) 118 | done++ 119 | delete taskQueue[result.idx] 120 | if (result.status === 'ok') { 121 | result.addedPaths.forEach( 122 | p => { 123 | entryPoints.push(p) 124 | COLORS.push(randomColor(entryPoints.length)) 125 | if (p.generator !== p.output) { 126 | if (linkMapping[p.generator]) { 127 | warn(`Output paths: "${linkMapping[p.generator]}" and "${p.output}" both use the same generator call: "${p.generator}"`) 128 | } else { 129 | linkMapping[p.generator] = p.output 130 | } 131 | } 132 | } 133 | ) 134 | clog(COLORS[result.idx], align(`(${done}/${entryPoints.length}) Finished:`), `${result.generator}`) 135 | results.push(result) 136 | } 137 | } 138 | } 139 | try { 140 | if (config.depFile) { 141 | await fs.writeFile(config.depFile, JSON.stringify( 142 | { 143 | commandLine: process.argv, 144 | allResults: allResults.map(res => _.omit(res, 'html')) 145 | }, 146 | null, 147 | 4 148 | )) 149 | warn(align('Wrote depfile:'), config.depFile) 150 | } 151 | } catch (e) { 152 | error(align('Encountered error when writing depfile:'), e.message) 153 | } 154 | return { 155 | results, 156 | linkMapping 157 | } 158 | } 159 | 160 | function mapLinks (html, linkMapping) { 161 | const root = hp.parse(html) 162 | root.querySelectorAll('a') 163 | .filter(a => linkMapping[a.getAttribute('href')]) 164 | .forEach(a => a.setAttribute('href', linkMapping[a.getAttribute('href')])) 165 | return formatHtml( 166 | root.toString(), 167 | { 168 | unformatted: ['style', 'prog'], 169 | preserve_newlines: false 170 | } 171 | ) 172 | return root.toString() 173 | } 174 | 175 | async function compile (config, browser) { 176 | const { results, linkMapping } = await compilePaths(config, browser) 177 | const allDepsSet = new Set() 178 | results.forEach(res => { 179 | res.deps.forEach(d => allDepsSet.add(d)) 180 | delete res.deps 181 | }) 182 | const allDeps = [...allDepsSet] 183 | const allFilesToCopy = allDeps.map(dep => path.join(config.input, dep)) 184 | 185 | const htmls = {} 186 | for (let i = 0; i < results.length; i++) { 187 | htmls[path.join(config.input, results[i].output)] = mapLinks(results[i].html, linkMapping) 188 | } 189 | 190 | for (const p of Object.keys(htmls)) { 191 | const out = p.replace(config.input, config.output) 192 | const dir = path.dirname(out) 193 | await fs.mkdir(dir, { recursive: true }) 194 | await fs.writeFile(out, htmls[p]) 195 | } 196 | if (config.copyResources) { 197 | for (const inp of allFilesToCopy) { 198 | log(align('Copying resource:'), `${inp}`) 199 | const out = inp.replace(config.input, config.output) 200 | if (await fileExists(out)) { 201 | await fs.rm(out, { recursive: true }) 202 | } 203 | const dir = path.dirname(out) 204 | await fs.mkdir(dir, { recursive: true }) 205 | try { 206 | await fs.cp(inp, out, { recursive: true }) 207 | } catch (e) { 208 | console.log(e) 209 | warn(align('Couldn\'t copy file:'), `${inp}`) 210 | } 211 | } 212 | } 213 | } 214 | 215 | module.exports = { 216 | compile, 217 | compileFile 218 | } 219 | -------------------------------------------------------------------------------- /constexpr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawnChrome } = require('chrome-debugging-client') 4 | 5 | const { ArgumentParser } = require('argparse') 6 | const { version } = require('./package.json') 7 | 8 | const fs = require('fs') 9 | const os = require('os') 10 | const path = require('path') 11 | // eslint-disable-next-line 12 | const { trace } = require('./utils') 13 | const { compile } = require('./compiler') 14 | const { error } = require('./utils') 15 | const { enableVerbose } = require('./utils') 16 | const express = require('express') 17 | 18 | async function main () { 19 | const parser = createArgParser() 20 | const argv = parser.parse_args() 21 | 22 | if (argv.verbose) { 23 | enableVerbose() 24 | } 25 | 26 | const config = { 27 | depFile: argv.depfile, 28 | jobCount: argv.jobcount, 29 | jobTimeout: argv.jobtimeout * 1000, 30 | copyResources: !argv.skipResources, 31 | paths: argv.entryPoints, 32 | literalTags: argv.literalTags, 33 | input: path.resolve(argv.input), 34 | output: path.resolve(argv.output) 35 | } 36 | 37 | if (!fs.existsSync(config.output)) { 38 | fs.mkdirSync(config.output) 39 | } 40 | if (!fs.lstatSync(config.output).isDirectory()) { 41 | parser.print_help() 42 | process.exit(1) 43 | } 44 | if (config.input === config.output) { 45 | error('"input" and "output" must be different directories') 46 | process.exit(1) 47 | } 48 | if (argv.entryPoints.length === 0) { 49 | error('Must provide at least one entry point') 50 | process.exit(1) 51 | } 52 | 53 | const app = express() 54 | app.use(express.static(config.input)) 55 | const server = app.listen(0) 56 | config.port = server.address().port 57 | 58 | try { 59 | const chrome = spawnChrome({ 60 | headless: argv.headless 61 | }) 62 | const browser = chrome.connection 63 | 64 | await compile(config, browser) 65 | 66 | await chrome.dispose() 67 | } catch (e) { 68 | console.log(e) 69 | } 70 | await server.close() 71 | } 72 | 73 | function createArgParser () { 74 | const parser = new ArgumentParser({ 75 | description: 'Evaluate and strip JS in your website ahead of time' 76 | }) 77 | 78 | parser.add_argument('-v', '--version', { action: 'version', version }) 79 | parser.add_argument('--input', { 80 | required: true, 81 | metavar: 'INPUT_DIRECTORY', 82 | help: 'Input website root directory' 83 | }) 84 | parser.add_argument('--output', { 85 | required: true, 86 | metavar: 'OUTPUT_DIRECTORY', 87 | help: 'Output directory' 88 | }) 89 | parser.add_argument('--entry', { 90 | action: 'append', 91 | dest: 'entryPoints', 92 | help: 'Add an HTML file to be used as entry point, paths must be relative to the website root, can be used multiple times, must provide at least one entry point', 93 | default: [] 94 | }) 95 | parser.add_argument('--skip-resources', { 96 | action: 'store_true', 97 | dest: 'skipResources', 98 | help: 'Do not copy resources to the output directory' 99 | }) 100 | parser.add_argument('--jobcount', { 101 | help: 'Number of compilation jobs to run in parallel', 102 | type: 'int', 103 | default: Math.floor(os.cpus().length * 1.5) 104 | }) 105 | parser.add_argument('--jobtimeout', { 106 | help: 'Time in milliseconds for which the compiler will wait for the pages to render', 107 | type: 'int', 108 | default: 10 109 | }) 110 | parser.add_argument('--literal-tag', { 111 | help: 'HTML tags whose content shouldn\'t be formatted. By default, style tag contents aren\'t formatted', 112 | action: 'append', 113 | dest: 'literalTags', 114 | default: ['style'] 115 | }) 116 | parser.add_argument('--depfile', { 117 | help: 'A JSON object containing the command line arguments, file dependency, compilation results will be written to this path' 118 | }) 119 | parser.add_argument('--headless', { 120 | action: 'store_true', 121 | help: 'Run chrome in headless mode, can be used for running in environments without display server' 122 | }) 123 | parser.add_argument('--verbose', { 124 | action: 'store_true', 125 | help: 'Enable verbose logging' 126 | }) 127 | 128 | return parser 129 | } 130 | 131 | main() 132 | .then(() => process.exit(0)) 133 | -------------------------------------------------------------------------------- /injections/compile_finish_hooks.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | new Promise((resolve) => { 3 | setTimeout(() => resolve({ status: 'timeout' }), $[jobTimeout]) 4 | window._ConstexprJS_.triggerCompilationHook = () => resolve({ 5 | status: 'ok', 6 | addedPaths: window._ConstexprJS_.addedPaths, 7 | addedExclusions: window._ConstexprJS_.addedExclusions, 8 | addedDependencies: window._ConstexprJS_.addedDependencies, 9 | deducedDependencies: window._ConstexprJS_.deducedDependencies, 10 | }) 11 | window._ConstexprJS_.compilationErrorHook = (message) => resolve({ status: 'abort', message }) 12 | }) 13 | /* eslint-enable */ 14 | -------------------------------------------------------------------------------- /injections/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | newPageScript: fs.readFileSync(path.resolve(__dirname) + '/new_page.js').toString(), 6 | compileFinishHooks: fs.readFileSync(path.resolve(__dirname) + '/compile_finish_hooks.js').toString() 7 | } 8 | -------------------------------------------------------------------------------- /injections/new_page.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | window._ConstexprJS_ = {} 3 | window._ConstexprJS_.addedPaths = [] 4 | window._ConstexprJS_.addedExclusions = [] 5 | window._ConstexprJS_.addedDependencies = [] 6 | window._ConstexprJS_.deducedDependencies = [] 7 | window._ConstexprJS_.triggerCompilationHook = null 8 | window._ConstexprJS_.compilationErrorHook = null 9 | 10 | function callWhenAvailable (fnr, ...args) { 11 | function f () { 12 | const fn = fnr() 13 | if (fn) { 14 | fn(...args) 15 | } else { 16 | setTimeout(f, 100) 17 | } 18 | } 19 | f() 20 | } 21 | 22 | window._ConstexprJS_.compile = () => { 23 | document.querySelectorAll('[constexpr]').forEach( 24 | el => el.remove() 25 | ) 26 | 27 | Array.from(document.querySelectorAll('[src]')) 28 | .forEach(el => window._ConstexprJS_.deducedDependencies.push(el.src)) 29 | Array.from(document.querySelectorAll('link[rel=stylesheet]')) 30 | .forEach(el => window._ConstexprJS_.deducedDependencies.push(el.href)) 31 | Array.from(document.querySelectorAll('link[rel=icon]')) 32 | .forEach(el => window._ConstexprJS_.deducedDependencies.push(el.href)) 33 | 34 | callWhenAvailable(() => window._ConstexprJS_.triggerCompilationHook) 35 | } 36 | window._ConstexprJS_.abort = (message) => { 37 | callWhenAvailable(() => window._ConstexprJS_.compilationErrorHook, message) 38 | } 39 | window._ConstexprJS_.addPath = (path) => { 40 | if (typeof (path) !== 'object' || typeof (path.generator) !== 'string' || typeof (path.output) !== 'string') { 41 | throw new Error('"path" must be objects with keys "generator" and "output" having strings as values') 42 | } 43 | window._ConstexprJS_.addedPaths.push({ generator: path.generator, output: path.output }) 44 | } 45 | window._ConstexprJS_.addExclusion = (path) => { 46 | if (typeof (path) !== 'string') { 47 | throw new Error('"path" must be a string') 48 | } 49 | window._ConstexprJS_.addedExclusions.push(path) 50 | } 51 | window._ConstexprJS_.addDependency = (path) => { 52 | if (typeof (path) !== 'string') { 53 | throw new Error('"path" must be a string') 54 | } 55 | window._ConstexprJS_.addedDependencies.push(path) 56 | } 57 | })() 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "constexprjs", 3 | "version": "1.4.1", 4 | "description": "Evaluate and strip JS in your website", 5 | "homepage": "https://amokfa.github.io/posts/constexprjs", 6 | "repository": "https://github.com/amokfa/ConstexprJS", 7 | "main": "compiler.js", 8 | "bin": { 9 | "constexprjs": "./constexpr.js" 10 | }, 11 | "scripts": { 12 | "lint": "eslint '**/*.js'", 13 | "fix": "eslint '**/*.js' --fix" 14 | }, 15 | "keywords": [ 16 | "static site generator", 17 | "jekyll", 18 | "compiler", 19 | "next.js", 20 | "nuxt.js", 21 | "nest.js" 22 | ], 23 | "files": [ 24 | "constexpr.js", 25 | "compiler.js", 26 | "injections", 27 | "utils.js", 28 | "README.md" 29 | ], 30 | "author": "Sagar Tiwari (https://amokfa.github.io)", 31 | "license": "ISC", 32 | "dependencies": { 33 | "argparse": "^2.0.1", 34 | "chalk": "^4.1.0", 35 | "chrome-debugging-client": "^2.0.0", 36 | "express": "^4.17.1", 37 | "js-beautify": "^1.14.11", 38 | "lodash": "^4.17.21", 39 | "node-html-parser": "^3.1.0", 40 | "random-seed": "^0.3.0", 41 | "url-join": "^4.0.1" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^8.51.0", 45 | "eslint-config-airbnb": "^19.0.4", 46 | "eslint-config-standard": "^17.1.0", 47 | "eslint-plugin-import": "^2.28.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/generator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const fsp = require('fs').promises 3 | const path = require('path') 4 | 5 | async function htmlFiles (fsBase, dir, isExcluded) { 6 | const files = (await fsp.readdir(dir)).filter(e => !e.startsWith('.')).map(file => path.join(dir, file)) 7 | const stats = await Promise.all(files.map(file => fsp.stat(file))) 8 | const htmls = [] 9 | for (let i = 0; i < files.length; i++) { 10 | if (stats[i].isFile() && files[i].toLowerCase().endsWith('.html')) { 11 | const path = files[i].replace(fsBase, '') 12 | if (isExcluded(path)) { 13 | warn(align('Ignoring path:'), `${path}`) 14 | } else { 15 | log(align('Found path:'), `${path}`) 16 | htmls.push(path) 17 | } 18 | } else if (stats[i].isDirectory()) { 19 | htmls.push(...(await htmlFiles(fsBase, files[i], isExcluded))) 20 | } 21 | } 22 | return htmls 23 | } 24 | 25 | async function sleep (n) { 26 | return new Promise((resolve) => { 27 | setTimeout(() => resolve(), n) 28 | }) 29 | } 30 | 31 | function fileExists (f) { 32 | return new Promise((resolve) => { 33 | fs.access(f, (err) => { 34 | if (err) { 35 | resolve(false) 36 | } else { 37 | resolve(true) 38 | } 39 | }) 40 | }) 41 | } 42 | 43 | const isChildOf = (child, parent) => { 44 | if (child === parent) return false 45 | const parentTokens = parent.split('/').filter(i => i.length) 46 | const childTokens = child.split('/').filter(i => i.length) 47 | return parentTokens.every((t, i) => childTokens[i] === t) 48 | } 49 | 50 | let verbose = false 51 | function enableVerbose () { 52 | verbose = true 53 | } 54 | 55 | const chalk = require('chalk') 56 | function logLine (c, ...args) { 57 | console.log(c(...args)) 58 | } 59 | 60 | function clog (color, ...args) { 61 | if (verbose) { 62 | if (color) { 63 | logLine(chalk.hex(color), ...args) 64 | } else { 65 | logLine(chalk, ...args) 66 | } 67 | } 68 | } 69 | function log (...args) { 70 | clog(false, ...args) 71 | } 72 | function warn (...args) { 73 | clog('#ffff00', ...args) 74 | } 75 | function error (...args) { 76 | logLine(chalk.hex('#ff0000').underline, ...args) 77 | } 78 | 79 | function align (s) { 80 | if (s.length >= 60) { 81 | return s + '\n' + '-'.repeat(60) 82 | } else { 83 | return s + '-'.repeat(60 - s.length) 84 | } 85 | } 86 | 87 | const fac = 0.7 88 | function tooClose (r1, r2, r3) { 89 | const sorted = [r1, r2, r3].sort((a, b) => a - b) 90 | // console.log(sorted) 91 | return sorted[0] / sorted[1] > fac || sorted[1] / sorted[2] > fac 92 | } 93 | 94 | function shuffle (rand, arr) { 95 | const result = [] 96 | while (arr.length !== 0) { 97 | const idx = rand(arr.length) 98 | result.push(arr[idx]) 99 | arr.splice(idx, 1) 100 | } 101 | return result 102 | } 103 | 104 | const random = require('random-seed') 105 | function randomColor (s) { 106 | const rand = random.create(s) 107 | let r 108 | let g 109 | let b 110 | do { 111 | const r1 = rand(200) + 56 112 | const r2 = rand(200) + 56 113 | const r3 = rand(200) + 56 114 | const cols = shuffle(rand, [r1, r2, r3]) 115 | r = cols[0] 116 | g = cols[1] 117 | b = cols[2] 118 | } while (tooClose(r, g, b)) 119 | return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0') 120 | } 121 | 122 | if (require.main === module) { 123 | verbose = true 124 | for (let i = 0; i < 100; i++) { 125 | const color = randomColor(i) 126 | // let color = rc({ 127 | // luminosity: 'bright' 128 | // }); 129 | clog(color, color) 130 | } 131 | } 132 | 133 | function thread (afn) { 134 | let ended = false 135 | const promise = (async () => { 136 | // eslint-disable-next-line 137 | while (!ended) { 138 | try { 139 | await afn() 140 | } catch (e) { 141 | break 142 | } 143 | } 144 | })() 145 | return async () => { 146 | ended = true 147 | await promise 148 | } 149 | } 150 | 151 | function trace (value) { 152 | console.log(value) 153 | return value 154 | } 155 | 156 | module.exports = { 157 | htmlFiles, 158 | sleep, 159 | fileExists, 160 | logLine, 161 | clog, 162 | log, 163 | warn, 164 | error, 165 | align, 166 | randomColor, 167 | enableVerbose, 168 | isChildOf, 169 | thread, 170 | trace 171 | } 172 | --------------------------------------------------------------------------------