├── .gitignore ├── .npmrc ├── .nvmrc ├── README.md ├── esbuild-plugins ├── esbuild-plugin-minify-css.js ├── esbuild-plugin-minify-html.js └── index.js ├── lib ├── cli.js ├── default.config.js ├── index.js └── util.js ├── package-lock.json ├── package.json ├── post-builds ├── copy.js ├── index.js ├── inject-esbuild-result.js ├── readme-to-html.js ├── run-static-server.js ├── watch-and-reload.js └── websocket-clients.js └── test ├── cli-1.spec.js ├── cli-2.spec.js ├── cli-3.spec.js ├── esbuild-plugins-spec.js ├── index.spec.js ├── post-builds ├── copy.spec.js ├── inject-esbuild-result.spec.js ├── run-static-server.spec.js └── watch-and-reload.spec.js └── test-files ├── excludes └── foo.html ├── index.html ├── main.js ├── test.css └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 15 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esbuild-x 2 | [esbuid](https://esbuild.github.io/) extended; esbuild + pre/post builds 3 | 4 | ## Features 5 | * esbuild CLI available (since it's a dependency) 6 | * esbuild npm available (since it's a dependency) 7 | * plus, accepting configuration file (e.g., `esbuild.config.js`) 8 | * plus, accepting custom pre build functions 9 | * plus, accepting custom post build functions 10 | 11 | ## Install 12 | ``` 13 | $ npm i esbuild-x -D 14 | ``` 15 | 16 | ## Usage as command 17 | ``` 18 | $ esbuild main.js --bundle 19 | $ esbuild-x --config=esbuild.config.js 20 | $ esbuild-x build 21 | $ esbuild-x serve 22 | ``` 23 | 24 | ## Usage as node module 25 | ``` 26 | // esbuild as it is 27 | const esbuild = require('esbuild'); 28 | esbuild.build({ 29 | entryPoints: ['src/main.js'], 30 | entryNames: '[name]-[hash]', 31 | outdir: 'dist', 32 | bundle: true 33 | }) 34 | 35 | // or esbuildX along with pre/post builds extended 36 | const esbuildX = require('esbuild-x'); 37 | esbuildX.build({ 38 | entryPoints: ['src/main.js'], 39 | entryNames: '[name]-[hash]', 40 | outdir: 'dist', 41 | bundle: true, 42 | preBuilds: [ function() {rimraf('dist')} ], 43 | postBuilds: [ function() {console.log('done')} ] 44 | }) 45 | ``` 46 | 47 | ## esbuild-x.config.js example 48 | * all esbuild options are allowed, https://esbuild.github.io/api/#build-api. 49 | * preBuilds: an array of function to run before esbuild.build() 50 | Example 51 | ``` 52 | module.exports = { 53 | build: { 54 | entryPoints: ['src/main.js'], 55 | entryNames: '[name]-[hash]', 56 | outdir: 'dist', 57 | bundle: true, 58 | ... 59 | preBuilds: [ function clear() {rimraf('dist')} ], 60 | ... 61 | } 62 | } 63 | ``` 64 | 65 | * postBuilds: an array of function to run after esbuild.build(). 66 | a post build function takes two parameters internally 67 | * esbuild option 68 | * esbuild result 69 | ``` 70 | module.exports = { 71 | build: { 72 | entryPoints: ['src/main.js'], 73 | entryNames: '[name]-[hash]', 74 | outdir: 'dist', 75 | bundle: true, 76 | ... 77 | postBuilds: [ 78 | async function(_, result) { // bundle analyzer 79 | let text = await esbuild.analyzeMetafile(result.metafile, {verbose: true}); 80 | console.log(text); 81 | } 82 | ] 83 | } 84 | } 85 | ``` 86 | A full example can be found here. 87 | https://github.com/elements-x/elements-x/blob/master/esbuild-x.config.js 88 | 89 | ## built-in esbuild plugins 90 | 91 | ### minifyCssPlugin / minifyHtmlPlubin 92 | esbuild plugin that minify css and html 93 | * Read .css files as a minified text, not as css object. 94 | * Read .html files as a minified text 95 | 96 | src/main.js 97 | ``` 98 | import html from './test.html'; 99 | import css from './test.css'; 100 | console.log(html + css); 101 | ``` 102 | 103 | Example 104 | ``` 105 | const esbuildX = require('esbuild-x'); 106 | const { minifyCssPlugin, minifyHtmlPlugin } = esbuildX.plugins; 107 | const options = { 108 | entryPoints: ['src/main.js'], 109 | plugins: [minifyCssPlugin, minifyHtmlPlugin] 110 | }; 111 | esbuildX.build(options).then(esbuildResult => { 112 | ... 113 | }); 114 | ``` 115 | 116 | ## built-in post builds 117 | 118 | ### copy 119 | copy files to a directory by replacing file contents 120 | 121 | #### parameters 122 | * fromsTo: string. Accepts glob patterns. e.g., 'src/**/!(*.js) public/* dist' 123 | * options: 124 | * replacements: list of replacement objects 125 | * match: replacement happens when this regular expression match to a file path. 126 | * find: regular expression to search string in a file. 127 | * replace: string value to replace the string found. 128 | * excludes: list of exclude patterns. 129 | 130 | Example 131 | ``` 132 | const esbuildX = require('esbuild-x'); 133 | const {copy} = esbuildX.postBuilds 134 | const options = { 135 | entryPoints: ['src/main.js'] 136 | postBuilds: [ 137 | copy('src/**/!(*.js) public/* dist', { 138 | replacements: [ {match: /index\.html/, find: /FOO/, replace: 'BAR'} ], 139 | excludes: [/node_moules/] 140 | }) 141 | ] 142 | } 143 | esbuildX.build(options).then(esbuildResult => { 144 | ... 145 | }) 146 | ``` 147 | 148 | ### injectEsbuildResult 149 | Inject esbuild compile output to index.html. 150 | 151 | e.g. 152 | ``` 153 | 154 | 155 | 156 | 157 | 158 | ``` 159 | 160 | Example 161 | ``` 162 | const esbuildX = require('esbuild-x'); 163 | const {injectEsbuildResult} = esbuildX.postBuilds; 164 | const options = { 165 | entryPoints: ['src/main.js'] 166 | postBuilds: [ injectEsbuildResult() ] 167 | } 168 | esbuildX.build(options).then(esbuildResult => { 169 | ... 170 | }) 171 | ``` 172 | 173 | ### runStaticServer 174 | Run a static http server for a given directory. Two parameters accepted 175 | * dir: a directory to run a static http server. required. 176 | * options (optional) 177 | * fs: file system to run a static server. e.g. `require('memfs')`. Default `require('fs')`. 178 | * port: port number to run a static server. Default 9100 179 | * notFound: 404 redirection logic. 180 | e.g. `{match: /.*$/, serve: path.join(dir, 'index.html')}` 181 | 182 | Example 183 | ``` 184 | const esbuildX = require('esbuild-x'); 185 | const {runStaticServer} = esbuildX.postBuilds; 186 | const options = { 187 | entryPoints: ['src/main.js'] 188 | postBuilds: [ 189 | runStaticServer('src', { 190 | fs: require('memfs'), 191 | port: 9100, 192 | notFound: {match: /.*$/, serve: path.join(dir, 'index.html') 193 | }}) 194 | ] 195 | } 196 | esbuildX.build(options).then(esbuildResult => { 197 | ... 198 | }) 199 | ``` 200 | 201 | ### watchAndReload 202 | Watch the given directory, and rebuild and reload when a file change happens. 203 | It also starts a WebSocket server to reload the browser client when a file change happens. 204 | 205 | Parameters: 206 | * dir: a directory to watch 207 | * websocket port: Optional, Default 9110. If null, it does not run websocket server 208 | 209 | Example 210 | ``` 211 | const esbuildX = require('esbuild-x'); 212 | const {watchAndReload} = esbuildX.postBuilds; 213 | const options = { 214 | entryPoints: ['src/main.js'] 215 | postBuilds: [ 216 | watchAndReload('src', 9110) 217 | ] 218 | } 219 | esbuildX.build(options).then(esbuildResult => { 220 | ... 221 | }) 222 | ``` 223 | -------------------------------------------------------------------------------- /esbuild-plugins/esbuild-plugin-minify-css.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const minifyCssPlugin = { 6 | name: 'minify-css', 7 | setup(build) { 8 | build.onResolve({ filter: /\.css$/ }, args => { 9 | // {path, importer, namespace, resolveDir, kind, pluginData} = args 10 | return { 11 | path: path.join(args.resolveDir, args.path), 12 | namespace: 'minify-css' 13 | // errors?: Message[]; 14 | // external?: boolean; 15 | // path?: string; 16 | // pluginData?: any; 17 | // pluginName?: string; 18 | // warnings?: Message[]; 19 | // watchDirs?: string[]; 20 | // watchFiles?: string[]; 21 | }; 22 | }); 23 | 24 | // onLoad() return the contents of the module and to tell esbuild how to interpret it. 25 | build.onLoad({ filter: /.*/, namespace: 'minify-css' }, async args => { 26 | // {path, namespace, pluginData} = argsff 27 | const fileContents = await fs.promises.readFile(args.path, 'utf8'); 28 | const loader = 'text'; 29 | const esbuildResult = await esbuild.build({ 30 | entryPoints: [args.path], 31 | minify: true, 32 | write: false, 33 | }); 34 | const contents = esbuildResult.outputFiles[0].text; 35 | 36 | return { 37 | contents, 38 | loader, 39 | // errors?: Message[]; 40 | // pluginData?: any; 41 | // pluginName?: string; 42 | // resolveDir?: string; 43 | // warnings?: Message[]; 44 | // watchDirs?: string[]; 45 | // watchFiles?: string[]; 46 | } 47 | }) 48 | }, 49 | } 50 | 51 | module.exports = minifyCssPlugin; -------------------------------------------------------------------------------- /esbuild-plugins/esbuild-plugin-minify-html.js: -------------------------------------------------------------------------------- 1 | const minify = require('html-minifier').minify; 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | const minifyHtmlPlugin = { 6 | name: 'minify-html', 7 | setup(build) { 8 | build.onResolve({ filter: /\.html$/ }, args => { 9 | // {path, importer, namespace, resolveDir, kind, pluginData} = args 10 | return { 11 | path: path.join(args.resolveDir, args.path), 12 | namespace: 'minify-html' 13 | }; 14 | }); 15 | 16 | // onLoad() return the contents of the module and to tell esbuild how to interpret it. 17 | build.onLoad({ filter: /.*/, namespace: 'minify-html' }, async args => { 18 | const fileContents = await fs.promises.readFile(args.path, 'utf8'); 19 | const contents = minify(fileContents, { 20 | minifyCSS: true, 21 | minifyJs: true, 22 | collapseWhitespace: true, 23 | removeAttributeQuotes: true, 24 | removeComments: true 25 | }); 26 | 27 | // {path, namespace, pluginData} = argsff 28 | return { 29 | contents, 30 | loader: 'text' 31 | } 32 | }) 33 | }, 34 | } 35 | 36 | module.exports = minifyHtmlPlugin; -------------------------------------------------------------------------------- /esbuild-plugins/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | minifyHtmlPlugin: require('./esbuild-plugin-minify-html'), 3 | minifyCssPlugin: require('./esbuild-plugin-minify-css') 4 | } -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const yargs = require('yargs/yargs') 5 | const { hideBin } = require('yargs/helpers') 6 | 7 | const { konsole } = require('./util.js'); 8 | const defaultConfig = require('./default.config'); 9 | const esbuildX = require('./index'); 10 | 11 | // command: e.g. build, serve, or src/main.js 12 | // commandArgs : e.g. --config=esbuild-x.config.js --outdir=out 13 | const argv = yargs(hideBin(process.argv)).argv 14 | let command = argv._[0] || 'build'; 15 | const commandArgs = Object.assign({}, {config:'esbuild-x.config.js'}, argv); 16 | delete commandArgs._; 17 | delete commandArgs.$0; 18 | 19 | const userConfigFile = commandArgs.config; 20 | const userConfig = fs.existsSync(userConfigFile) ? 21 | require(path.join(process.cwd(), userConfigFile)) : {}; 22 | const userConfigKeys = Object.keys(userConfig); 23 | const allPossibleKeys = ['build', 'serve', ...userConfigKeys]; 24 | 25 | if (command.match(/\.[a-z]+$/)) { 26 | commandArgs.entryPoints = [command]; 27 | command = 'build'; 28 | } else if (!allPossibleKeys.includes(command)) { 29 | konsole.error('Error, invalid command') 30 | process.exit(1); 31 | } 32 | 33 | // convert 'true' and 'false' to booleans 34 | for(var key in commandArgs) { 35 | (commandArgs[key] === 'true') && (commandArgs[key] = true); 36 | (commandArgs[key] === 'false') && (commandArgs[key] = false); 37 | } 38 | 39 | // merge options 40 | const defaultConfigOptions = defaultConfig[command]; 41 | const userConfigOptions = userConfig[command]; 42 | const options = Object.assign( 43 | {}, 44 | defaultConfigOptions, 45 | userConfigOptions, 46 | commandArgs 47 | ); 48 | 49 | (async function() { 50 | const analyze = options.analyze; 51 | delete options.config; 52 | delete options.analyze; 53 | // console.log({options, command}); 54 | // options.entryPoints = [command]; 55 | const result = await esbuildX.build(options); 56 | if (analyze) { 57 | const verbose = analyze === 'verbose'; 58 | let analytics = await esbuild.analyzeMetafile(result.metafile, {verbose}); 59 | console.log(analytics) 60 | } 61 | })(); -------------------------------------------------------------------------------- /lib/default.config.js: -------------------------------------------------------------------------------- 1 | const { copy, injectEsbuildResult, runStaticServer, watchAndReload} = require('../post-builds'); 2 | 3 | module.exports = { 4 | build: { 5 | entryPoints: ['src/main.js'], 6 | entryNames: '[name]-[hash]', 7 | outdir: 'dist', 8 | bundle: true, 9 | minify: true, 10 | metafile: true, 11 | write: true, 12 | sourcemap: 'external' 13 | }, 14 | serve: { 15 | entryPoints: ['src/main.js'], 16 | entryNames: '[name]', 17 | outdir: 'dist', 18 | bundle: true, 19 | minify: false, 20 | metafile: true, 21 | watch: false, 22 | write: false, 23 | sourcemap: 'external', 24 | // belows are custom options for serve 25 | postBuilds: [ 26 | copy('src/**/* dist'), 27 | injectEsbuildResult(9000), 28 | runStaticServer('src', { 29 | port: 9100, 30 | notFound: {match: /^\//, serve: 'index.html'} 31 | }), 32 | watchAndReload(['src', 'lib']) 33 | ] 34 | } 35 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const esbuild = require('esbuild'); 3 | 4 | const build = async function(options) { 5 | // run preBuilds 6 | (options.preBuilds || []).forEach(preBuildFunc => preBuildFunc(options)); 7 | 8 | // run esbuild 9 | const esbuildOptions = Object.keys(options).reduce( (acc, key) => { 10 | !['preBuilds', 'postBuilds'].includes(key) && (acc[key] = options[key]); 11 | return acc; 12 | }, {metafile: true}); 13 | const esbuildResult = await esbuild.build(esbuildOptions); 14 | 15 | // run postBuilds 16 | (options.postBuilds || []).forEach(postBuildFunc => postBuildFunc(options, esbuildResult) ); 17 | return Promise.resolve(esbuildResult); 18 | }; 19 | 20 | module.exports = Object.assign({}, esbuild, { 21 | build, 22 | plugins: require('../esbuild-plugins'), 23 | postBuilds: require('../post-builds') 24 | }); -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | const konsole = { 2 | LOG_LEVEL: 'info', 3 | LOG_LEVELS: 4 | { all: 0, debug: 1, log: 2, info: 3, warn: 4, error: 5, none: 6 }, 5 | 6 | debug: function() { konsole.write('debug', arguments) }, 7 | info: function() { konsole.write('info', arguments) }, 8 | log: function() { konsole.write('log', arguments) }, 9 | warn: function() { konsole.write('warn', arguments) }, 10 | error: function() { konsole.write('error', arguments) }, 11 | 12 | setLevel: function(level) { konsole.LOG_LEVEL = level}, 13 | write: function(funcName, args) { 14 | const restrictedLevel = konsole.LOG_LEVELS[konsole.LOG_LEVEL]; 15 | const requiredLevel = konsole.LOG_LEVELS[funcName]; 16 | if (restrictedLevel <= requiredLevel) { 17 | console[funcName].apply(console, args); 18 | return true; 19 | } 20 | } 21 | } 22 | 23 | module.exports = { konsole }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esbuild-x", 3 | "version": "0.4.1", 4 | "description": "esbuild + post builds(copy files, inject html, run static server, and watch files)", 5 | "main": "lib/index.js", 6 | "bin": { 7 | "esbuild-x": "lib/cli.js" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/elements-x/esbuild-x.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/elements-x/esbuild-x/issues" 18 | }, 19 | "homepage": "https://github.com/elements-x/esbuild-x#readme", 20 | "author": "allenhwkim@gmail.com", 21 | "license": "MIT", 22 | "keywords": [ 23 | "bundler", 24 | "webpack", 25 | "esbuild", 26 | "static", 27 | "html", 28 | "javascript", 29 | "es6", 30 | "inject", 31 | "static server", 32 | "copy", 33 | "watch", 34 | "websocket" 35 | ], 36 | "dependencies": { 37 | "chokidar": "^3.5.2", 38 | "esbuild": "^0.13.4", 39 | "glob": "^7.2.0", 40 | "html-minifier": "^4.0.0", 41 | "marked": "^4.0.15", 42 | "memfs": "^3.3.0", 43 | "ws": "^8.2.3", 44 | "yargs": "^17.2.1" 45 | }, 46 | "devDependencies": { 47 | "axios": "^0.24.0", 48 | "jest": "^27.3.1", 49 | "kill-port": "^1.6.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /post-builds/copy.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const nodefs = require('fs'); 4 | 5 | function copyFromTo(from, to, {fs, replacements, excludes}) { 6 | const wildcard = from.indexOf('*') !== -1; 7 | const pattern = !wildcard && nodefs.lstatSync(from).isDirectory() ? `${from}/**/*` : from; 8 | const fromDirname = from.match(/\//) ? path.dirname(from.replace(/\/\*.*/, '/wildcard')) : from; 9 | 10 | glob.sync(pattern).forEach(file => { 11 | const excluded = excludes.some(exclude => file.match(exclude)); 12 | if (excluded) return; 13 | 14 | const target = file.replace(fromDirname, to); 15 | const targetDir= path.dirname(target); 16 | if (!fs.existsSync(targetDir)) { 17 | fs.mkdirSync(targetDir, {recursive: true}); 18 | // console.debug('creating directory', targetDir); 19 | } 20 | if (nodefs.lstatSync(file).isDirectory()) { 21 | fs.mkdirSync(target, {recursive: true}); 22 | // console.log('[esbuild-x] creating directory', target); 23 | console.log('[esbuild-x copy]', {from: file, to: target}); 24 | } else { 25 | // if match to replace, run replace 26 | const repls = replacements.filter(repl => repl.match.test(file)); 27 | if (repls.length) { 28 | let contents = nodefs.readFileSync(file, 'utf8'); 29 | repls.forEach(replacement => { 30 | contents = contents.replace(replacement.find, replacement.replace); 31 | }); 32 | fs.writeFileSync(target, contents); 33 | } else { 34 | fs.writeFileSync(target, nodefs.readFileSync(file)); 35 | } 36 | // console.log('[esbuild-x] copying files', {from: file, to: target}); 37 | } 38 | }); 39 | } 40 | 41 | // copy files or directories to a given directory 42 | module.exports = function copy(fromTos, {replacements, excludes} = {}) { 43 | replacements = (replacements || []).concat({ 44 | match: /index\.html/, 45 | find: /]*)>/, 46 | replace: `` 47 | }); 48 | excludes = excludes || []; 49 | 50 | return function copy(options={}, esbuildResult) { 51 | const fs = options.write ? require('fs') : require('memfs'); 52 | fromTos = typeof fromTos === 'string' ? [fromTos] : fromTos; 53 | fromTos.forEach(strExpr => { 54 | const froms = strExpr.split(' ').slice(0, -1); 55 | const to = strExpr.split(' ').slice(-1)[0]; 56 | console.info(`[esbuild-x copy] ${froms} -> ${to}`); 57 | froms.forEach( from => { 58 | if (!from.match(/\*/) && !nodefs.existsSync(from)) { // specified a file, but not exists 59 | console.log('[esbuild-x copy] not exists', from); 60 | } else { 61 | copyFromTo(from, to, {fs, replacements, excludes}) 62 | } 63 | }); 64 | }) 65 | } 66 | } -------------------------------------------------------------------------------- /post-builds/index.js: -------------------------------------------------------------------------------- 1 | const copy = require('./copy'); 2 | const injectEsbuildResult = require('./inject-esbuild-result'); 3 | const runStaticServer = require('./run-static-server'); 4 | const watchAndReload = require('./watch-and-reload'); 5 | const readmeToHtml = require('./readme-to-html'); 6 | 7 | module.exports = { 8 | copy, 9 | injectEsbuildResult, 10 | runStaticServer, 11 | watchAndReload, 12 | readmeToHtml 13 | }; -------------------------------------------------------------------------------- /post-builds/inject-esbuild-result.js: -------------------------------------------------------------------------------- 1 | const { build } = require('esbuild'); 2 | const path = require('path'); 3 | 4 | module.exports = function injectEsbuildResult(srcIndexPath) { 5 | srcIndexPath ||= path.join(process.cwd(), 'src', 'index.html'); 6 | 7 | return function(buildOptions, buildResult) { 8 | const fs = buildOptions.write ? require('fs') : require('memfs'); 9 | 10 | // write build result, e.g. main.js, to outdir if write is false 11 | if (!buildOptions.write) { 12 | fs.mkdirSync(path.join(process.cwd(), buildOptions.outdir), {recursive: true}); 13 | buildResult.outputFiles.forEach(file => { 14 | if (file.path.startsWith('/')) { 15 | fs.writeFileSync(file.path, file.contents) 16 | } else { 17 | const filePath = path.join(process.cwd(), buildOptions.outdir, file.path); 18 | fs.writeFileSync(filePath, file.contents) 19 | } 20 | }); 21 | } 22 | 23 | // get html to inject to the end of of index.html 24 | const buildFileNames = buildResult.outputFiles ? 25 | buildResult.outputFiles.map(el => el.path) : Object.keys(buildResult.metafile.outputs); 26 | const htmlToInject = buildFileNames.map(fileName => { 27 | const path = fileName.split('/').slice(-1)[0]; 28 | const ext = path.match(/\.([a-z]+)$/)[1]; 29 | return ext === 'js' ? `` : 30 | ext === 'css' ? `` : ''; 31 | }).join(''); 32 | 33 | // update index.html 34 | // const srcIndexPath = path.join(process.cwd(), 'src', 'index.html'); 35 | const outIndexPath = path.join(buildOptions.outdir, 'index.html'); 36 | const contents = require('fs').readFileSync(srcIndexPath, {encoding: 'utf8'}) 37 | .replace(/<\/body>/, `${htmlToInject}\n`); 38 | fs.writeFileSync(outIndexPath, contents); 39 | 40 | console.info(`[esbuild-x inject-esbuild-results] ${htmlToInject} into ${outIndexPath}`); 41 | 42 | return outIndexPath; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /post-builds/readme-to-html.js: -------------------------------------------------------------------------------- 1 | const marked = require('marked'); 2 | const path = require('path'); 3 | 4 | module.exports = function readmeToHtml(readMePath, htmlPath) { 5 | readMePath = readMePath || path.join(process.cwd(), 'README.md'); 6 | htmlPath = htmlPath || path.join(process.cwd(), 'dist/readme.html'); 7 | 8 | return function(buildOptions, buildResult) { 9 | const fs = buildOptions.write ? require('fs') : require('memfs'); 10 | const markdown = require('fs').readFileSync(readMePath, 'utf8'); 11 | 12 | const html = marked.parse(markdown).replace(/
/g, '
'); 13 | fs.writeFileSync(htmlPath, html); 14 | console.info(`[esbuild-x readme-to-html] ${readMePath} into ${htmlPath}`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /post-builds/run-static-server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const http = require('http'); 3 | 4 | // dir 5 | // options 6 | // - port 7 | // - notFound: {match: /^\/(component|tool|css)/, serve: 'dist/index.html'} 8 | module.exports = function runStaticServer(dir, {fs, port, notFound, open}={}) { 9 | // fs = fs || require('fs'); 10 | port = port || 9100; 11 | notFound = notFound || {match: /\/[^\.]+$/, serve: 'index.html'}; 12 | 13 | return function(options, esbuildResult) { 14 | fs = fs || (options.write ? require('fs') : require('memfs')); 15 | /** 16 | * run the static server 17 | */ 18 | const server = http.createServer(function (req, res) { 19 | let filePath = new URL(`file://${req.url}`).pathname; 20 | filePath = path.join(dir, filePath); 21 | fs.existsSync(filePath) && fs.lstatSync(filePath).isDirectory() && 22 | (filePath = path.join(filePath, 'index.html')); 23 | 24 | if (fs.existsSync(filePath)) { 25 | const contents = fs.readFileSync(filePath); 26 | res.end(contents); 27 | } else if (req.url.match(notFound.match)) { 28 | // console.info('\n[esbuild-x serve]', 404, req.url, '->', notFound.serve ); // too much output when run cypress test 29 | const notFoundServeFilePath = path.join(dir, notFound.serve); 30 | const contents = fs.readFileSync(notFoundServeFilePath, {encoding: 'utf8'}); 31 | res.end(contents); 32 | } 33 | }); 34 | server.listen(port); 35 | console.info(`[esbuild-x run-static-server] http static server running, http://localhost:${port}`); 36 | 37 | if (open) { 38 | require('open')(`http://localhost:${port}`); 39 | } 40 | return server; 41 | }; 42 | } -------------------------------------------------------------------------------- /post-builds/watch-and-reload.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const path = require('path'); 3 | const WebSocketServer = require('ws').Server; 4 | const wsClients = require('./websocket-clients'); 5 | const injectEsbuildResult = require('./inject-esbuild-result'); 6 | 7 | // returns options for esbuild only from all possible options by excluding keys 8 | function getEsbuildOptions(allOptions) { 9 | const excludes = `config,preBuilds,postBuilds`.split(','); 10 | const esbuildOptions = {...allOptions}; 11 | excludes.forEach(key => delete esbuildOptions[key]); 12 | return esbuildOptions; 13 | } 14 | 15 | function longestCommonSubstring(str1, str2) { 16 | let i = 0; 17 | while (str1.charAt(i) === str2.charAt(i)) i++; 18 | return str1.substring(0, i); 19 | } 20 | 21 | function runWebSocketServer(port) { 22 | const wss = new WebSocketServer({port}); 23 | // wss.on('connection', socket => wsClients.push(socket)); 24 | wss.on('connection', socket => { 25 | wsClients.pop(); 26 | wsClients.push(socket); 27 | }); // talk only to the last one 28 | console.info(`[esbuild-x watch-and-reload] websocket server running on ${port}`); 29 | return wss; 30 | } 31 | 32 | function injectWebSocketClient(port, srcIndexHTML, outIndexPath, fs) { 33 | const wsHtml = `\n`; 37 | const contents = srcIndexHTML.replace(/<\/body>/, `${wsHtml}\n`); 38 | fs.writeFileSync(outIndexPath, contents); 39 | } 40 | 41 | module.exports = function runWatchAndReload(watchDir, websocketPort = 9110, srcIndexPath) { 42 | // watch file change and broadcast it to web browsers 43 | 44 | return function runWatchAndReload(options, buildResult) { 45 | const fs = options.write ? require('fs') : require('memfs'); 46 | 47 | /** 48 | * run websocket server to reload web browsers when file change 49 | */ 50 | const wss = websocketPort && runWebSocketServer(websocketPort); 51 | const srcIndexHTML = require('fs').readFileSync(srcIndexPath, {encoding: 'utf8'}); 52 | const outIndexPath = path.join(options.outdir, 'index.html'); 53 | injectWebSocketClient(websocketPort, srcIndexHTML, outIndexPath, fs); // this injects ws client into index.html 54 | 55 | /** 56 | * watch file changes and rebuild, copy file, then broadcast to web browsers 57 | */ 58 | const watcher = require('chokidar').watch(watchDir, { 59 | ignoreInitial: true, 60 | interval: 1000 61 | }); 62 | watcher.on('all', async (event, filePath) => { 63 | console.info(`[esbuild-x serve] file ${event} detected in ${filePath}`); 64 | 65 | // rebuild 66 | const esbuildOptions = getEsbuildOptions(options); 67 | esbuildOptions.metafile = true; 68 | const esbuildResult = await esbuild.build(esbuildOptions); 69 | esbuildResult.outputFiles.forEach(outputFile => { 70 | console.log('[esbuild-x watch-and-reload]', {outFilePath: outputFile.path}); 71 | fs.writeFileSync(outputFile.path, outputFile.contents); 72 | }); 73 | 74 | // Inject new build .js file into index.html 75 | const indexPath = injectEsbuildResult(srcIndexPath)(options, esbuildResult); 76 | // Inject websocket client to index.html 77 | injectWebSocketClient(websocketPort, srcIndexHTML, outIndexPath, fs); 78 | 79 | // When a file changed outside of build process, copy it to dest except index.html. 80 | // e.g. src/assets/logo.png, src/components/ace.html 81 | if (!esbuildResult.metafile.inputs[filePath] && !filePath.match(/index\.html$/)) { 82 | const contents = require('fs').readFileSync(filePath, {encoding: 'utf8'}); 83 | 84 | const commonPath = longestCommonSubstring(filePath, options.outdir); 85 | const pathRelPath = filePath.replace(commonPath, ''); 86 | const outdirRelPath = options.outdir.replace(commonPath, ''); 87 | const destPath = pathRelPath.replace(/^.*?\//, outdirRelPath + '/'); 88 | 89 | const outPath = commonPath + destPath; 90 | if (fs.existsSync(path.dirname(outPath)) === false) { 91 | fs.mkdirSync(path.dirname(outPath)); 92 | } 93 | 94 | fs.writeFileSync(commonPath + destPath, contents); 95 | console.info('[esbuild-x watch-and-reload]', filePath, '>', destPath ) 96 | } 97 | 98 | // broadcast to web browsers 99 | console.log('[esbuild-x watch-and-reload]', 'web socket client length', wsClients.length); 100 | wsClients.forEach(wsClient => wsClient.send('reload')); 101 | console.info(`[esbuild-x watch-and-reload] watching changes on ${watchDir}`); 102 | }); 103 | 104 | return {watcher, wss}; 105 | } 106 | 107 | } -------------------------------------------------------------------------------- /post-builds/websocket-clients.js: -------------------------------------------------------------------------------- 1 | module.exports = []; -------------------------------------------------------------------------------- /test/cli-1.spec.js: -------------------------------------------------------------------------------- 1 | const esbuildX = require('../lib'); 2 | const fs = require('fs'); 3 | const {konsole} = require('../lib/util.js'); 4 | 5 | jest.mock('../lib/util.js'); 6 | konsole.error = jest.fn().mockReturnValue(null); 7 | 8 | jest.mock('fs'); 9 | jest.mock('../lib'); 10 | 11 | /** 12 | * cli app are the same except it uses the following 13 | * - process.stdin(.stdout, .argv, .exit) 14 | */ 15 | 16 | test('esbuild-x cli "esbuild-x main.js"', done => { 17 | fs.existsSync = jest.fn().mockReturnValue(false); 18 | 19 | process.argv = `node esbuild-x main.js`.split(' '); 20 | require('../lib/cli.js'); 21 | expect(esbuildX.build).toHaveBeenCalledWith({ 22 | entryPoints: [ 'main.js' ], 23 | entryNames: '[name]-[hash]', 24 | outdir: 'dist', 25 | bundle: true, 26 | minify: true, 27 | metafile: true, 28 | write: true, 29 | sourcemap: 'external' 30 | }); 31 | done(); 32 | }); 33 | 34 | // test('esbuild-x cli "esbuild-x INVALID"', () => { 35 | // fs.existsSync = jest.fn().mockReturnValue(false); 36 | // jest.spyOn(process, 'exit').mockImplementationOnce(() => { 37 | // throw new Error('process.exit() was called.') 38 | // }); 39 | 40 | // process.argv = `node esbuild-x INVALID`.split(' '); 41 | 42 | // expect(_ => require('../lib/cli.js')).toThrow('process.exit() was called.'); 43 | // expect(process.exit).toHaveBeenCalledWith(1); 44 | // expect(konsole.error).toHaveBeenCalledWith('Error, invalid command'); 45 | // }); 46 | 47 | // test('esbuild-x cli - esbuild-x serve --outdir=out --minify=false', () => { 48 | // fs.existsSync = jest.fn().mockReturnValue(false); 49 | // esbuildX.build = jest.fn(); 50 | 51 | // process.argv = `node esbuild-x serve --outdir=out --minify=false`.split(' '); 52 | // require('../lib/cli.js'); 53 | 54 | // expect(esbuildX.build).toHaveBeenCalledWith(expect.objectContaining( 55 | // { 56 | // entryPoints: [ 'src/main.js' ], 57 | // entryNames: '[name]', 58 | // outdir: 'out', 59 | // bundle: true, 60 | // minify: false, 61 | // metafile: true, 62 | // watch: false, 63 | // write: false, 64 | // sourcemap: 'external' 65 | // }) 66 | // ) 67 | // }); -------------------------------------------------------------------------------- /test/cli-2.spec.js: -------------------------------------------------------------------------------- 1 | const esbuildX = require('../lib'); 2 | const fs = require('fs'); 3 | const {konsole} = require('../lib/util.js'); 4 | 5 | jest.mock('../lib/util.js'); 6 | konsole.error = jest.fn().mockReturnValue(null); 7 | 8 | jest.mock('fs'); 9 | jest.mock('../lib'); 10 | 11 | /** 12 | * cli app are the same except it uses the following 13 | * - process.stdin(.stdout, .argv, .exit) 14 | */ 15 | 16 | test('esbuild-x cli "esbuild-x INVALID"', () => { 17 | fs.existsSync = jest.fn().mockReturnValue(false); 18 | jest.spyOn(process, 'exit').mockImplementationOnce(() => { 19 | throw new Error('process.exit() was called.') 20 | }); 21 | 22 | process.argv = `node esbuild-x INVALID`.split(' '); 23 | 24 | expect(_ => require('../lib/cli.js')).toThrow('process.exit() was called.'); 25 | expect(process.exit).toHaveBeenCalledWith(1); 26 | expect(konsole.error).toHaveBeenCalledWith('Error, invalid command'); 27 | }); 28 | 29 | // test('esbuild-x cli - esbuild-x serve --outdir=out --minify=false', () => { 30 | // fs.existsSync = jest.fn().mockReturnValue(false); 31 | // esbuildX.build = jest.fn(); 32 | 33 | // process.argv = `node esbuild-x serve --outdir=out --minify=false`.split(' '); 34 | // require('../lib/cli.js'); 35 | 36 | // expect(esbuildX.build).toHaveBeenCalledWith(expect.objectContaining( 37 | // { 38 | // entryPoints: [ 'src/main.js' ], 39 | // entryNames: '[name]', 40 | // outdir: 'out', 41 | // bundle: true, 42 | // minify: false, 43 | // metafile: true, 44 | // watch: false, 45 | // write: false, 46 | // sourcemap: 'external' 47 | // }) 48 | // ) 49 | // }); -------------------------------------------------------------------------------- /test/cli-3.spec.js: -------------------------------------------------------------------------------- 1 | const esbuildX = require('../lib'); 2 | const fs = require('fs'); 3 | const {konsole} = require('../lib/util.js'); 4 | 5 | jest.mock('../lib/util.js'); 6 | konsole.error = jest.fn().mockReturnValue(null); 7 | 8 | jest.mock('fs'); 9 | jest.mock('../lib'); 10 | 11 | test('esbuild-x cli - esbuild-x serve --outdir=out --minify=false', () => { 12 | fs.existsSync = jest.fn().mockReturnValue(false); 13 | esbuildX.build = jest.fn(); 14 | 15 | process.argv = `node esbuild-x serve --outdir=out --minify=false`.split(' '); 16 | require('../lib/cli.js'); 17 | 18 | expect(esbuildX.build).toHaveBeenCalledWith(expect.objectContaining( 19 | { 20 | entryPoints: [ 'src/main.js' ], 21 | entryNames: '[name]', 22 | outdir: 'out', 23 | bundle: true, 24 | minify: false, 25 | metafile: true, 26 | watch: false, 27 | write: false, 28 | sourcemap: 'external' 29 | }) 30 | ) 31 | }); -------------------------------------------------------------------------------- /test/esbuild-plugins-spec.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const path = require('path'); 3 | const {minifyCssPlugin, minifyHtmlPlugin} = require('../esbuild-plugins'); 4 | 5 | let esbuildResult; 6 | beforeEach( async () => { 7 | const options = { 8 | entryPoints: [path.join(__dirname, 'test-files/main.js')], 9 | plugins: [minifyCssPlugin, minifyHtmlPlugin], 10 | entryNames: '[name]', 11 | bundle: true, 12 | minify: true, 13 | write: false 14 | }; 15 | 16 | esbuildResult = await esbuild.build(options); 17 | }); 18 | 19 | afterEach(() => {/* test cleanup */}); 20 | 21 | test('plugin - minify html / minify css', () => { 22 | const out = esbuildResult.outputFiles[0].text; 23 | expect(out.length).toBe(488); 24 | }); 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const {vol} = require('memfs'); 3 | 4 | const { copy } = require('../post-builds'); 5 | const {konsole} = require('../lib/util'); 6 | konsole.LOG_LEVEL = 'error'; 7 | 8 | const esbuildX = require('../lib'); 9 | 10 | beforeEach( async () => {}); 11 | afterEach( async () => { }); 12 | 13 | test('esbuildX.build', async () => { 14 | const preBuildFunc = jest.fn(); 15 | const postBuildFunc = jest.fn(); 16 | const result = await esbuildX.build({ 17 | entryPoints: ['test/test-files/main.js'], 18 | entryNames: '[name]', 19 | outdir: 'dist', 20 | bundle: true, 21 | minify: true, 22 | write: false, 23 | metafile: true, 24 | plugins: [esbuildX.plugins.minifyHtmlPlugin], 25 | preBuilds: [preBuildFunc], 26 | postBuilds: [postBuildFunc] 27 | }); 28 | 29 | // esbuildX properties 30 | expect(typeof esbuildX.build).toBe('function'); 31 | expect(typeof esbuildX.buildSync).toBe('function'); 32 | expect(typeof esbuildX.transformSync).toBe('function'); 33 | expect(esbuildX.plugins).toBeTruthy(); 34 | expect(esbuildX.postBuilds).toBeTruthy(); 35 | 36 | // esbuildX.build must calls preBuild and postBuild functions 37 | expect(preBuildFunc).toHaveBeenCalled(); 38 | expect(postBuildFunc).toHaveBeenCalled(); 39 | 40 | // esbuildX.build must run esbuild.build 41 | expect(result.warnings.length).toBe(0); 42 | expect(result.errors.length).toBe(0); 43 | expect(result.metafile.outputs['dist/main.js']).toBeTruthy(); 44 | expect(result.metafile.outputs['dist/main.css']).toBeTruthy(); 45 | 46 | }); 47 | -------------------------------------------------------------------------------- /test/post-builds/copy.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { copy } = require('../../post-builds'); 3 | const {konsole} = require('../../lib/util'); 4 | konsole.LOG_LEVEL = 'error'; 5 | 6 | beforeEach( async () => {}); 7 | afterEach( async () => { }); 8 | 9 | test('post-builds - copy', () => { 10 | const {fs, vol} = require('memfs'); 11 | const replacements = [ 12 | {match: /index\.html/, find: /FOO/, replace: 'BAR'} 13 | ]; 14 | const excludes = [/excludes/]; 15 | // path is relative to main directory, which package.json exists 16 | copy('test/test-files/**/!(*.js) dist', {fs, replacements, excludes})(); 17 | 18 | const result = vol.toJSON(); 19 | const prefix = path.join(process.cwd(), 'dist'); 20 | expect(result[`${prefix}/index.html`]).toBeTruthy(); 21 | expect(result[`${prefix}/test.html`]).toBeTruthy(); 22 | expect(result[`${prefix}/test.css`]).toBeTruthy(); 23 | expect(result[`${prefix}/excludes`]).toBeFalsy(); 24 | expect(result[`${prefix}/excludes/foo.html`]).toBeFalsy(); 25 | expect(result[`${prefix}/index.html`]).toContain(''); 27 | }); 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/post-builds/inject-esbuild-result.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { injectEsbuildResult } = require('../../post-builds'); 3 | const { konsole } = require('../../lib/util'); 4 | konsole.LOG_LEVEL = 'error'; 5 | 6 | // beforeEach( async () => { }); 7 | // afterEach( async () => { }); 8 | 9 | test('post-builds - injectEsbuildResult', () => { 10 | const {fs, vol} = require('memfs'); 11 | 12 | // write to index.html, path is relative to main directory, which package.json exists 13 | fs.mkdirSync('dist', {recursive: true}); 14 | fs.writeFileSync('dist/index.html', ``); 15 | 16 | // FYI, path is relative to main directory, which package.json exists 17 | const buildOptions = { entryPoints: 'test/test-files/main.js', outdir: 'dist' }; 18 | const wss = injectEsbuildResult('test/test-files/index.html')(buildOptions, { 19 | outputFiles: [ {path: 'main.js', contents: 'MAIN_JS'} ] 20 | }); 21 | 22 | // then verify the result 23 | const result = vol.toJSON(); 24 | 25 | expect(result[path.resolve('dist/main.js')]).toBe('MAIN_JS'); 26 | expect(result[path.resolve('dist/index.html')]).toContain(` `); 27 | }); 28 | -------------------------------------------------------------------------------- /test/post-builds/run-static-server.spec.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const killport = require('kill-port'); 3 | 4 | const { runStaticServer } = require('../../post-builds'); 5 | const {konsole} = require('../../lib/util'); 6 | konsole.LOG_LEVEL = 'error'; 7 | 8 | const port = 9100; 9 | beforeAll(async () => await killport(port)); 10 | // beforeEach( async () => { }); 11 | // afterEach( async () => { }); 12 | 13 | test('post-builds - runStaticServer', async () => { 14 | const server = runStaticServer('test/test-files', { // relative to main directory 15 | fs: require('fs'), 16 | port: port 17 | })({}); 18 | 19 | // serve test.html 20 | const res1 = await axios(`http://localhost:${port}/test.html`); 21 | expect(res1.status).toBe(200); 22 | 23 | // serve test.css 24 | const res2 = await axios(`http://localhost:${port}/test.css`); 25 | expect(res2.status).toBe(200); 26 | 27 | // serve / (/index.html) 28 | const res3 = await axios(`http://localhost:${port}`); 29 | 30 | expect(res3.status).toBe(200); 31 | 32 | // serve 404 -> /index.html 33 | const res4 = await axios(`http://localhost:${port}/foo`); 34 | expect(res4.status).toBe(200); 35 | 36 | server.close(); 37 | }); -------------------------------------------------------------------------------- /test/post-builds/watch-and-reload.spec.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const { fs, vol } = require('memfs'); 3 | const path = require('path'); 4 | const killport = require('kill-port'); 5 | 6 | const { watchAndReload } = require('../../post-builds'); 7 | const wsClients = require('../../post-builds/websocket-clients'); 8 | const {konsole} = require('../../lib/util'); 9 | konsole.LOG_LEVEL = 'error'; 10 | 11 | const wsPort = 9110; 12 | beforeAll(async () => { 13 | await killport(wsPort); 14 | }); 15 | 16 | test('post-builds - watchAndReload', done => { 17 | // path is relative to main directory, which package.json 18 | // const watchDir = path.join(__dirname, '..', 'test-files'); 19 | const watchDir = 'test/test-files'; 20 | const buildOptions = { 21 | bundle: true, 22 | write: false, 23 | metafile: true, 24 | outdir: 'dist', 25 | loader: {'.css': 'text', '.html': 'text'}, 26 | entryPoints: ['test/test-files/main.js'] 27 | }; 28 | require('memfs').mkdirSync('dist', {recursive: true}); 29 | const {watcher} = watchAndReload(watchDir, null, 'test/test-files/index.html')(buildOptions); 30 | 31 | setTimeout(_ => { // to avoid initial changes detection 32 | const filename = 'test/test-files/index.html'; 33 | const contents = require('fs').readFileSync(filename); 34 | require('fs').writeFileSync(filename, contents); 35 | 36 | setTimeout(_ => { 37 | // rebuild must happen 38 | expect(fs.existsSync('dist/main.js')).toBe(true); 39 | // updated file file must be copied to outdir 40 | //expect(fs.existsSync('dist/index.html')).toBe(true); 41 | watcher.close(); 42 | done() 43 | }, 500) 44 | }, 1000); 45 | }); 46 | 47 | test('post-builds - watch and reload - websocket', done => { 48 | const {fs, vol} = require('memfs'); 49 | const watchDir = 'test/test-files'; 50 | const buildOptions = { 51 | bundle: true, 52 | write: false, 53 | metafile: true, 54 | outdir: 'dist', 55 | loader: {'.css': 'text', '.html': 'text'}, 56 | entryPoints: ['test/test-files/main.js'] 57 | }; 58 | fs.mkdirSync('dist', {recursive: true}); 59 | fs.writeFileSync( 'dist/index.html', ``); 60 | 61 | const {watcher, wss} = watchAndReload(watchDir, wsPort, 'test/test-files/index.html')(buildOptions); 62 | setTimeout(_ => { // to avoid initial changes detection 63 | // then verify the result 64 | const result = vol.toJSON(); 65 | const outPath = path.resolve(path.join('dist', 'index.html')); 66 | expect(result[outPath]).toContain(`ws://localhost:${wsPort}`); 67 | expect(result[outPath]).toContain('ws.onmessage = e =>'); 68 | expect(result[outPath]).toContain('window.location.reload();'); 69 | 70 | expect(wsClients.length).toBe(0); 71 | const ws = new WebSocket(`ws://localhost:${wsPort}`); // a websocket client 72 | ws.on('open', _ => expect(wsClients.length).toBe(1)) 73 | 74 | setTimeout(_ => { 75 | ws.close(); 76 | wss.close(); 77 | watcher.close(); 78 | done(); 79 | }, 1000) 80 | }); 81 | }); -------------------------------------------------------------------------------- /test/test-files/excludes/foo.html: -------------------------------------------------------------------------------- 1 | this must be excluded -------------------------------------------------------------------------------- /test/test-files/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/test-files/main.js: -------------------------------------------------------------------------------- 1 | import html from './test.html'; 2 | import css from './test.css'; 3 | console.log(html + css); -------------------------------------------------------------------------------- /test/test-files/test.css: -------------------------------------------------------------------------------- 1 | /* Applies to the entire body of the HTML document (except where overridden by more specific 2 | selectors). */ 3 | body { 4 | margin: 25px; 5 | background-color: rgb(240,240,240); 6 | font-family: arial, sans-serif; 7 | font-size: 14px; 8 | } 9 | 10 | /* Applies to all

...

elements. */ 11 | h1 { 12 | font-size: 35px; 13 | font-weight: normal; 14 | margin-top: 5px; 15 | } 16 | 17 | /* Applies to all elements with <... class="someclass"> specified. */ 18 | .someclass { color: red; } 19 | 20 | /* Applies to the element with <... id="someid"> specified. */ 21 | #someid { color: green; } -------------------------------------------------------------------------------- /test/test-files/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is the title of the webpage! 5 | 6 | 7 |

8 | This is an example paragraph. Anything in the 9 | body tag will appear on the page, 10 | just like this p tag and its contents. 11 |

12 | 13 | --------------------------------------------------------------------------------