├── .gitignore ├── .npmignore ├── .npmrc ├── LICENSE ├── README.md ├── cjs ├── bundler.js ├── graph.js ├── handlers.js ├── index.js ├── package.json ├── rollup.js └── utils.js ├── esm ├── bundler.js ├── graph.js ├── handlers.js ├── index.js ├── rollup.js └── utils.js ├── examples └── builtin-elements │ ├── README.md │ ├── ff.html │ ├── index.html │ ├── js-in-json.js │ ├── js │ └── main.js │ ├── package.json │ └── server.js ├── logo ├── JSinJSON.24.png ├── JSinJSON.png └── JSinJSON.svg ├── package.json └── test ├── index.js ├── package.json ├── project ├── exports.js ├── imports.js ├── module.js ├── package.json └── test.js └── test.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | js-in.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | examples/ 7 | node_modules/ 8 | rollup/ 9 | test/ 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2021, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JS in JSN 2 | 3 | A server side agnostic way to stream JavaScript, ideal for: 4 | 5 | * inline hydration 6 | * network free ad-hoc dependencies 7 | * bootstrap on demand 8 | * libraries on demand 9 | * one JSON file to rule all SSR cases 10 | 11 | The **Session** utility is currently available for: 12 | 13 | * [JS](https://github.com/WebReflection/js-in-json-session#readme) 14 | * [PHP](https://github.com/WebReflection/js-in-json-session/blob/main/php/session.php) 15 | * [Python](https://github.com/WebReflection/js-in-json-session/blob/main/python/session.py) 16 | 17 | - - - 18 | 19 | An "*islands friendly*" approach to Server Side Rendering able to produce *stream-able JS* on demand, via any programming language, through a single JSON bundle file instrumented to *flush()* any script, after optional transpilation and/or minification. 20 | 21 | The produced output can be also pre-generated and served as static content, with the advantages that **js-in-json bundles require zero network activity**: forget round-trips, thousand *ESM* requests per library or project, and simply provide all it's needed right on the page. 22 | 23 | ```js 24 | // a basic serving example 25 | const {JSinJSON} = require('js-in-json'); 26 | 27 | // see ## Options 28 | const {options} = require('./js-in-json-options.js'); 29 | 30 | const islands = JSinJSON(options); 31 | // islands.save(); when needed to create the JSON bundle 32 | 33 | http.createServer((req, res) => { 34 | // see ## Session 35 | const js = islands.session(); 36 | js.add('Main'); 37 | res.writeHead(200, {'content-type': 'text/html'}); 38 | res.write(` 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | `.trim()); 47 | js.add('Footer'); 48 | if (global.condition) { 49 | js.add('SpecialCondition'); 50 | res.write(``); 51 | } 52 | res.end(); 53 | }); 54 | ``` 55 | 56 | ## Session 57 | 58 | A *js-in-json* session can be initialized right away via `js-in-json/session` exported *Session* class, or through the main `JSinJSON(options).session()` utility. 59 | 60 | A session created via `JSinJSON` optionally accepts a JSON bundle, retrieved from *options*, if not provided, and it exposes 2 methods: 61 | 62 | * `add("ModuleName")` to *flush* its content *only once* and, if repeatedly added, and available, bootstrap its *code* 63 | * `flush()` to return all modules and their dependencies previously added and, if available, their code to bootstrap 64 | 65 | In order to have a *session*, a JSON bundle must be created. 66 | 67 | 68 | ## Options 69 | 70 | Following the object literal with all its defaults that can be passed to the `JSinJSON(options)` export. 71 | 72 | ```js 73 | const {save, session} = JSinJSON({ 74 | 75 | // TOP LEVEL CONFIG ONLY 76 | 77 | // MANDATORY 78 | // the root folder from which each `input` is retrieved 79 | // used to resolve the optional output, if relative to this folder 80 | root: '/full/project/root/folder' 81 | 82 | // OPTIONAL 83 | // where to store the resulting JSON cache usable via JSinJSON.session(cache) 84 | // if omitted, the cache is still processed and returned 85 | output: './bundle.json', 86 | // the global context used to attach the `require` like function 87 | global: 'self', 88 | // the `require` like unique function name, automatically generated, 89 | // and it's different per each saved JSON (hint: don't specify it) 90 | prefix: '_uid', 91 | 92 | 93 | // OPTIONAL EXTRAS: CAN BE OVERWRITTEN PER EACH MODULE 94 | // use Babel transformer to target @babel/preset-env 95 | babel: true, 96 | // use terser to minify produced code 97 | minify: true, 98 | // transform specific bare imports into other imports, it's {} by default 99 | // see: rollup-plugin-includepaths 100 | replace: { 101 | // example: replace CE polyfill with an empty file 102 | '@ungap/custom-elements': './empty.js' 103 | }, 104 | // executed each time a JSinJSON.session.flush() happens 105 | // no matter which module has been added to the stack 106 | // it's missing/no-op by default and it has no access 107 | // to the outer scope of this file (it's serialized as function) 108 | code(require) { 109 | // each code receives the `require` like function 110 | const {upgradeAll} = require('Bootstrap'); 111 | upgradeAll(); 112 | }, 113 | // an object literal to define all modules flushed in the page 114 | // whenever any of these is needed 115 | modules: { 116 | // the module name available via the `require` like function 117 | Bootstrap: { 118 | // MANDATORY 119 | // the ESM entry point for this module 120 | input: './bootstrap.js', 121 | 122 | // OPTIONAL: overwrite top level options per each module 123 | // don't transform and/or don't minify 124 | babel: false, 125 | minify: false, 126 | // will be merged with the top level 127 | replace: {'other': './file.js'}, 128 | // don't flush anything when injected 129 | code: null 130 | }, 131 | 132 | // other module example 133 | Login: { 134 | input: './login.js', 135 | code() { 136 | document.documentElement.classList.add('wait'); 137 | fetch('/login/challenge').then(b => b.json).then(result => { 138 | self.challenge = result; 139 | document.documentElement.classList.remove('wait'); 140 | }); 141 | } 142 | } 143 | } 144 | }); 145 | ``` 146 | 147 | ### Options Rules / Limitations 148 | 149 | * the `root` should better be a fully qualified path, instead of relative 150 | * the `code` is always transformed with `@babel/preset-env` target 151 | * the `code` **cannot be asynchronous** 152 | * modules *cannot* have `_` as name prefix, that's reserved for internal resolutions 153 | * modules *should* have *non-npm* modules names, to avoid conflicts/clashing with imports 154 | * modules *can* be capitalized 155 | -------------------------------------------------------------------------------- /cjs/bundler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {writeFile, unlink} = require('fs/promises'); 21 | const {env} = require('process'); 22 | 23 | const {transformSync} = require("@babel/core"); 24 | const {minify: terser} = require('terser'); 25 | 26 | const {iife} = require('./rollup.js'); 27 | 28 | const { 29 | babelOptions, 30 | getBody, 31 | getCallback, 32 | getGlobal, 33 | getModule, 34 | getName, 35 | getNames, 36 | getRealName, 37 | hasOwnProperty, 38 | stringify, 39 | slice, 40 | warn 41 | } = require('./utils.js'); 42 | 43 | const {exportHandler, importHandler} = require('./handlers.js'); 44 | const etag = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('etag')); 45 | 46 | const addCacheEntry = async (CommonJS, name, module, parsed, remove) => { 47 | const {cache, code} = module; 48 | if (hasOwnProperty.call(cache, name)) 49 | return; 50 | const dependencies = new Set; 51 | const body = await moduleTransformer( 52 | CommonJS, 53 | name, 54 | module, 55 | dependencies, 56 | parsed, 57 | remove 58 | ); 59 | cache[name] = { 60 | module: body, 61 | etag: etag(body), 62 | code: code ? ( 63 | typeof code === 'string' ? 64 | `!${code}(${module.require});` : 65 | (await codeTransformer(module)) 66 | ) : '', 67 | dependencies: [...dependencies] 68 | }; 69 | }; 70 | 71 | const chunk = (info, esm, cjs) => ({ 72 | start: info.start, 73 | end: info.end, 74 | esm, cjs 75 | }); 76 | 77 | const codeTransformer = async ({babel, code, minify, require}) => { 78 | let output = `(${getCallback(code)})(${require});`; 79 | if (babel) 80 | output = transformSync( 81 | output, babelOptions).code.replace(/^"use strict";/, '' 82 | ); 83 | if (minify) 84 | output = (await terser(output)).code; 85 | return output; 86 | }; 87 | 88 | const getOutput = (code, chunks) => { 89 | const output = []; 90 | const {length} = chunks; 91 | let c = 0; 92 | for (let i = 0; i < length; i++) { 93 | output.push( 94 | code.slice(c, chunks[i].start), 95 | chunks[i].cjs 96 | ); 97 | c = chunks[i].end; 98 | } 99 | output.push(length ? code.slice(c) : code); 100 | return output.join('').trim(); 101 | }; 102 | 103 | const moduleTransformer = async ( 104 | CommonJS, name, module, dependencies, parsed, remove 105 | ) => { 106 | let {graph, input, replace, require} = module; 107 | if (parsed.has(input)) 108 | return; 109 | parsed.add(input); 110 | const {code} = graph.get(input); 111 | const chunks = []; 112 | for (const item of getBody(code)) { 113 | const {source} = item; 114 | const esm = slice(code, item); 115 | switch (item.type) { 116 | case 'ExportAllDeclaration': { 117 | await exportHandler( 118 | CommonJS, input, dependencies, module, parsed, remove, 119 | getRealName(code, source, replace), 120 | { 121 | ...exportUtils, 122 | // TODO: find a way to resolve modules via their entry point 123 | // only if these modules are ESM ... otherwise think about 124 | // warning here and but the bundler include the whole library? 125 | async ifModule(name) { 126 | warn( 127 | 'export * from', 128 | `\x1b[1m${name[0] === '_' ? input : name}\x1b[0m`, 129 | 'is being exported instead as default' 130 | ); 131 | const rep = `export default ${getGlobal(require, name)};`; 132 | chunks.push(chunk(item, esm, rep)); 133 | }, 134 | ifLocal(name) { 135 | const rep = esm.replace(slice(code, source), stringify(name)); 136 | chunks.push(chunk(item, esm, rep)); 137 | } 138 | } 139 | ); 140 | break; 141 | } 142 | case 'ExportNamedDeclaration': { 143 | if (source) { 144 | const {specifiers} = item; 145 | await exportHandler( 146 | CommonJS, input, dependencies, module, parsed, remove, 147 | getRealName(code, source, replace), 148 | { 149 | ...exportUtils, 150 | ifModule(name) { 151 | const {imports, exports} = getNames(specifiers); 152 | const rep = imports.join(', '); 153 | chunks.push(chunk(item, esm, [ 154 | `const {${rep}} = ${getModule(require, name)};`, 155 | `export {${exports.join(', ')}};` 156 | ].join('\n'))); 157 | }, 158 | ifLocal(name) { 159 | const {imports, exports} = getNames(specifiers); 160 | const rep = imports.map(n => n.replace(':', ' as')).join(', '); 161 | chunks.push(chunk(item, esm, [ 162 | `import {${rep}} from ${stringify(name)};`, 163 | `export {${exports.join(', ')}};` 164 | ].join('\n'))); 165 | } 166 | } 167 | ); 168 | } 169 | break; 170 | } 171 | case 'ImportDeclaration': { 172 | await importHandler( 173 | CommonJS, input, dependencies, module, parsed, remove, 174 | getRealName(code, source, replace), 175 | { 176 | ...exportUtils, 177 | specifiers: item.specifiers, 178 | ifModule(names) { 179 | chunks.push(chunk(item, esm, names)); 180 | }, 181 | ifLocal(name) { 182 | const rep = esm.replace(slice(code, source), stringify(name)); 183 | chunks.push(chunk(item, esm, rep)); 184 | } 185 | } 186 | ); 187 | break; 188 | } 189 | } 190 | } 191 | if (chunks.length) { 192 | input = getName(input); 193 | await saveFile(input, code, chunks, remove); 194 | } 195 | return name ? (await iife(input, name, false, module)) : ''; 196 | }; 197 | 198 | const saveFile = (file, code, chunks, remove) => { 199 | if (remove.has(file)) { 200 | warn(`possible circular dependency for ${file}`); 201 | return; 202 | } 203 | remove.add(file); 204 | return writeFile(file, getOutput(code, chunks)); 205 | }; 206 | 207 | const exportUtils = {addCacheEntry, moduleTransformer}; 208 | 209 | const parse = async (CommonJS, graph, modules) => { 210 | const parsed = new Set; 211 | const remove = new Set; 212 | for (const [input, {name, count, key}] of graph.entries()) { 213 | if (count > 1 || name === key) { 214 | const module = modules[name] || modules[key]; 215 | await addCacheEntry( 216 | CommonJS, name, {...module, input}, parsed, remove 217 | ); 218 | } 219 | } 220 | if (!/^(?:1|true|y|yes)$/i.test(env.JS_IN_JSON_DEBUG)) 221 | await Promise.all([...remove].map(file => unlink(file))); 222 | }; 223 | exports.parse = parse; 224 | -------------------------------------------------------------------------------- /cjs/graph.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {readFile} = require('fs/promises'); 21 | const {dirname, resolve} = require('path'); 22 | 23 | const { 24 | getBody, getInclude, hasOwnProperty, isJS, isLocal, slice 25 | } = require('./utils.js'); 26 | 27 | const {keys} = Object; 28 | 29 | const parse = async (file, entry, map, include, index, key) => { 30 | if (!map.has(file)) 31 | map.set(file, { 32 | name: '_' + (index.i++).toString(36), 33 | code: (await readFile(file)).toString(), 34 | count: 0, 35 | key 36 | }); 37 | 38 | const info = map.get(file); 39 | 40 | if (entry) 41 | info.name = entry; 42 | 43 | if (info.count++) 44 | return; 45 | 46 | // TODO: the AST for crawled files could be stored too 47 | for (const {type, source} of getBody(info.code)) { 48 | switch (type) { 49 | case 'ExportNamedDeclaration': 50 | if (!source) 51 | break; 52 | case 'ExportAllDeclaration': 53 | case 'ImportDeclaration': 54 | let name = slice(info.code, source).slice(1, -1); 55 | if (hasOwnProperty.call(include, name)) 56 | name = include[name]; 57 | if (isJS(name) && isLocal(name)) 58 | await parse( 59 | resolve(dirname(file), name), '', map, include, index, key 60 | ); 61 | break; 62 | } 63 | } 64 | }; 65 | 66 | const crawl = async (options) => { 67 | const map = new Map; 68 | const index = {i: 0}; 69 | const {modules, root} = options; 70 | for (const entry of keys(modules)) { 71 | const {input, replace} = modules[entry]; 72 | const include = getInclude(root, replace || options.replace || {}); 73 | await parse(resolve(root, input), entry, map, include, index, entry); 74 | } 75 | return new Map([...map.entries()].sort( 76 | ([a, {count: A}], [z, {count: Z}]) => (Z - A) 77 | )); 78 | }; 79 | exports.crawl = crawl; 80 | -------------------------------------------------------------------------------- /cjs/handlers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const etag = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('etag')); 21 | const {dirname, resolve} = require('path'); 22 | const {iife} = require('./rollup.js'); 23 | const { 24 | getGlobal, getModule, getName, hasOwnProperty, isJS, isLocal 25 | } = require('./utils.js'); 26 | 27 | const createCacheEntry = async ( 28 | CommonJS, module, dependencies, innerName 29 | ) => { 30 | const {cache} = module; 31 | if (!dependencies.has(innerName)) 32 | dependencies.add(innerName); 33 | if (!hasOwnProperty.call(cache, innerName)) { 34 | const body = await iife( 35 | CommonJS.resolve(innerName), 36 | innerName, 37 | true, 38 | module 39 | ); 40 | cache[innerName] = { 41 | module: body, 42 | etag: etag(body), 43 | code: '', 44 | dependencies: [] 45 | }; 46 | } 47 | }; 48 | 49 | const createLocalEntry = async ( 50 | CommonJS, dependencies, module, parsed, remove, 51 | input, 52 | moduleTransformer, 53 | ifLocal 54 | ) => { 55 | await moduleTransformer( 56 | CommonJS, '', 57 | {...module, input}, 58 | dependencies, parsed, remove 59 | ); 60 | const newName = getName(input); 61 | await ifLocal(remove.has(newName) ? newName : input); 62 | }; 63 | 64 | const exportHandler = async ( 65 | CommonJS, input, dependencies, module, parsed, remove, innerName, 66 | { 67 | addCacheEntry, 68 | moduleTransformer, 69 | ifModule, 70 | ifLocal 71 | } 72 | ) => { 73 | const {graph} = module; 74 | if (isLocal(innerName)) { 75 | if (!isJS(innerName)) 76 | return; 77 | innerName = resolve(dirname(input), innerName); 78 | const {count, name} = graph.get(innerName); 79 | if (count > 1) { 80 | innerName = name; 81 | if (!dependencies.has(name)) 82 | dependencies.add(name); 83 | await addCacheEntry(CommonJS, innerName, module, parsed, remove); 84 | await ifModule(innerName); 85 | } 86 | else { 87 | await createLocalEntry( 88 | CommonJS, dependencies, module, parsed, remove, 89 | innerName, 90 | moduleTransformer, 91 | ifLocal 92 | ); 93 | } 94 | } 95 | else { 96 | await createCacheEntry(CommonJS, module, dependencies, innerName); 97 | await ifModule(innerName); 98 | } 99 | }; 100 | exports.exportHandler = exportHandler; 101 | 102 | const importHandler = async ( 103 | CommonJS, input, dependencies, module, parsed, remove, innerName, 104 | { 105 | addCacheEntry, 106 | moduleTransformer, 107 | ifModule, 108 | ifLocal, 109 | specifiers 110 | } 111 | ) => { 112 | const {graph, require} = module; 113 | if (isLocal(innerName)) { 114 | if (!isJS(innerName)) 115 | return; 116 | innerName = resolve(dirname(input), innerName); 117 | const {count, name} = graph.get(innerName); 118 | if (count > 1) { 119 | innerName = name; 120 | if (!dependencies.has(name)) 121 | dependencies.add(name); 122 | await addCacheEntry(CommonJS, innerName, module, parsed, remove); 123 | } 124 | else { 125 | await createLocalEntry( 126 | CommonJS, dependencies, module, parsed, remove, 127 | innerName, 128 | moduleTransformer, 129 | ifLocal 130 | ); 131 | return; 132 | } 133 | } 134 | else { 135 | await createCacheEntry(CommonJS, module, dependencies, innerName); 136 | } 137 | const imports = []; 138 | const names = []; 139 | for (const {type, imported, local} of specifiers) { 140 | switch(type) { 141 | case 'ImportDefaultSpecifier': 142 | imports.push( 143 | `const ${local.name} = ${getModule(require, innerName)};` 144 | ); 145 | break; 146 | case 'ImportNamespaceSpecifier': 147 | imports.push( 148 | `const ${local.name} = ${getGlobal(require, innerName)};` 149 | ); 150 | break; 151 | case 'ImportSpecifier': 152 | names.push( 153 | local.name === imported.name ? 154 | local.name : 155 | `${imported.name}: ${local.name}` 156 | ); 157 | break; 158 | } 159 | } 160 | 161 | if (names.length) { 162 | const rep = getGlobal(require, innerName); 163 | imports.push(`const {${names.join(', ')}} = ${rep};`); 164 | } 165 | 166 | ifModule(imports.join('\n')); 167 | }; 168 | exports.importHandler = importHandler; 169 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {createRequire} = require('module'); 21 | const {join, resolve} = require('path'); 22 | const {writeFile} = require('fs/promises'); 23 | 24 | const Session = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('js-in-json-session')); 25 | 26 | const {parse} = require('./bundler.js'); 27 | const {crawl} = require('./graph.js'); 28 | const {getGlobal, getInclude, isSimple, keys, stringify} = require('./utils.js'); 29 | 30 | const defaults = { 31 | babel: true, 32 | minify: true, 33 | global: 'self', 34 | prefix: `_${Date.now().toString(36).slice(-2)}`, 35 | code: null, 36 | replace: null 37 | }; 38 | 39 | const createModuleEntry = (module, options) => ({ 40 | input: resolve(options.root, module.input), 41 | babel: !!getEntryValue('babel', module, options), 42 | minify: !!getEntryValue('minify', module, options), 43 | code: getEntryValue('code', module, options), 44 | replace: getEntryValue('replace', module, options), 45 | }); 46 | 47 | const getEntryValue = (name, module, options) => { 48 | let value = module[name]; 49 | if (value == null) { 50 | value = options[name]; 51 | if (value == null) 52 | value = defaults[name]; 53 | } 54 | return value; 55 | }; 56 | 57 | let instances = 0; 58 | const JSinJSON = options => { 59 | const {output, root} = options; 60 | const id = (instances++).toString(36); 61 | const CommonJS = createRequire(join(root, 'node_modules')); 62 | let json = null; 63 | return { 64 | session(cache = json) { 65 | return new Session(cache || (json = CommonJS(resolve(root, output)))); 66 | }, 67 | async save() { 68 | const graph = await crawl(options); 69 | const main = `${getEntryValue('prefix', {}, options)}${id}`; 70 | const global = getEntryValue('global', {}, options); 71 | const namespace = getGlobal(global, main); 72 | const require = isSimple(main) ? main : namespace; 73 | const cache = { 74 | _: { 75 | module: `${namespace}=function _($){return _[$]};`, 76 | code: '', 77 | dependencies: [] 78 | } 79 | }; 80 | const modules = {}; 81 | for (const key of keys(options.modules)) { 82 | const entry = createModuleEntry(options.modules[key], options); 83 | if (entry.replace) 84 | entry.replace = getInclude(root, entry.replace); 85 | modules[key] = { 86 | ...entry, 87 | global, 88 | namespace, 89 | require, 90 | cache, 91 | graph 92 | }; 93 | } 94 | await parse(CommonJS, graph, modules); 95 | if (output) 96 | await writeFile( 97 | resolve(root, output), 98 | stringify(cache, null, 2) 99 | ); 100 | return cache; 101 | } 102 | }; 103 | }; 104 | exports.JSinJSON = JSinJSON; 105 | 106 | exports.Session = Session; 107 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/rollup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {rollup} = require('rollup'); 21 | const includePaths = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('rollup-plugin-includepaths')); 22 | const terser = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@rollup/plugin-terser')); 23 | const {babel} = require('@rollup/plugin-babel'); 24 | const {nodeResolve} = require('@rollup/plugin-node-resolve'); 25 | const commonjs = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('@rollup/plugin-commonjs')); 26 | 27 | const {babelOptions, getGlobal} = require('./utils.js'); 28 | 29 | const iife = async (input, name, esModule, { 30 | babel: transpile, 31 | minify, 32 | global, 33 | namespace, 34 | replace, 35 | require 36 | }) => { 37 | const bundle = await rollup({ 38 | input, 39 | plugins: [].concat( 40 | replace ? [includePaths({include: replace})] : [], 41 | [nodeResolve()], 42 | [commonjs()], 43 | transpile ? [babel({...babelOptions, babelHelpers: 'runtime'})] : [], 44 | minify ? [terser()] : [] 45 | ) 46 | }); 47 | const ignore = namespace === require ? global : require; 48 | const {output} = await bundle.generate({ 49 | esModule: false, 50 | name: '__', 51 | format: 'iife', 52 | exports: 'named', 53 | globals: esModule ? {} : {[ignore]: ignore} 54 | }); 55 | const module = output.map(({code}) => code).join('\n').trim(); 56 | return module.replace( 57 | /^(?:var|const|let)\s+__\s*=/, 58 | `${getGlobal(require, name)}=` 59 | ); 60 | }; 61 | exports.iife = iife; 62 | -------------------------------------------------------------------------------- /cjs/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*! 3 | * ISC License 4 | * 5 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 6 | * 7 | * Permission to use, copy, modify, and/or distribute this software for any 8 | * purpose with or without fee is hereby granted, provided that the above 9 | * copyright notice and this permission notice appear in all copies. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 14 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 15 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 16 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 17 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 18 | */ 19 | 20 | const {extname, resolve} = require('path'); 21 | 22 | const parser = require('@babel/parser'); 23 | 24 | const {keys} = Object; 25 | const {stringify} = JSON; 26 | const {hasOwnProperty} = {}; 27 | 28 | exports.hasOwnProperty = hasOwnProperty; 29 | exports.keys = keys; 30 | exports.stringify = stringify; 31 | 32 | // TODO: find out in which case this is actually needed as rollup is forced to export names 33 | const asModule = name => `(m=>m.__esModule&&m.default||m)(${name})`; 34 | exports.asModule = asModule; 35 | 36 | const getBody = code => parser.parse(code, parserOptions).program.body; 37 | exports.getBody = getBody; 38 | 39 | const getCallback = callback => ''.replace.call( 40 | callback, 41 | /^\s*([^(]+)/, 42 | (_, $1) => { 43 | return /^(?:function|)$/.test($1.trim()) ? $1 : 'function'; 44 | } 45 | ); 46 | exports.getCallback = getCallback; 47 | 48 | const getGlobal = (namespace, name) => 49 | `${namespace}${isSimple(name) ? `.${name}` : `[${stringify(name)}]`}`; 50 | exports.getGlobal = getGlobal; 51 | 52 | const getInclude = (root, hash) => { 53 | const include = {}; 54 | for (const key of keys(hash)) 55 | include[key] = resolve(root, hash[key]); 56 | return include; 57 | }; 58 | exports.getInclude = getInclude; 59 | 60 | const getModule = (require, name) => name[0] === '_' ? 61 | getGlobal(require, name) : 62 | asModule(getGlobal(require, name)); 63 | exports.getModule = getModule; 64 | 65 | const getName = name => { 66 | const ext = extname(name); 67 | return `${name.slice(0, -ext.length)}@JSinJSON${ext}`; 68 | }; 69 | exports.getName = getName; 70 | 71 | const getNames = specifiers => { 72 | const imports = []; 73 | const exports = []; 74 | for (const {local, exported} of specifiers) { 75 | const {name} = exported; 76 | imports.push(local.name == name ? name : `${local.name}: ${name}`); 77 | exports.push(name); 78 | } 79 | return {imports, exports}; 80 | }; 81 | exports.getNames = getNames; 82 | 83 | const getRealName = (code, source, replace) => { 84 | let name = slice(code, source).slice(1, -1); 85 | if (replace && hasOwnProperty.call(replace, name)) 86 | name = replace[name]; 87 | return name; 88 | }; 89 | exports.getRealName = getRealName; 90 | 91 | const isJS = name => /^\.[mc]?js$/i.test(extname(name)); 92 | exports.isJS = isJS; 93 | const isLocal = name => /^[./]/.test(name); 94 | exports.isLocal = isLocal; 95 | const isSimple = name => /^[$_a-z]+[$_0-9a-z]*$/i.test(name); 96 | exports.isSimple = isSimple; 97 | 98 | const slice = (code, info) => code.slice(info.start, info.end); 99 | exports.slice = slice; 100 | 101 | const warn = (...args) => { 102 | console.warn('⚠ \x1b[1mWarning\x1b[0m', ...args); 103 | }; 104 | exports.warn = warn; 105 | 106 | const defaults = { 107 | babel: true, 108 | minify: true, 109 | global: 'self', 110 | prefix: `_${Date.now().toString(36).slice(-2)}` 111 | }; 112 | exports.defaults = defaults; 113 | 114 | const babelOptions = { 115 | presets: ['@babel/preset-env'], 116 | plugins: [['@babel/plugin-transform-runtime', {useESModules: true}]] 117 | }; 118 | exports.babelOptions = babelOptions; 119 | 120 | const parserOptions = { 121 | allowAwaitOutsideFunction: true, 122 | sourceType: 'module', 123 | plugins: [ 124 | // 'estree', 125 | 'jsx', 126 | 'typescript', 127 | 'exportExtensions', 128 | 'exportDefaultFrom', 129 | 'exportNamespaceFrom', 130 | 'dynamicImport', 131 | 'importMeta', 132 | 'asyncGenerators', 133 | 'bigInt', 134 | 'classProperties', 135 | 'classPrivateProperties', 136 | 'classPrivateMethods', 137 | ['decorators', {decoratorsBeforeExport: true}], 138 | 'doExpressions', 139 | 'functionBind', 140 | 'functionSent', 141 | 'logicalAssignment', 142 | 'nullishCoalescingOperator', 143 | 'numericSeparator', 144 | 'objectRestSpread', 145 | 'optionalCatchBinding', 146 | 'optionalChaining', 147 | 'partialApplication', 148 | ['pipelineOperator', {proposal: 'minimal'}], 149 | 'throwExpressions', 150 | 'topLevelAwait' 151 | ] 152 | }; 153 | exports.parserOptions = parserOptions; 154 | -------------------------------------------------------------------------------- /esm/bundler.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {writeFile, unlink} from 'fs/promises'; 20 | import {env} from 'process'; 21 | 22 | import {transformSync} from "@babel/core"; 23 | import {minify as terser} from 'terser'; 24 | 25 | import {iife} from './rollup.js'; 26 | 27 | import { 28 | babelOptions, 29 | getBody, getCallback, getGlobal, getModule, getName, getNames, getRealName, 30 | hasOwnProperty, stringify, 31 | slice, warn 32 | } from './utils.js'; 33 | 34 | import {exportHandler, importHandler} from './handlers.js'; 35 | import etag from 'etag'; 36 | 37 | const addCacheEntry = async (CommonJS, name, module, parsed, remove) => { 38 | const {cache, code} = module; 39 | if (hasOwnProperty.call(cache, name)) 40 | return; 41 | const dependencies = new Set; 42 | const body = await moduleTransformer( 43 | CommonJS, 44 | name, 45 | module, 46 | dependencies, 47 | parsed, 48 | remove 49 | ); 50 | cache[name] = { 51 | module: body, 52 | etag: etag(body), 53 | code: code ? ( 54 | typeof code === 'string' ? 55 | `!${code}(${module.require});` : 56 | (await codeTransformer(module)) 57 | ) : '', 58 | dependencies: [...dependencies] 59 | }; 60 | }; 61 | 62 | const chunk = (info, esm, cjs) => ({ 63 | start: info.start, 64 | end: info.end, 65 | esm, cjs 66 | }); 67 | 68 | const codeTransformer = async ({babel, code, minify, require}) => { 69 | let output = `(${getCallback(code)})(${require});`; 70 | if (babel) 71 | output = transformSync( 72 | output, babelOptions).code.replace(/^"use strict";/, '' 73 | ); 74 | if (minify) 75 | output = (await terser(output)).code; 76 | return output; 77 | }; 78 | 79 | const getOutput = (code, chunks) => { 80 | const output = []; 81 | const {length} = chunks; 82 | let c = 0; 83 | for (let i = 0; i < length; i++) { 84 | output.push( 85 | code.slice(c, chunks[i].start), 86 | chunks[i].cjs 87 | ); 88 | c = chunks[i].end; 89 | } 90 | output.push(length ? code.slice(c) : code); 91 | return output.join('').trim(); 92 | }; 93 | 94 | const moduleTransformer = async ( 95 | CommonJS, name, module, dependencies, parsed, remove 96 | ) => { 97 | let {graph, input, replace, require} = module; 98 | if (parsed.has(input)) 99 | return; 100 | parsed.add(input); 101 | const {code} = graph.get(input); 102 | const chunks = []; 103 | for (const item of getBody(code)) { 104 | const {source} = item; 105 | const esm = slice(code, item); 106 | switch (item.type) { 107 | case 'ExportAllDeclaration': { 108 | await exportHandler( 109 | CommonJS, input, dependencies, module, parsed, remove, 110 | getRealName(code, source, replace), 111 | { 112 | ...exportUtils, 113 | // TODO: find a way to resolve modules via their entry point 114 | // only if these modules are ESM ... otherwise think about 115 | // warning here and but the bundler include the whole library? 116 | async ifModule(name) { 117 | warn( 118 | 'export * from', 119 | `\x1b[1m${name[0] === '_' ? input : name}\x1b[0m`, 120 | 'is being exported instead as default' 121 | ); 122 | const rep = `export default ${getGlobal(require, name)};`; 123 | chunks.push(chunk(item, esm, rep)); 124 | }, 125 | ifLocal(name) { 126 | const rep = esm.replace(slice(code, source), stringify(name)); 127 | chunks.push(chunk(item, esm, rep)); 128 | } 129 | } 130 | ); 131 | break; 132 | } 133 | case 'ExportNamedDeclaration': { 134 | if (source) { 135 | const {specifiers} = item; 136 | await exportHandler( 137 | CommonJS, input, dependencies, module, parsed, remove, 138 | getRealName(code, source, replace), 139 | { 140 | ...exportUtils, 141 | ifModule(name) { 142 | const {imports, exports} = getNames(specifiers); 143 | const rep = imports.join(', '); 144 | chunks.push(chunk(item, esm, [ 145 | `const {${rep}} = ${getModule(require, name)};`, 146 | `export {${exports.join(', ')}};` 147 | ].join('\n'))); 148 | }, 149 | ifLocal(name) { 150 | const {imports, exports} = getNames(specifiers); 151 | const rep = imports.map(n => n.replace(':', ' as')).join(', '); 152 | chunks.push(chunk(item, esm, [ 153 | `import {${rep}} from ${stringify(name)};`, 154 | `export {${exports.join(', ')}};` 155 | ].join('\n'))); 156 | } 157 | } 158 | ); 159 | } 160 | break; 161 | } 162 | case 'ImportDeclaration': { 163 | await importHandler( 164 | CommonJS, input, dependencies, module, parsed, remove, 165 | getRealName(code, source, replace), 166 | { 167 | ...exportUtils, 168 | specifiers: item.specifiers, 169 | ifModule(names) { 170 | chunks.push(chunk(item, esm, names)); 171 | }, 172 | ifLocal(name) { 173 | const rep = esm.replace(slice(code, source), stringify(name)); 174 | chunks.push(chunk(item, esm, rep)); 175 | } 176 | } 177 | ); 178 | break; 179 | } 180 | } 181 | } 182 | if (chunks.length) { 183 | input = getName(input); 184 | await saveFile(input, code, chunks, remove); 185 | } 186 | return name ? (await iife(input, name, false, module)) : ''; 187 | }; 188 | 189 | const saveFile = (file, code, chunks, remove) => { 190 | if (remove.has(file)) { 191 | warn(`possible circular dependency for ${file}`); 192 | return; 193 | } 194 | remove.add(file); 195 | return writeFile(file, getOutput(code, chunks)); 196 | }; 197 | 198 | const exportUtils = {addCacheEntry, moduleTransformer}; 199 | 200 | export const parse = async (CommonJS, graph, modules) => { 201 | const parsed = new Set; 202 | const remove = new Set; 203 | for (const [input, {name, count, key}] of graph.entries()) { 204 | if (count > 1 || name === key) { 205 | const module = modules[name] || modules[key]; 206 | await addCacheEntry( 207 | CommonJS, name, {...module, input}, parsed, remove 208 | ); 209 | } 210 | } 211 | if (!/^(?:1|true|y|yes)$/i.test(env.JS_IN_JSON_DEBUG)) 212 | await Promise.all([...remove].map(file => unlink(file))); 213 | }; 214 | -------------------------------------------------------------------------------- /esm/graph.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {readFile} from 'fs/promises'; 20 | import {dirname, resolve} from 'path'; 21 | 22 | import { 23 | getBody, getInclude, 24 | hasOwnProperty, isJS, isLocal, slice 25 | } from './utils.js'; 26 | 27 | const {keys} = Object; 28 | 29 | const parse = async (file, entry, map, include, index, key) => { 30 | if (!map.has(file)) 31 | map.set(file, { 32 | name: '_' + (index.i++).toString(36), 33 | code: (await readFile(file)).toString(), 34 | count: 0, 35 | key 36 | }); 37 | 38 | const info = map.get(file); 39 | 40 | if (entry) 41 | info.name = entry; 42 | 43 | if (info.count++) 44 | return; 45 | 46 | // TODO: the AST for crawled files could be stored too 47 | for (const {type, source} of getBody(info.code)) { 48 | switch (type) { 49 | case 'ExportNamedDeclaration': 50 | if (!source) 51 | break; 52 | case 'ExportAllDeclaration': 53 | case 'ImportDeclaration': 54 | let name = slice(info.code, source).slice(1, -1); 55 | if (hasOwnProperty.call(include, name)) 56 | name = include[name]; 57 | if (isJS(name) && isLocal(name)) 58 | await parse( 59 | resolve(dirname(file), name), '', map, include, index, key 60 | ); 61 | break; 62 | } 63 | } 64 | }; 65 | 66 | export const crawl = async (options) => { 67 | const map = new Map; 68 | const index = {i: 0}; 69 | const {modules, root} = options; 70 | for (const entry of keys(modules)) { 71 | const {input, replace} = modules[entry]; 72 | const include = getInclude(root, replace || options.replace || {}); 73 | await parse(resolve(root, input), entry, map, include, index, entry); 74 | } 75 | return new Map([...map.entries()].sort( 76 | ([a, {count: A}], [z, {count: Z}]) => (Z - A) 77 | )); 78 | }; 79 | -------------------------------------------------------------------------------- /esm/handlers.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import etag from 'etag'; 20 | import {dirname, resolve} from 'path'; 21 | import {iife} from './rollup.js'; 22 | import { 23 | getGlobal, getModule, getName, 24 | hasOwnProperty, isJS, isLocal 25 | } from './utils.js'; 26 | 27 | const createCacheEntry = async ( 28 | CommonJS, module, dependencies, innerName 29 | ) => { 30 | const {cache} = module; 31 | if (!dependencies.has(innerName)) 32 | dependencies.add(innerName); 33 | if (!hasOwnProperty.call(cache, innerName)) { 34 | const body = await iife( 35 | CommonJS.resolve(innerName), 36 | innerName, 37 | true, 38 | module 39 | ); 40 | cache[innerName] = { 41 | module: body, 42 | etag: etag(body), 43 | code: '', 44 | dependencies: [] 45 | }; 46 | } 47 | }; 48 | 49 | const createLocalEntry = async ( 50 | CommonJS, dependencies, module, parsed, remove, 51 | input, 52 | moduleTransformer, 53 | ifLocal 54 | ) => { 55 | await moduleTransformer( 56 | CommonJS, '', 57 | {...module, input}, 58 | dependencies, parsed, remove 59 | ); 60 | const newName = getName(input); 61 | await ifLocal(remove.has(newName) ? newName : input); 62 | }; 63 | 64 | export const exportHandler = async ( 65 | CommonJS, input, dependencies, module, parsed, remove, innerName, 66 | { 67 | addCacheEntry, 68 | moduleTransformer, 69 | ifModule, 70 | ifLocal 71 | } 72 | ) => { 73 | const {graph} = module; 74 | if (isLocal(innerName)) { 75 | if (!isJS(innerName)) 76 | return; 77 | innerName = resolve(dirname(input), innerName); 78 | const {count, name} = graph.get(innerName); 79 | if (count > 1) { 80 | innerName = name; 81 | if (!dependencies.has(name)) 82 | dependencies.add(name); 83 | await addCacheEntry(CommonJS, innerName, module, parsed, remove); 84 | await ifModule(innerName); 85 | } 86 | else { 87 | await createLocalEntry( 88 | CommonJS, dependencies, module, parsed, remove, 89 | innerName, 90 | moduleTransformer, 91 | ifLocal 92 | ); 93 | } 94 | } 95 | else { 96 | await createCacheEntry(CommonJS, module, dependencies, innerName); 97 | await ifModule(innerName); 98 | } 99 | }; 100 | 101 | export const importHandler = async ( 102 | CommonJS, input, dependencies, module, parsed, remove, innerName, 103 | { 104 | addCacheEntry, 105 | moduleTransformer, 106 | ifModule, 107 | ifLocal, 108 | specifiers 109 | } 110 | ) => { 111 | const {graph, require} = module; 112 | if (isLocal(innerName)) { 113 | if (!isJS(innerName)) 114 | return; 115 | innerName = resolve(dirname(input), innerName); 116 | const {count, name} = graph.get(innerName); 117 | if (count > 1) { 118 | innerName = name; 119 | if (!dependencies.has(name)) 120 | dependencies.add(name); 121 | await addCacheEntry(CommonJS, innerName, module, parsed, remove); 122 | } 123 | else { 124 | await createLocalEntry( 125 | CommonJS, dependencies, module, parsed, remove, 126 | innerName, 127 | moduleTransformer, 128 | ifLocal 129 | ); 130 | return; 131 | } 132 | } 133 | else { 134 | await createCacheEntry(CommonJS, module, dependencies, innerName); 135 | } 136 | const imports = []; 137 | const names = []; 138 | for (const {type, imported, local} of specifiers) { 139 | switch(type) { 140 | case 'ImportDefaultSpecifier': 141 | imports.push( 142 | `const ${local.name} = ${getModule(require, innerName)};` 143 | ); 144 | break; 145 | case 'ImportNamespaceSpecifier': 146 | imports.push( 147 | `const ${local.name} = ${getGlobal(require, innerName)};` 148 | ); 149 | break; 150 | case 'ImportSpecifier': 151 | names.push( 152 | local.name === imported.name ? 153 | local.name : 154 | `${imported.name}: ${local.name}` 155 | ); 156 | break; 157 | } 158 | } 159 | 160 | if (names.length) { 161 | const rep = getGlobal(require, innerName); 162 | imports.push(`const {${names.join(', ')}} = ${rep};`); 163 | } 164 | 165 | ifModule(imports.join('\n')); 166 | }; 167 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {createRequire} from 'module'; 20 | import {join, resolve} from 'path'; 21 | import {writeFile} from 'fs/promises'; 22 | 23 | import Session from 'js-in-json-session'; 24 | 25 | import {parse} from './bundler.js'; 26 | import {crawl} from './graph.js'; 27 | import {getGlobal, getInclude, isSimple, keys, stringify} from './utils.js'; 28 | 29 | const defaults = { 30 | babel: true, 31 | minify: true, 32 | global: 'self', 33 | prefix: `_${Date.now().toString(36).slice(-2)}`, 34 | code: null, 35 | replace: null 36 | }; 37 | 38 | const createModuleEntry = (module, options) => ({ 39 | input: resolve(options.root, module.input), 40 | babel: !!getEntryValue('babel', module, options), 41 | minify: !!getEntryValue('minify', module, options), 42 | code: getEntryValue('code', module, options), 43 | replace: getEntryValue('replace', module, options), 44 | }); 45 | 46 | const getEntryValue = (name, module, options) => { 47 | let value = module[name]; 48 | if (value == null) { 49 | value = options[name]; 50 | if (value == null) 51 | value = defaults[name]; 52 | } 53 | return value; 54 | }; 55 | 56 | let instances = 0; 57 | export const JSinJSON = options => { 58 | const {output, root} = options; 59 | const id = (instances++).toString(36); 60 | const CommonJS = createRequire(join(root, 'node_modules')); 61 | let json = null; 62 | return { 63 | session(cache = json) { 64 | return new Session(cache || (json = CommonJS(resolve(root, output)))); 65 | }, 66 | async save() { 67 | const graph = await crawl(options); 68 | const main = `${getEntryValue('prefix', {}, options)}${id}`; 69 | const global = getEntryValue('global', {}, options); 70 | const namespace = getGlobal(global, main); 71 | const require = isSimple(main) ? main : namespace; 72 | const cache = { 73 | _: { 74 | module: `${namespace}=function _($){return _[$]};`, 75 | code: '', 76 | dependencies: [] 77 | } 78 | }; 79 | const modules = {}; 80 | for (const key of keys(options.modules)) { 81 | const entry = createModuleEntry(options.modules[key], options); 82 | if (entry.replace) 83 | entry.replace = getInclude(root, entry.replace); 84 | modules[key] = { 85 | ...entry, 86 | global, 87 | namespace, 88 | require, 89 | cache, 90 | graph 91 | }; 92 | } 93 | await parse(CommonJS, graph, modules); 94 | if (output) 95 | await writeFile( 96 | resolve(root, output), 97 | stringify(cache, null, 2) 98 | ); 99 | return cache; 100 | } 101 | }; 102 | }; 103 | 104 | export {Session}; 105 | -------------------------------------------------------------------------------- /esm/rollup.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {rollup} from 'rollup'; 20 | import includePaths from 'rollup-plugin-includepaths'; 21 | import terser from '@rollup/plugin-terser'; 22 | import {babel} from '@rollup/plugin-babel'; 23 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 24 | import commonjs from '@rollup/plugin-commonjs'; 25 | 26 | import {babelOptions, getGlobal} from './utils.js'; 27 | 28 | export const iife = async (input, name, esModule, { 29 | babel: transpile, 30 | minify, 31 | global, 32 | namespace, 33 | replace, 34 | require 35 | }) => { 36 | const bundle = await rollup({ 37 | input, 38 | plugins: [].concat( 39 | replace ? [includePaths({include: replace})] : [], 40 | [nodeResolve()], 41 | [commonjs()], 42 | transpile ? [babel({...babelOptions, babelHelpers: 'runtime'})] : [], 43 | minify ? [terser()] : [] 44 | ) 45 | }); 46 | const ignore = namespace === require ? global : require; 47 | const {output} = await bundle.generate({ 48 | esModule: false, 49 | name: '__', 50 | format: 'iife', 51 | exports: 'named', 52 | globals: esModule ? {} : {[ignore]: ignore} 53 | }); 54 | const module = output.map(({code}) => code).join('\n').trim(); 55 | return module.replace( 56 | /^(?:var|const|let)\s+__\s*=/, 57 | `${getGlobal(require, name)}=` 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /esm/utils.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ISC License 3 | * 4 | * Copyright (c) 2021, Andrea Giammarchi, @WebReflection 5 | * 6 | * Permission to use, copy, modify, and/or distribute this software for any 7 | * purpose with or without fee is hereby granted, provided that the above 8 | * copyright notice and this permission notice appear in all copies. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 13 | * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 15 | * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 16 | * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | */ 18 | 19 | import {extname, resolve} from 'path'; 20 | 21 | import * as parser from '@babel/parser'; 22 | 23 | const {keys} = Object; 24 | const {stringify} = JSON; 25 | const {hasOwnProperty} = {}; 26 | 27 | export {hasOwnProperty, keys, stringify}; 28 | 29 | // TODO: find out in which case this is actually needed as rollup is forced to export names 30 | export const asModule = name => `(m=>m.__esModule&&m.default||m)(${name})`; 31 | 32 | export const getBody = code => parser.parse(code, parserOptions).program.body; 33 | 34 | export const getCallback = callback => ''.replace.call( 35 | callback, 36 | /^\s*([^(]+)/, 37 | (_, $1) => { 38 | return /^(?:function|)$/.test($1.trim()) ? $1 : 'function'; 39 | } 40 | ); 41 | 42 | export const getGlobal = (namespace, name) => 43 | `${namespace}${isSimple(name) ? `.${name}` : `[${stringify(name)}]`}`; 44 | 45 | export const getInclude = (root, hash) => { 46 | const include = {}; 47 | for (const key of keys(hash)) 48 | include[key] = resolve(root, hash[key]); 49 | return include; 50 | }; 51 | 52 | export const getModule = (require, name) => name[0] === '_' ? 53 | getGlobal(require, name) : 54 | asModule(getGlobal(require, name)); 55 | 56 | export const getName = name => { 57 | const ext = extname(name); 58 | return `${name.slice(0, -ext.length)}@JSinJSON${ext}`; 59 | }; 60 | 61 | export const getNames = specifiers => { 62 | const imports = []; 63 | const exports = []; 64 | for (const {local, exported} of specifiers) { 65 | const {name} = exported; 66 | imports.push(local.name == name ? name : `${local.name}: ${name}`); 67 | exports.push(name); 68 | } 69 | return {imports, exports}; 70 | }; 71 | 72 | export const getRealName = (code, source, replace) => { 73 | let name = slice(code, source).slice(1, -1); 74 | if (replace && hasOwnProperty.call(replace, name)) 75 | name = replace[name]; 76 | return name; 77 | }; 78 | 79 | export const isJS = name => /^\.[mc]?js$/i.test(extname(name)); 80 | export const isLocal = name => /^[./]/.test(name); 81 | export const isSimple = name => /^[$_a-z]+[$_0-9a-z]*$/i.test(name); 82 | 83 | export const slice = (code, info) => code.slice(info.start, info.end); 84 | 85 | export const warn = (...args) => { 86 | console.warn('⚠ \x1b[1mWarning\x1b[0m', ...args); 87 | }; 88 | 89 | export const defaults = { 90 | babel: true, 91 | minify: true, 92 | global: 'self', 93 | prefix: `_${Date.now().toString(36).slice(-2)}` 94 | }; 95 | 96 | export const babelOptions = { 97 | presets: ['@babel/preset-env'], 98 | plugins: [['@babel/plugin-transform-runtime', {useESModules: true}]] 99 | }; 100 | 101 | export const parserOptions = { 102 | allowAwaitOutsideFunction: true, 103 | sourceType: 'module', 104 | plugins: [ 105 | // 'estree', 106 | 'jsx', 107 | 'typescript', 108 | 'exportExtensions', 109 | 'exportDefaultFrom', 110 | 'exportNamespaceFrom', 111 | 'dynamicImport', 112 | 'importMeta', 113 | 'asyncGenerators', 114 | 'bigInt', 115 | 'classProperties', 116 | 'classPrivateProperties', 117 | 'classPrivateMethods', 118 | ['decorators', {decoratorsBeforeExport: true}], 119 | 'doExpressions', 120 | 'functionBind', 121 | 'functionSent', 122 | 'logicalAssignment', 123 | 'nullishCoalescingOperator', 124 | 'numericSeparator', 125 | 'objectRestSpread', 126 | 'optionalCatchBinding', 127 | 'optionalChaining', 128 | 'partialApplication', 129 | ['pipelineOperator', {proposal: 'minimal'}], 130 | 'throwExpressions', 131 | 'topLevelAwait' 132 | ] 133 | }; 134 | -------------------------------------------------------------------------------- /examples/builtin-elements/README.md: -------------------------------------------------------------------------------- 1 | # Builtin Elements 2 | 3 | [Live result](https://webreflection.github.io/js-in-json/examples/builtin-elements/) 4 | 5 | ```sh 6 | cd examples/builtin-elements 7 | npm i 8 | PORT=8080 node server.js 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/builtin-elements/ff.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |