Move svelte compilation to external rollup and add pre-compile-views cli

This commit is contained in:
Alice Gaudon 2021-03-26 10:56:32 +01:00
parent e5a9b9908d
commit 053313002c
7 changed files with 168 additions and 124 deletions

View File

@ -88,6 +88,7 @@ module.exports = {
},
ignorePatterns: [
'.eslintrc.js',
'rollup.config.js',
'jest.config.js',
'dist/**/*',
'config/**/*',

View File

@ -17,8 +17,9 @@
"clean": "node scripts/clean.js",
"prepare-sources": "node scripts/prepare-sources.js",
"compile": "yarn clean && tsc",
"build": "yarn prepare-sources && yarn compile && node scripts/dist.js",
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon -i build\" \"maildev\"",
"build": "yarn prepare-sources && yarn compile && node . pre-compile-views && node scripts/dist.js",
"build-production": "NODE_ENV=production yarn build",
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,ViewPreCompile,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon -i public -i build\" \"nodemon -i public -i build -- pre-compile-views --watch\" \"maildev\"",
"lint": "eslint .",
"release": "yarn build && yarn lint && yarn test && cd dist && yarn publish"
},

70
rollup.config.js Normal file
View File

@ -0,0 +1,70 @@
import path from "path";
import svelte from "rollup-plugin-svelte";
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess";
import cssOnlyRollupPlugin from "rollup-plugin-css-only";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import {terser} from "rollup-plugin-terser";
import livereloadRollupPlugin from "rollup-plugin-livereload";
const production = process.env.ENV === 'production';
const buildDir = process.env.BUILD_DIR;
const publicDir = process.env.PUBLIC_DIR;
const input = process.env.INPUT.split(':');
export default commandLineArgs => {
const type = commandLineArgs.input;
delete commandLineArgs.input;
return {
input: input,
output: {
sourcemap: true,
format: 'es',
name: 'bundle',
dir: path.join(publicDir, 'js'),
entryFileNames: (chunkInfo) => {
const name = chunkInfo.facadeModuleId ?
path.relative(buildDir, chunkInfo.facadeModuleId) :
chunkInfo.name;
return name + '.js';
},
},
plugins: [
svelte({
preprocess: sveltePreprocess({
typescript: {
tsconfigFile: 'tsconfig.views.json',
},
}),
compilerOptions: {
dev: !production,
hydratable: true,
},
}),
// Extract css into separate files
cssOnlyRollupPlugin({output: 'bundle.css'}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
// Live reload in dev
!production && !!commandLineArgs.watch && livereloadRollupPlugin('public'),
// Minify in production
production && terser(),
],
watch: {
clearScreen: false,
},
};
};

View File

@ -17,6 +17,7 @@ import CacheProvider from "./CacheProvider";
import RedisComponent from "./components/RedisComponent";
import Extendable from "./Extendable";
import {logger, loggingContextMiddleware} from "./Logger";
import FrontendToolsComponent from "./components/FrontendToolsComponent";
import TemplateError = lib.TemplateError;
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
@ -226,6 +227,7 @@ export default abstract class Application implements Extendable<ApplicationCompo
const flags = {
verbose: false,
fullHttpRequests: false,
watch: false,
};
let mainCommand: string | null = null;
const mainCommandArgs: string[] = [];
@ -237,7 +239,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
case '--full-http-requests':
flags.fullHttpRequests = true;
break;
case '--watch':
flags.watch = true;
break;
case 'migration':
case 'pre-compile-views':
if (mainCommand === null) mainCommand = args[i];
else throw new Error(`Only one main command can be used at once (${mainCommand},${args[i]})`);
break;
@ -257,6 +263,9 @@ export default abstract class Application implements Extendable<ApplicationCompo
await MysqlConnectionManager.migrationCommand(mainCommandArgs);
await this.stop();
break;
case 'pre-compile-views':
await this.as(FrontendToolsComponent).preCompileViews(flags.watch);
break;
default:
logger.fatal('Unimplemented main command', mainCommand);
break;

View File

@ -29,12 +29,6 @@ export default class FrontendToolsComponent extends ApplicationComponent {
logger.info('Asset cache disabled.');
}
// Pre-compile and watch in dev mode
if (config.get<boolean>('view.dev')) {
await this.viewEngine.preCompileAll();
await this.viewEngine.watch();
}
// Setup express view engine
app.engine(this.viewEngine.getExtension(), (path, options, callback) => {
this.viewEngine.render(path, options as Record<string, unknown>, callback)
@ -45,7 +39,7 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}
public async stop(): Promise<void> {
await this.viewEngine.end?.();
await this.viewEngine.stop();
}
public async handle(router: Router): Promise<void> {
@ -57,4 +51,8 @@ export default class FrontendToolsComponent extends ApplicationComponent {
next();
});
}
public async preCompileViews(watch: boolean): Promise<void> {
await this.viewEngine.preCompileAll(watch);
}
}

View File

@ -1,6 +1,5 @@
import crypto from "crypto";
import path from "path";
import {PreRenderedChunk, rollup, RollupBuild, RollupOptions, RollupWatcher, RollupWatchOptions, watch} from "rollup";
import config from "config";
import ViewEngine from "./ViewEngine";
import {logger} from "../Logger";
@ -10,12 +9,7 @@ import {compile, preprocess} from "svelte/compiler";
import {sveltePreprocess} from "svelte-preprocess/dist/autoProcess";
import requireFromString from "require-from-string";
import {CssResult} from "svelte/types/compiler/interfaces";
import svelte from "rollup-plugin-svelte";
import cssOnlyRollupPlugin from "rollup-plugin-css-only";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import {terser} from "rollup-plugin-terser";
import livereloadRollupPlugin from "rollup-plugin-livereload";
import * as child_process from "child_process";
const BACKEND_CODE_PREFIX = 'swaf.';
const COMPILED_SVELTE_EXTENSION = '.swafview';
@ -37,7 +31,7 @@ export default class SvelteViewEngine extends ViewEngine {
backendLines: string[],
}> = {};
private rollup?: RollupBuild | RollupWatcher;
private rollup?: child_process.ChildProcess;
public constructor(
buildDir: string,
@ -112,7 +106,8 @@ export default class SvelteViewEngine extends ViewEngine {
callback(null, output);
}
public async end(): Promise<void> {
public async stop(): Promise<void> {
await super.stop();
await this.stopRollup();
}
@ -130,7 +125,7 @@ export default class SvelteViewEngine extends ViewEngine {
const file = await this.resolveFileFromCanonicalName(canonicalName);
const intermediateFile = path.join(this.getBuildDir(), canonicalName);
logger.debug(canonicalName + ' > ', 'Pre-compiling', file, '->', intermediateFile);
logger.info(canonicalName + ' > ', 'Pre-compiling', file, '->', intermediateFile);
const source = await this.fileCache.get(file, config.get<boolean>('view.dev'));
const allBackendLines: string[] = [];
@ -157,7 +152,7 @@ export default class SvelteViewEngine extends ViewEngine {
await afs.writeFile(swafViewFile, finalCode);
if (alsoCompileDependents && Object.keys(this.dependencyCache).indexOf(canonicalName) >= 0) {
logger.debug(canonicalName + ' > ', 'Compiling dependents...');
logger.info(canonicalName + ' > ', 'Compiling dependents...');
for (const dependent of [...this.dependencyCache[canonicalName]]) {
await this.preCompile(dependent, true);
}
@ -271,8 +266,8 @@ export default class SvelteViewEngine extends ViewEngine {
return generated;
}
public async afterPreCompile(): Promise<void> {
await this.bundle(...Object.keys(this.backendCodeCache));
public async afterPreCompile(watch: boolean): Promise<void> {
await this.bundle(watch, ...Object.keys(this.backendCodeCache));
}
@ -284,13 +279,22 @@ export default class SvelteViewEngine extends ViewEngine {
delete this.backendCodeCache[this.toCanonicalName(file)];
}
public async onFileRemove(file: string): Promise<void> {
const canonicalName = this.toCanonicalName(file);
delete this.backendCodeCache[canonicalName];
delete this.dependencyCache[canonicalName];
Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName));
await this.stopRollup();
await this.afterPreCompile(true);
}
private async compileSsr(canonicalName: string, file: string, code: string): Promise<{
head: string,
css: CssResult,
html: string,
}> {
// Svelte preprocess
logger.debug(canonicalName + ' > ', 'Preprocessing svelte', file);
logger.info(canonicalName + ' > ', 'Preprocessing svelte', file);
const preprocessed = await preprocess(
code,
sveltePreprocess({
@ -304,7 +308,7 @@ export default class SvelteViewEngine extends ViewEngine {
);
// Svelte compile
logger.debug(canonicalName + ' > ', 'Compiling svelte ssr', file);
logger.info(canonicalName + ' > ', 'Compiling svelte ssr', file);
const svelteSsr = compile(preprocessed.code, {
dev: config.get<boolean>('view.dev'),
generate: 'ssr',
@ -324,103 +328,43 @@ export default class SvelteViewEngine extends ViewEngine {
});
}
/**
* TODO: load rollup config from external source
*/
private async bundle(...canonicalViewNames: string[]): Promise<void> {
logger.debug('Bundling');
const production = !config.get<boolean>('view.dev');
const options: RollupOptions | RollupWatchOptions = {
input: canonicalViewNames.map(name => path.join(this.getBuildDir(), name)),
output: {
sourcemap: true,
format: 'es',
name: 'bundle',
dir: path.join(this.getPublicDir(), 'js'),
entryFileNames: (chunkInfo: PreRenderedChunk): string => {
const name = chunkInfo.facadeModuleId ?
path.relative(this.getBuildDir(), chunkInfo.facadeModuleId) :
chunkInfo.name;
return name + '.js';
},
},
plugins: [
svelte({
preprocess: sveltePreprocess({
typescript: {
tsconfigFile: 'tsconfig.views.json',
},
}),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
hydratable: true,
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
cssOnlyRollupPlugin({output: 'bundle.css'}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
],
watch: {
buildDelay: 1000,
clearScreen: false,
},
};
if (production) {
// If we're building for production (npm run build
// instead of npm run dev), minify
options.plugins?.push(terser());
} else {
// Watch the `public` directory and refresh the
// browser on changes when not in production
const plugin = livereloadRollupPlugin('public');
options.plugins?.push(plugin);
}
private async bundle(watch: boolean, ...canonicalViewNames: string[]): Promise<void> {
logger.info('Bundling...');
// Prepare output dir
for (const name of canonicalViewNames) {
await afs.mkdir(path.dirname(path.join(this.getPublicDir(), 'js', name)), {recursive: true});
}
if (production) {
if (!this.rollup) {
this.rollup = await rollup(options);
const production = !config.get<boolean>('view.dev');
const input = canonicalViewNames.map(name => path.join(this.getBuildDir(), name));
await this.rollup.write({
format: 'es',
dir: path.join(this.getPublicDir(), 'js'),
});
}
} else {
if (!this.rollup) {
this.rollup = watch(options);
this.rollup.on('event', (event) => {
if (event.code === 'ERROR' || event.code === 'BUNDLE_END') {
event.result?.close().catch(err => logger.error(err));
logger.debug('Bundled from watch');
}
});
}
if (!this.rollup) {
const args = [
'rollup',
'-c', 'rollup.config.js',
'--environment', `ENV:${production ? 'production' : 'dev'},BUILD_DIR:${this.getBuildDir()},PUBLIC_DIR:${this.getPublicDir()},INPUT:${input.join(':')}`,
];
if (watch) args.push('--watch');
this.rollup = child_process.spawn('yarn', args, {stdio: [process.stdin, process.stdout, process.stderr]});
logger.info('Rollup started');
this.rollup.once('exit', () => {
logger.info('Rollup stopped');
this.rollup = undefined;
});
}
}
private async stopRollup(): Promise<void> {
if (this.rollup) {
logger.debug('Stopping rollup...');
await this.rollup.close();
this.rollup = undefined;
logger.info(`Stopping rollup (${this.rollup.pid})...`);
await new Promise<void>((resolve, reject) => {
if (!this.rollup) return resolve();
this.rollup.once('exit', () => {
resolve();
});
if (!this.rollup.kill("SIGTERM")) reject('Could not stop rollup.');
});
}
}
}

View File

@ -1,11 +1,12 @@
import path from "path";
import fs from "fs";
import chokidar from "chokidar";
import chokidar, {FSWatcher} from "chokidar";
import {logger} from "../Logger";
import {afs, readdirRecursively} from "../Utils";
export default abstract class ViewEngine {
protected readonly viewPaths: string[];
private watcher?: FSWatcher;
/**
* @param buildDir A temporary directory that will contain any non-final or final non-public asset.
@ -41,8 +42,6 @@ export default abstract class ViewEngine {
callback: (err: Error | null, output?: string) => void,
): Promise<void>;
public end?(): Promise<void>;
public getViewPaths(): string[] {
return this.viewPaths;
}
@ -61,9 +60,9 @@ export default abstract class ViewEngine {
public preCompile?(canonicalName: string, alsoCompileDependents: boolean): Promise<void>;
public afterPreCompile?(): Promise<void>;
public afterPreCompile?(watch: boolean): Promise<void>;
public async preCompileAll(): Promise<void> {
public async preCompileAll(watch: boolean): Promise<void> {
if (this.preCompile) {
logger.info(`Pre-compiling ${this.getExtension()} views...`);
@ -83,38 +82,60 @@ export default abstract class ViewEngine {
}
}
await this.afterPreCompile?.();
await this.afterPreCompile?.(watch);
if (watch) {
await this.watch();
}
}
public onNewFile?(file: string): Promise<void>;
public onFileChange?(file: string): Promise<void>;
public async watch(): Promise<void> {
const watcher = chokidar.watch(this.devWatchedViewDir, {persistent: true});
watcher.on('ready', () => {
logger.debug(`Watching ${this.getExtension()} assets for changes`);
public onFileRemove?(file: string): Promise<void>;
watcher.on('add', (file) => {
public async watch(): Promise<void> {
this.watcher = chokidar.watch(this.devWatchedViewDir, {persistent: true});
this.watcher.on('ready', () => {
if (!this.watcher) return;
logger.info(`Watching ${this.getExtension()} assets for changes`);
this.watcher.on('add', (file) => {
if (file.endsWith('.' + this.getExtension())) {
(this.onNewFile ? this.onNewFile(file) : Promise.resolve())
.then(() => this.preCompile?.(this.toCanonicalName(file), true))
.then(() => this.afterPreCompile?.())
.then(() => this.afterPreCompile?.(true))
.catch(err => logger.error(err));
}
});
watcher.on('change', (file) => {
this.watcher.on('change', (file) => {
if (file.endsWith('.' + this.getExtension())) {
(this.onFileChange ? this.onFileChange(file) : Promise.resolve())
.then(() => this.preCompile?.(this.toCanonicalName(file), true))
.then(() => this.afterPreCompile?.())
.then(() => this.afterPreCompile?.(true))
.catch(err => logger.error(err));
}
});
this.watcher.on('unlink', (file) => {
if (file.endsWith('.' + this.getExtension())) {
(this.onFileRemove ? this.onFileRemove(file) : Promise.resolve())
.catch(err => logger.error(err));
}
});
});
}
public async stop(): Promise<void> {
if (this.watcher) {
await this.watcher.close();
this.watcher = undefined;
}
}
protected toCanonicalName(file: string): string {
const resolvedFilePath = path.resolve(file);