├── .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 |
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 |
--------------------------------------------------------------------------------