svelte-runner
Version:
Run svelte components. Zero configuration necessary.
503 lines (414 loc) • 14.6 kB
JavaScript
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;
;