UNPKG

svelte-runner

Version:

Run svelte components. Zero configuration necessary.

503 lines (414 loc) 14.6 kB
#!/usr/bin/env node 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var pathUtils = require('path'); var express = require('express'); var openPath = require('open'); var pathType = require('path-type'); var httpProxy = require('http-proxy'); var svelte = require('rollup-plugin-svelte-hot'); var sveltePreprocess = require('svelte-preprocess'); var makeNollupMiddleware = require('nollup/lib/dev-middleware'); var commonjs = require('@rollup/plugin-commonjs'); var nodeResolve = require('@rollup/plugin-node-resolve'); var virtual = require('@rollup/plugin-virtual'); var sucrase = require('@rollup/plugin-sucrase'); var rollup = require('rollup'); var autoProcess = require('svelte-preprocess/dist/autoProcess'); var rollupPluginTerser = require('rollup-plugin-terser'); var svelte$1 = require('rollup-plugin-svelte'); var commander = require('commander'); var fs = require('fs'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var pathUtils__default = /*#__PURE__*/_interopDefaultLegacy(pathUtils); var express__default = /*#__PURE__*/_interopDefaultLegacy(express); var openPath__default = /*#__PURE__*/_interopDefaultLegacy(openPath); var httpProxy__default = /*#__PURE__*/_interopDefaultLegacy(httpProxy); var svelte__default = /*#__PURE__*/_interopDefaultLegacy(svelte); var sveltePreprocess__default = /*#__PURE__*/_interopDefaultLegacy(sveltePreprocess); var makeNollupMiddleware__default = /*#__PURE__*/_interopDefaultLegacy(makeNollupMiddleware); var commonjs__default = /*#__PURE__*/_interopDefaultLegacy(commonjs); var nodeResolve__default = /*#__PURE__*/_interopDefaultLegacy(nodeResolve); var virtual__default = /*#__PURE__*/_interopDefaultLegacy(virtual); var sucrase__default = /*#__PURE__*/_interopDefaultLegacy(sucrase); var svelte__default$1 = /*#__PURE__*/_interopDefaultLegacy(svelte$1); const generatedLocations = { js: '/_sr-gen/main.js', jsMap: '/_sr-gen/main.js.map', css: '/_sr-gen/main.css', cssMap: '/_sr-gen/main.css.map', additionalScript: (path) => pathUtils__default['default'].join('/_sr-gen', path), }; function makeTemplate(separateStyles, coreOptions) { const headTags = [ `<meta charset="UTF-8">`, `<meta name="viewport" content="width=device-width, initial-scale=1.0">`, `<title>${coreOptions.title || 'Svelte App'}</title>`, `<script defer src="${generatedLocations.js}"></script>`, ]; if (separateStyles) headTags.push(`<link rel="stylesheet" href="${generatedLocations.css}">`); if (coreOptions.additionalScripts) headTags.push( ...coreOptions.additionalScripts.map(file => `<script defer src="${generatedLocations.additionalScript(file)}"></script>`) ); if (coreOptions.headers) headTags.push(coreOptions.headers); return `<!DOCTYPE html><html lang="en"><head>${headTags.join('')}</head><body></body></html>` } function pathIsPattern(pattern, path) { if (!pattern.startsWith('/')) throw new Error(`${pattern} is an improperly configured pattern. All patterns must start with a slash.`) if (!path.startsWith('/')) throw new Error(`How on earth did you get a path that doesn't start with a slash?`) // Remove the first slash from the path/pattern, and split the rest up on the slashes const patternSegments = pattern.slice(1).split('/'); const pathSegments = path.slice(1).split('/'); let patternIndex = 0; let lastWasGreedy = false; let failed = false; for (let pathSegment of pathSegments) { const patternSegment = patternSegments[patternIndex]; if (!patternSegment) { if (!lastWasGreedy) failed = true; break } else if (patternSegment === '**') { patternIndex++; const nextPatternSegment = patternSegments[patternIndex]; if (nextPatternSegment && isEqual(nextPatternSegment, pathSegment)) patternIndex++; else lastWasGreedy = true; continue } else { if (isEqual(patternSegment, pathSegment)) { lastWasGreedy = false; patternIndex++; continue } else if (lastWasGreedy) { continue } else { failed = true; break } } } if (!failed && patternIndex < patternSegments.length) failed = true; return !failed } function isEqual(patternSegment, pathSegment) { if (patternSegment === '*') return true if (patternSegment.indexOf('*') !== -1) { const parts = patternSegment.split('*'); let playPath = pathSegment; let failed = false; for (let i in parts) { const index = Number(i); const part = parts[index]; const firstOne = index === 0; const lastOne = index === parts.length - 1; const matchIndex = playPath.indexOf(part); if (matchIndex === -1) { failed = true; break } if (firstOne && matchIndex !== 0) { failed = true; break } if (lastOne && matchIndex + part.length !== playPath.length) { // the last item is not at the end failed = true; break } playPath = playPath.slice(matchIndex + part.length); } if (failed) return false return true } return patternSegment === pathSegment } async function server({ staticMap, template, serveBuild, port, host, open, css }) { const app = express__default['default'](); app.use(serveBuild(app)); const proxy = httpProxy__default['default'].createProxyServer(); if (css) { app.get(generatedLocations.css, (_, res) => { res.contentType('.css'); res.send(css().code); }); app.get(generatedLocations.cssMap, (_, res) => { res.send(css().map); }); } app.use(async (req, res, next) => { const resolution = await resolveStaticMap(staticMap, req.path, req.method); if (!resolution) return next() if (resolution.file) res.sendFile(pathUtils.resolve(resolution.file)); else if (resolution.serve) { if (resolution.serve === 'template') { res.contentType('.html'); res.send(template); } else { res.status(400); res.send('Invalid serve type'); } } else if (resolution.proxy) { proxy.web(req, res, { target: `http://${resolution.proxy.host}:${resolution.proxy.port}`, ws: true }); } }); await new Promise(resolve => { app.listen(port, host, async () => { const addr = `http://${host}:${port}`; console.log(`Listening on ${addr}`); if (open) await openPath__default['default'](addr); resolve(); }); }); } async function resolveStaticMap(staticMap, path, method) { let patternFound = null; for (let pattern in staticMap) { if (pattern.startsWith(method.toUpperCase() + ' ')) pattern = pattern.slice(method.length).trim(); if (!pattern.startsWith('/')) continue const value = staticMap[pattern]; const doesMatch = pathIsPattern(pattern, path); if (!doesMatch) continue if (!value.search) { patternFound = value; break } let searchFile = path.split('/').slice(value.search.removeSegments).join('/'); if (searchFile.startsWith('/')) searchFile = searchFile.slice(1); const searchPath = pathUtils.join(value.search.folder, searchFile); if (await pathType.isFile(searchPath)) { patternFound = { file: searchPath, }; break } } return patternFound } const defaultCoreOptions = { title: 'Svelte App', entryFile: 'App.svelte', additionalScripts: [], headers: ``, host: 'localhost', icon: null, open: false, port: 3000, realFavicon: null, staticMap: { '/**': { serve: 'template', }, }, template: null, }; const rollupPlugins = (entryFile, svelte) => { return [ entryFile.slice(-7) === '.svelte' && virtual__default['default']({ '@Entry': `import App from "${pathUtils__default['default'].relative( process.cwd(), entryFile )}"\nconst app = new App({ target: document.body })\nif (import.meta.hot) { import.meta.hot.dispose(() => { app.$destroy() }) import.meta.hot.accept() }\nexport default app`, }), svelte, nodeResolve__default['default']({ browser: true, dedupe: ['svelte'], }), commonjs__default['default'](), sucrase__default['default']({ transforms: ['typescript'] }), ] }; async function dev(coreOptions = {}, devOptions = {}) { const options = Object.assign({}, defaultCoreOptions, coreOptions); const hmrOptions = { optimistic: true, noPreserveState: false, compatNollup: true, // Bug in plugin. Option is called `nollup` in the docs }; let cssMap = ``; let cssCode = ``; const config = { input: '@Entry', output: { sourcemap: true, format: 'iife', name: 'app', file: generatedLocations.js, }, plugins: rollupPlugins( options.entryFile, svelte__default['default']({ // @ts-ignore dev: true, hot: devOptions.disableHMR ? false : hmrOptions, css: devOptions.disableHMR ? css => { cssCode = css.code + `\n\n//# sourceMappingUrl=${generatedLocations.cssMap}`; cssMap = JSON.stringify(css.map); } : false, preprocess: sveltePreprocess__default['default'](), }) ), watch: { clearScreen: false, }, }; await server({ staticMap: options.staticMap, host: options.host, open: options.open, port: options.port, template: makeTemplate(!!devOptions.disableHMR, options), css: devOptions.disableHMR ? () => ({ code: cssCode, map: cssMap }) : null, serveBuild(app) { return makeNollupMiddleware__default['default'](app, config, { hot: devOptions.disableHMR ? false : true, }) }, }); } async function prod(coreOptions = {}, prodOptions = {}) { const options = Object.assign({}, defaultCoreOptions, coreOptions); const { css, js, jsMap } = await getJSAndCSS(options, prodOptions); const { staticMap, host, template, open, port } = options; const serveBuild = (app) => (req, res, next) => { if (req.path === generatedLocations.js) { res.contentType('application/js'); res.setHeader('SourceMap', generatedLocations.jsMap); res.send(js); } else if (req.path === generatedLocations.jsMap) res.send(jsMap); else next(); }; await server({ staticMap, template: template || makeTemplate(true, options), css, host, open, port, serveBuild, }); } async function getJSAndCSS(options, prodOptions = {}) { let css = ``; let cssMap = ``; const bundle = await rollup.rollup({ input: options.entryFile, plugins: rollupPlugins( options.entryFile, svelte__default$1['default']({ css: cssObj => { css = cssObj.code; cssMap = JSON.stringify(cssObj.map); }, preprocess: autoProcess.sveltePreprocess(), }) ).concat(prodOptions.minify && rollupPluginTerser.terser()), }); const isSvelteEntry = options.entryFile.slice(-7) === '.svelte'; const { output } = await bundle.generate({ format: 'iife', sourcemap: true, name: isSvelteEntry ? 'SvelteRootComponent' : 'app' }); let main = output[0]; if (isSvelteEntry) main.code = `${main.code};;new SvelteRootComponent({ target: document.body })`; return { js: main.code, jsMap: main.map, css: () => ({ code: css, map: cssMap }), } } var version = "1.0.0"; commander.program.name('svelte-runner').version(version); commander.program.helpOption('-h, --help'); commander.program .option('-e, --entry-file <file>', 'Initial file. Code should be svelte, js, or ts', defaultCoreOptions.entryFile) .option('-i, --icon <imageFile>', 'Path to the favicon') .option('-t, --title <string>', 'Title of the app') .option('-o, --open', 'Open the app in default browser') .option('-p, --port <number>', 'Port to start the app on', String(defaultCoreOptions.port)) .option('--host <host>', 'Host to bind to', defaultCoreOptions.host) .option('--real-favicon <fileOrJSON>', 'Json options for real-favicon') .option('--headers <fileOrTags>', 'Extra tags to be inserted into the <head> of the template') .option('--template <stringOrFile>', 'Custom template (index.html)') .option('--static-map <fileOrJSON>', 'Static map. https://github.com/Vehmloewff/svelte-runner#static-meta'); let additionalScripts = []; let additionalWatchScripts = []; commander.program.option('--add-script <file>', "Path to an additional js file to be inserted into the app's head", value => { additionalScripts.push(value); }); let disableHMR = false; commander.program .command('dev') .option( '--add-watch-script <file>', 'Like --add-script, but the file is watched and the app is reloaded when it the script changes', value => { additionalWatchScripts.push(value); // All additional scripts to be watched must also be added to the template if (additionalScripts.indexOf(value) === -1) additionalScripts.push(value); } ) .option('--no-hmr', 'Disables hot module replacement', () => (disableHMR = true)) .description('Serves the app in a development environment') .action(async () => dev(await getCoreOptions(), await getDevOptions())); commander.program .command('prod') .option('--minify', 'Minify the generated code') .description('Serves up the app in a production environment') .action(async () => prod(await getCoreOptions(), await getProdOptions())); commander.program.parse(process.argv); async function getCoreOptions() { const res = { entryFile: commander.program.entryFile, icon: commander.program.icon, title: commander.program.title, open: commander.program.open, port: commander.program.port ? Number(commander.program.port) : undefined, host: commander.program.host, realFavicon: commander.program.realFavicon ? parseJSON(await readIfFilepath(commander.program.realFavicon), 'Could not load realFavicon meta:') : undefined, template: commander.program.realFavicon ? await readIfFilepath(commander.program.template) : undefined, staticMap: commander.program.staticMap ? parseJSON(await readIfFilepath(commander.program.staticMap), 'Could not load static map:') : undefined, additionalScripts: additionalScripts.length ? additionalScripts : undefined, }; Object.keys(res).forEach(key => { // @ts-ignore if (res[key] === undefined) delete res[key]; }); return res } async function getDevOptions() { return { additionalWatchScripts, disableHMR, } } async function getProdOptions() { return { minify: commander.program.minify, } } async function readIfFilepath(maybePath) { if (maybePath.startsWith('{') || maybePath.startsWith('[')) return maybePath return await new Promise(resolve => { fs.readFile(maybePath, 'utf-8', (err, data) => { if (err) resolve(maybePath); else resolve(data); }); }) } function parseJSON(json, errorMsg) { try { JSON.parse(json); } catch (e) { throw new Error(`${errorMsg}\n${e}\nJSON Dump: ${json}`) } } exports.getDevOptions = getDevOptions; exports.getProdOptions = getProdOptions;