diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..4910a0e
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,135 @@
+module.exports = {
+ root: true,
+ parser: '@typescript-eslint/parser',
+ plugins: [
+ 'svelte3',
+ '@typescript-eslint',
+ 'import',
+ 'simple-import-sort',
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: [
+ './tsconfig.test.json',
+ './src/tsconfig.json',
+ './src/common/tsconfig.json',
+ './src/assets/ts/tsconfig.eslint.json',
+ './src/assets/views/tsconfig.json',
+ ]
+ },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ ],
+ rules: {
+ indent: [
+ 'error',
+ 4,
+ {
+ SwitchCase: 1
+ }
+ ],
+ 'no-trailing-spaces': 'error',
+ 'max-len': [
+ 'error',
+ {
+ code: 120,
+ ignoreTemplateLiterals: true,
+ ignoreRegExpLiterals: true
+ }
+ ],
+ semi: 'off',
+ '@typescript-eslint/semi': [
+ 'error'
+ ],
+ 'no-extra-semi': 'error',
+ 'eol-last': 'error',
+ 'comma-dangle': 'off',
+ 'simple-import-sort/imports': 'error',
+ 'no-extra-parens': 'off',
+ 'no-nested-ternary': 'error',
+ 'no-return-await': 'off',
+ 'no-useless-return': 'error',
+ 'no-useless-constructor': 'off',
+ 'import/extensions': ['error', 'ignorePackages'],
+ '@typescript-eslint/comma-dangle': [
+ 'error',
+ {
+ arrays: 'always-multiline',
+ objects: 'always-multiline',
+ imports: 'always-multiline',
+ exports: 'always-multiline',
+ functions: 'always-multiline',
+ enums: 'always-multiline',
+ generics: 'always-multiline',
+ tuples: 'always-multiline'
+ }
+ ],
+ '@typescript-eslint/no-extra-parens': [
+ 'error'
+ ],
+ '@typescript-eslint/no-inferrable-types': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'error',
+ '@typescript-eslint/no-unnecessary-condition': 'error',
+ '@typescript-eslint/no-unused-vars': [
+ 'error',
+ {
+ argsIgnorePattern: '^_'
+ }
+ ],
+ '@typescript-eslint/no-non-null-assertion': 'error',
+ '@typescript-eslint/no-useless-constructor': [
+ 'error'
+ ],
+ '@typescript-eslint/return-await': [
+ 'error',
+ 'always'
+ ],
+ '@typescript-eslint/explicit-member-accessibility': [
+ 'error',
+ {
+ accessibility: 'explicit'
+ }
+ ],
+ '@typescript-eslint/no-floating-promises': 'error',
+ },
+ ignorePatterns: [
+ '.eslintrc.js',
+ 'rollup.config.js',
+ 'jest.config.js',
+ 'dist/**/*',
+ 'config/**/*',
+ 'intermediates/**/*',
+ 'public/**/*',
+ 'scripts/**/*',
+ 'src/frontend/register_svelte/register_svelte.js',
+ ],
+ overrides: [
+ {
+ files: [
+ 'test/**/*'
+ ],
+ rules: {
+ 'max-len': [
+ 'error',
+ {
+ code: 120,
+ ignoreTemplateLiterals: true,
+ ignoreRegExpLiterals: true,
+ ignoreStrings: true
+ }
+ ]
+ }
+ },
+ {
+ files: ['*.svelte'],
+ processor: 'svelte3/svelte3'
+ }
+ ],
+ settings: {
+ 'svelte3/typescript': require('typescript'),
+ 'svelte3/ignore-styles': function (attributes) {
+ return !!(attributes['lang'] && attributes['lang'] !== 'css');
+ }
+ },
+}
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index a506e1e..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,115 +0,0 @@
-{
- "root": true,
- "parser": "@typescript-eslint/parser",
- "plugins": [
- "@typescript-eslint"
- ],
- "parserOptions": {
- "project": [
- "./tsconfig.json",
- "./tsconfig.test.json",
- "./tsconfig.frontend.json"
- ]
- },
- "extends": [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended"
- ],
- "rules": {
- "indent": [
- "error",
- 4,
- {
- "SwitchCase": 1
- }
- ],
- "no-trailing-spaces": "error",
- "max-len": [
- "error",
- {
- "code": 120,
- "ignoreStrings": true,
- "ignoreTemplateLiterals": true,
- "ignoreRegExpLiterals": true
- }
- ],
- "semi": "off",
- "@typescript-eslint/semi": [
- "error"
- ],
- "no-extra-semi": "error",
- "eol-last": "error",
- "comma-dangle": "off",
- "@typescript-eslint/comma-dangle": [
- "error",
- {
- "arrays": "always-multiline",
- "objects": "always-multiline",
- "imports": "always-multiline",
- "exports": "always-multiline",
- "functions": "always-multiline",
- "enums": "always-multiline",
- "generics": "always-multiline",
- "tuples": "always-multiline"
- }
- ],
- "no-extra-parens": "off",
- "@typescript-eslint/no-extra-parens": [
- "error"
- ],
- "no-nested-ternary": "error",
- "@typescript-eslint/no-inferrable-types": "off",
- "@typescript-eslint/explicit-module-boundary-types": "error",
- "@typescript-eslint/no-unnecessary-condition": "error",
- "@typescript-eslint/no-unused-vars": [
- "error",
- {
- "argsIgnorePattern": "^_"
- }
- ],
- "@typescript-eslint/no-non-null-assertion": "error",
- "no-useless-return": "error",
- "no-useless-constructor": "off",
- "@typescript-eslint/no-useless-constructor": [
- "error"
- ],
- "no-return-await": "off",
- "@typescript-eslint/return-await": [
- "error",
- "always"
- ],
- "@typescript-eslint/explicit-member-accessibility": [
- "error",
- {
- "accessibility": "explicit"
- }
- ],
- "@typescript-eslint/no-floating-promises": "error"
- },
- "ignorePatterns": [
- "jest.config.js",
- "scripts/**/*",
- "webpack.config.js",
- "dist/**/*",
- "public/**/*",
- "config/**/*"
- ],
- "overrides": [
- {
- "files": [
- "test/**/*"
- ],
- "rules": {
- "max-len": [
- "error",
- {
- "code": 120,
- "ignoreTemplateLiterals": true,
- "ignoreRegExpLiterals": true,
- "ignoreStrings": true
- }
- ]
- }
- }
- ]
-}
diff --git a/.gitignore b/.gitignore
index 86d9475..896c53e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ storage/uploads
config/local.*
src/package.json
+
+intermediates/
+dist/
\ No newline at end of file
diff --git a/assets/sass/_fonts.scss b/assets/sass/_fonts.scss
deleted file mode 100644
index 6c26b55..0000000
--- a/assets/sass/_fonts.scss
+++ /dev/null
@@ -1,81 +0,0 @@
-/* vietnamese */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5iU1EQVg.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5jU1EQVg.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 300;
- font-display: swap;
- src: local('Nunito Sans Light'), local('NunitoSans-Light'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8WAc5tU1E.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cceyI9tScg.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8ccezI9tScg.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: local('Nunito Sans Regular'), local('NunitoSans-Regular'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe0qMImSLYBIv1o4X1M8cce9I9s.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5iU1EQVg.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5jU1EQVg.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Nunito Sans';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: local('Nunito Sans Bold'), local('NunitoSans-Bold'), url(https://fonts.gstatic.com/s/nunitosans/v5/pe03MImSLYBIv1o4X1M8cc8GBs5tU1E.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
diff --git a/assets/sass/error.scss b/assets/sass/error.scss
deleted file mode 100644
index d36ad0b..0000000
--- a/assets/sass/error.scss
+++ /dev/null
@@ -1,90 +0,0 @@
-@import "layout";
-
-header, footer {
- margin: 0;
- padding: 0;
- height: 0;
-}
-
-main {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-
- .messages {
- margin-bottom: 32px;
- }
-
- .error-code {
- font-size: 36px;
- }
-
- .error-message {
- font-size: 32px;
- }
-
- .error-instructions {
- margin-top: 32px;
- font-size: 20px;
- }
-
- nav {
- margin-top: 32px;
- }
-
- &::before {
- content: "Oops";
- position: absolute;
- z-index: -1;
-
- font-size: #{'min(50vh, 40vw)'};
- opacity: 0.025;
- }
-}
-
-.contact {
- text-align: center;
- padding: 8px;
-}
-
-.logo {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- margin-top: 24px;
- text-align: center;
-
- a {
- position: relative;
- padding: 16px;
-
- color: $defaultTextColor;
-
- &:hover {
- color: #fff;
-
- &::before {
- opacity: 0.2;
- }
- }
-
- &::before {
- content: "";
- position: absolute;
- width: 100%;
- height: 100%;
- top: 0;
- left: 0;
-
- background-image: url(../img/logo.svg);
- background-repeat: no-repeat;
- background-position: center;
- background-size: 64px;
-
- opacity: 0.075;
- filter: contrast(0);
- }
- }
-}
\ No newline at end of file
diff --git a/assets/sass/responsivity_tools.scss b/assets/sass/responsivity_tools.scss
deleted file mode 100644
index 0e3d232..0000000
--- a/assets/sass/responsivity_tools.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-@import "vars";
-
-@mixin container {
- width: 100%;
- padding: 0 8px;
-
- @media (min-width: $mobileThreshold) {
- margin: 0 auto;
- padding: 0 16px;
- }
-
- @media (min-width: $desktopThreshold) {
- width: $desktopThreshold;
- }
-}
-
-.container {
- @include container;
-}
diff --git a/assets/ts/PersistentWebSocket.ts b/assets/ts/PersistentWebSocket.ts
deleted file mode 100644
index 98e474f..0000000
--- a/assets/ts/PersistentWebSocket.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-export default class PersistentWebsocket {
- private webSocket?: WebSocket;
-
- public constructor(
- protected readonly url: string,
- private readonly handler: MessageHandler,
- protected readonly reconnectOnClose: boolean = true,
- ) {
- }
-
- public run(): void {
- const _webSocket = this.webSocket = new WebSocket(this.url);
- this.webSocket.addEventListener('open', () => {
- console.debug('Websocket connected');
- });
- this.webSocket.addEventListener('message', (e) => {
- this.handler(_webSocket, e);
- });
- this.webSocket.addEventListener('error', (e) => {
- console.error('Websocket error', e);
- });
- this.webSocket.addEventListener('close', (e) => {
- this.webSocket = undefined;
- console.debug('Websocket closed', e.code, e.reason);
-
- if (this.reconnectOnClose) {
- setTimeout(() => this.run(), 1000);
- }
- });
- }
-
- public send(data: string): void {
- if (!this.webSocket) throw new Error('WebSocket not connected');
-
- this.webSocket.send(data);
- }
-}
-
-export type MessageHandler = (webSocket: WebSocket, e: MessageEvent) => void;
diff --git a/assets/ts/app.ts b/assets/ts/app.ts
deleted file mode 100644
index 8594552..0000000
--- a/assets/ts/app.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import './external_links';
-import './message_icons';
-import './forms';
-import './copyable_text';
-import './tooltips-and-dropdowns';
-import './main_menu';
-import './font-awesome';
-
-// css
-import '../sass/app.scss';
diff --git a/assets/ts/external_links.ts b/assets/ts/external_links.ts
deleted file mode 100644
index bc9ba26..0000000
--- a/assets/ts/external_links.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import feather from "feather-icons";
-
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('a[target="_blank"]').forEach(el => {
- if (!el.classList.contains('no-icon')) {
- el.innerHTML += ``;
- }
- });
-
- feather.replace();
-});
diff --git a/assets/ts/font-awesome.ts b/assets/ts/font-awesome.ts
deleted file mode 100644
index 5ce65ff..0000000
--- a/assets/ts/font-awesome.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import '../../node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss';
-import '../../node_modules/@fortawesome/fontawesome-free/scss/regular.scss';
-import '../../node_modules/@fortawesome/fontawesome-free/scss/solid.scss';
-import '../../node_modules/@fortawesome/fontawesome-free/scss/brands.scss';
diff --git a/assets/ts/forms.ts b/assets/ts/forms.ts
deleted file mode 100644
index ece465d..0000000
--- a/assets/ts/forms.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * For labels to update their state (css selectors based on the value attribute)
- */
-import {ValidationError} from "swaf/db/Validator";
-
-export function updateInputs(): void {
- document.querySelectorAll('input, textarea').forEach(el => {
- if (!el.dataset.inputSetup) {
- el.dataset.inputSetup = 'true';
- if (el.type !== 'checkbox') {
- el.setAttribute('value', el.value);
- el.addEventListener('change', () => {
- el.setAttribute('value', el.value);
- });
- }
- }
- });
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- updateInputs();
-});
-
-export function applyFormMessages(
- formElement: HTMLFormElement,
- messages: { [p: string]: ValidationError },
-): void {
- for (const fieldName of Object.keys(messages)) {
- const field = formElement.querySelector('#field-' + fieldName);
- if (!field) continue;
-
- let parent = field.parentElement;
- while (parent && !parent.classList.contains('form-field')) parent = parent.parentElement;
-
- let err = field.querySelector('.error');
- if (!err) {
- err = document.createElement('div');
- err.classList.add('error');
- parent?.insertBefore(err, parent.querySelector('.hint') || parent);
- }
- err.innerHTML = ` ${messages[fieldName].message}`;
- }
-}
diff --git a/assets/ts/main_menu.ts b/assets/ts/main_menu.ts
deleted file mode 100644
index 4e19e40..0000000
--- a/assets/ts/main_menu.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- const menuButton = document.getElementById('menu-button');
- const mainMenu = document.getElementById('main-menu');
-
- if (menuButton) {
- menuButton.addEventListener('click', (e) => {
- e.stopPropagation();
- mainMenu?.classList.toggle('open');
- });
- }
-
- if (mainMenu) {
- mainMenu.addEventListener('click', (e) => {
- e.stopPropagation();
- });
-
- document.addEventListener('click', () => {
- mainMenu.classList.remove('open');
- });
- }
-});
diff --git a/assets/ts/message_icons.ts b/assets/ts/message_icons.ts
deleted file mode 100644
index b737de3..0000000
--- a/assets/ts/message_icons.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import feather from "feather-icons";
-
-document.addEventListener('DOMContentLoaded', () => {
- const messageTypeToIcon: { [p: string]: string } = {
- info: 'info',
- success: 'check',
- warning: 'alert-triangle',
- error: 'x-circle',
- question: 'help-circle',
- };
-
- document.querySelectorAll('.message').forEach(el => {
- const icon = el.querySelector('.icon');
- const type = el.dataset['type'];
- if (!icon || !type) return;
- if (!messageTypeToIcon[type]) throw new Error(`No icon for type ${type}`);
-
- const svgContainer = document.createElement('div');
- svgContainer.innerHTML = feather.icons[messageTypeToIcon[type]].toSvg();
-
- if (svgContainer.firstChild) el.insertBefore(svgContainer.firstChild, icon);
- icon.remove();
- });
-
- feather.replace();
-});
diff --git a/assets/ts/tooltips-and-dropdowns.ts b/assets/ts/tooltips-and-dropdowns.ts
deleted file mode 100644
index 069b405..0000000
--- a/assets/ts/tooltips-and-dropdowns.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export function updateTooltips(): void {
- console.debug('Updating tooltips');
- const elements = document.querySelectorAll('.tip, .dropdown');
-
- // Calculate max potential displacement
- let max = 0;
- elements.forEach(el => {
- const box = el.getBoundingClientRect();
- if (max < box.height) max = box.height;
- });
-
- // Prevent displacement
- elements.forEach(el => {
- if (!el.dataset.tooltipSetup) {
- el.dataset.tooltipSetup = 'true';
- const box = el.getBoundingClientRect();
- if (box.bottom >= document.body.clientHeight - (max + 32)) {
- el.classList.add('top');
- }
- }
- });
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- window.addEventListener('popstate', () => {
- updateTooltips();
- });
- window.requestAnimationFrame(() => {
- updateTooltips();
- });
-});
diff --git a/package.json b/package.json
index 6e31048..6054174 100644
--- a/package.json
+++ b/package.json
@@ -10,58 +10,52 @@
"test": "jest --verbose --runInBand",
"clean": "node scripts/clean.js",
"prepare-sources": "node scripts/prepare-sources.js",
- "compile": "yarn clean && tsc",
- "build": "yarn prepare-sources && yarn compile && webpack --mode production",
- "dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
- "start": "yarn build && node",
- "lint": "eslint ."
+ "compile": "yarn clean && yarn prepare-sources && tsc --build",
+ "build": "yarn compile && node . pre-compile-views && node scripts/dist.js",
+ "build-production": "NODE_ENV=production yarn build",
+ "dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"",
+ "lint": "eslint .",
+ "start": "yarn build-production && node ."
},
"devDependencies": {
- "@babel/core": "^7.9.0",
- "@babel/preset-env": "^7.9.5",
- "@fortawesome/fontawesome-free": "^5.14.0",
- "@types/config": "^0.0.38",
+ "@tsconfig/svelte": "^3.0.0",
+ "@types/config": "^0.0.40",
"@types/express": "^4.17.6",
"@types/express-session": "^1.17.0",
"@types/feather-icons": "^4.7.0",
- "@types/formidable": "^1.0.31",
- "@types/jest": "^26.0.4",
+ "@types/formidable": "^2.0.0",
+ "@types/jest": "^27.0.3",
"@types/mysql": "^2.15.15",
- "@types/node": "^14.6.3",
+ "@types/node": "^17.0.4",
"@types/nodemailer": "^6.4.0",
"@types/nunjucks": "^3.1.3",
- "@types/ws": "^7.2.6",
- "@typescript-eslint/eslint-plugin": "^4.3.0",
- "@typescript-eslint/parser": "^4.3.0",
- "babel-loader": "^8.1.0",
+ "@types/ws": "^8.2.0",
+ "@typescript-eslint/eslint-plugin": "^5.4.0",
+ "@typescript-eslint/parser": "^5.4.0",
"concurrently": "^6.0.0",
- "css-loader": "^5.0.0",
- "eslint": "^7.10.0",
+ "eslint": "^8.3.0",
+ "eslint-plugin-import": "^2.25.3",
+ "eslint-plugin-simple-import-sort": "^7.0.0",
+ "eslint-plugin-svelte3": "^3.2.1",
"feather-icons": "^4.28.0",
- "file-loader": "^6.0.0",
- "imagemin": "^7.0.0",
"imagemin-gifsicle": "^7.0.0",
- "imagemin-mozjpeg": "^9.0.0",
- "imagemin-pngquant": "^9.0.0",
- "imagemin-svgo": "^9.0.0",
- "img-loader": "^3.0.1",
- "jest": "^26.1.0",
+ "imagemin-mozjpeg": "^10.0.0",
+ "imagemin-pngquant": "^9.0.2",
+ "imagemin-svgo": "^10.0.0",
+ "imagemin-webp": "^7.0.0",
+ "jest": "^27.0.4",
"maildev": "^1.1.0",
- "mini-css-extract-plugin": "^1.2.1",
- "node-sass": "^5.0.0",
"nodemon": "^2.0.3",
- "sass-loader": "^11.0.1",
- "terser-webpack-plugin": "^5.0.3",
- "ts-jest": "^26.1.1",
- "ts-loader": "^9.1.0",
- "typescript": "^4.0.2",
- "webpack": "^5.3.2",
- "webpack-cli": "^4.1.0"
+ "sass": "^1.32.12",
+ "svelte": "^3.44.2",
+ "svgo": "^2.3.0",
+ "ts-jest": "^27.0.3",
+ "typescript": "^4.0.2"
},
"dependencies": {
"config": "^3.3.1",
"express": "^4.17.1",
"formidable": "^1.2.2",
- "swaf": "^0.23.0"
+ "swaf": "^0.24.7"
}
}
diff --git a/scripts/_functions.js b/scripts/_functions.js
new file mode 100644
index 0000000..0fea1c4
--- /dev/null
+++ b/scripts/_functions.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+const path = require('path');
+
+function copyRecursively(file, destination) {
+ const target = path.join(destination, path.basename(file));
+ if (fs.statSync(file).isDirectory()) {
+ console.log('mkdir', target);
+
+ fs.mkdirSync(target, {recursive: true});
+ fs.readdirSync(file).forEach(f => {
+ copyRecursively(path.join(file, f), target);
+ });
+ } else {
+ console.log('> cp ', target);
+
+ fs.copyFileSync(file, target);
+ }
+}
+
+module.exports = {
+ copyRecursively,
+};
\ No newline at end of file
diff --git a/scripts/clean.js b/scripts/clean.js
index d5ef093..32ebb24 100644
--- a/scripts/clean.js
+++ b/scripts/clean.js
@@ -1,7 +1,9 @@
const fs = require('fs');
[
+ 'intermediates',
'dist',
+ 'public',
].forEach(file => {
if (fs.existsSync(file)) {
console.log('Cleaning', file, '...');
diff --git a/scripts/dist.js b/scripts/dist.js
new file mode 100644
index 0000000..a0ead2a
--- /dev/null
+++ b/scripts/dist.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+const path = require('path');
+const {copyRecursively} = require('./_functions.js');
+
+
+[
+ 'yarn.lock',
+ 'README.md',
+ 'config/',
+].forEach(file => {
+ copyRecursively(file, 'dist');
+});
+
+fs.mkdirSync('dist/types', {recursive: true});
+
+fs.readdirSync('src/types').forEach(file => {
+ copyRecursively(path.join('src/types', file), 'dist/types');
+});
+
+fs.readdirSync('src/assets').forEach(file => {
+ copyRecursively(path.join('src/assets', file), 'dist/assets');
+});
diff --git a/scripts/prepare-sources.js b/scripts/prepare-sources.js
index a78c2f3..c8ee7b9 100644
--- a/scripts/prepare-sources.js
+++ b/scripts/prepare-sources.js
@@ -1,4 +1,28 @@
const fs = require('fs');
const path = require('path');
-fs.copyFileSync('package.json', path.join('src', 'package.json'));
+// These folders must exist for nodemon not to loop indefinitely.
+[
+ 'public',
+ 'dist',
+ 'intermediates',
+ 'intermediates/assets',
+].forEach(dir => {
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir);
+});
+
+// Symlink to build/common
+const commonLocalSymlink = path.resolve('intermediates/common-local');
+if (!fs.existsSync(commonLocalSymlink)) {
+ const target = path.resolve('dist/common-local');
+ fs.symlinkSync(target, commonLocalSymlink);
+}
+
+const commonSymlink = path.resolve('intermediates/common');
+if (!fs.existsSync(commonSymlink)) {
+ const target = path.resolve('node_modules/swaf/common');
+ fs.symlinkSync(target, commonSymlink);
+}
+
+// Copy package.json
+fs.copyFileSync('package.json', 'dist/package.json');
diff --git a/src/App.ts b/src/App.ts
index 931fbba..ee26b8b 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -1,57 +1,39 @@
import Application from "swaf/Application";
-import Migration, {MigrationType} from "swaf/db/Migration";
-import CreateMigrationsTable from "swaf/migrations/CreateMigrationsTable";
+import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
+import CsrfProtectionComponent from "swaf/components/CsrfProtectionComponent";
import ExpressAppComponent from "swaf/components/ExpressAppComponent";
-import NunjucksComponent from "swaf/components/NunjucksComponent";
-import MysqlComponent from "swaf/components/MysqlComponent";
+import FormHelperComponent from "swaf/components/FormHelperComponent";
+import FrontendToolsComponent from "swaf/components/FrontendToolsComponent";
import LogRequestsComponent from "swaf/components/LogRequestsComponent";
+import MailComponent from "swaf/components/MailComponent";
+import MaintenanceComponent from "swaf/components/MaintenanceComponent";
+import MysqlComponent from "swaf/components/MysqlComponent";
+import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
import RedisComponent from "swaf/components/RedisComponent";
import ServeStaticDirectoryComponent from "swaf/components/ServeStaticDirectoryComponent";
-import MaintenanceComponent from "swaf/components/MaintenanceComponent";
-import MailComponent from "swaf/components/MailComponent";
import SessionComponent from "swaf/components/SessionComponent";
-import FormHelperComponent from "swaf/components/FormHelperComponent";
-import CsrfProtectionComponent from "swaf/components/CsrfProtectionComponent";
import WebSocketServerComponent from "swaf/components/WebSocketServerComponent";
-import AboutController from "./controllers/AboutController";
-import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
-import MagicLinkWebSocketListener from "swaf/auth/magic_link/MagicLinkWebSocketListener";
-import FileController from "./controllers/FileController";
-import CreateAuthTokensTable from "./migrations/CreateAuthTokensTable";
-import AuthComponent from "swaf/auth/AuthComponent";
-import CreateFilesTable from "./migrations/CreateFilesTable";
-import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
-import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
-import AuthTokenController from "./controllers/AuthTokenController";
-import URLRedirectController from "./controllers/URLRedirectController";
-import LinkController from "./controllers/LinkController";
-import BackendController from "swaf/helpers/BackendController";
-import DummyMigration from "swaf/migrations/DummyMigration";
+import Migration, {MigrationType} from "swaf/db/Migration";
+import AssetCompiler from "swaf/frontend/AssetCompiler";
+import CopyAssetPreCompiler from "swaf/frontend/CopyAssetPreCompiler";
+import MailViewEngine from "swaf/frontend/MailViewEngine";
+import NunjucksViewEngine from "swaf/frontend/NunjucksViewEngine";
+import ScssAssetPreCompiler from "swaf/frontend/ScssAssetPreCompiler";
+import SvelteViewEngine from "swaf/frontend/SvelteViewEngine";
+import TypeScriptPreCompiler from "swaf/frontend/TypeScriptPreCompiler";
+import CreateMigrationsTable from "swaf/migrations/CreateMigrationsTable";
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
-import CreateUsersAndUserEmailsTableMigration from "swaf/auth/migrations/CreateUsersAndUserEmailsTableMigration";
-import CreateMagicLinksTableMigration from "swaf/auth/magic_link/CreateMagicLinksTableMigration";
-import AddApprovedFieldToUsersTableMigration from "swaf/auth/migrations/AddApprovedFieldToUsersTableMigration";
-import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
-import MagicLinkAuthMethod from "swaf/auth/magic_link/MagicLinkAuthMethod";
-import {MAGIC_LINK_MAIL} from "swaf/Mails";
-import PasswordAuthMethod from "swaf/auth/password/PasswordAuthMethod";
-import MailController from "swaf/mail/MailController";
-import AccountController from "swaf/auth/AccountController";
-import AuthController from "swaf/auth/AuthController";
-import MagicLinkController from "swaf/auth/magic_link/MagicLinkController";
-import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
-import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
-import AddPasswordToUsersMigration from "swaf/auth/password/AddPasswordToUsersMigration";
-import DeleteOldFilesJobComponent from "./DeleteOldFilesJobComponent";
-import packageJson = require('./package.json');
-import ReplaceTtlWithExpiresAtFilesTable from "./migrations/ReplaceTtlWithExpiresAtFilesTable";
+import DummyMigration from "swaf/migrations/DummyMigration";
+
+import HomeController from "./controllers/HomeController.js";
export default class App extends Application {
public constructor(
+ version: string,
private readonly addr: string,
private readonly port: number,
) {
- super(packageJson.version);
+ super(version);
}
protected getMigrations(): MigrationType[] {
@@ -91,18 +73,27 @@ export default class App extends Application {
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
// Dynamic views and routes
- this.use(new NunjucksComponent());
+ const intermediateDirectory = 'intermediates/assets';
+ const assetCompiler = new AssetCompiler(intermediateDirectory, 'public');
+ const additionalViewPaths = ['test/assets'];
+ this.use(new FrontendToolsComponent(
+ assetCompiler,
+ new CopyAssetPreCompiler(intermediateDirectory, '', 'json', additionalViewPaths, false),
+ new ScssAssetPreCompiler(intermediateDirectory, assetCompiler.targetDir, 'scss', additionalViewPaths),
+ new CopyAssetPreCompiler(intermediateDirectory, 'img', 'svg', additionalViewPaths, true),
+ new TypeScriptPreCompiler(intermediateDirectory, additionalViewPaths),
+ new SvelteViewEngine(intermediateDirectory, ...additionalViewPaths),
+ new NunjucksViewEngine(intermediateDirectory, ...additionalViewPaths),
+ ));
this.use(new PreviousUrlComponent());
// Maintenance
- this.use(new MaintenanceComponent(this, () => {
- return this.as(RedisComponent).canServe() && this.as(MysqlComponent).canServe();
- }));
+ this.use(new MaintenanceComponent());
this.use(new AutoUpdateComponent());
// Services
this.use(new MysqlComponent());
- this.use(new MailComponent());
+ this.use(new MailComponent(new MailViewEngine(intermediateDirectory, ...additionalViewPaths)));
// Session
this.use(new RedisComponent());
@@ -117,6 +108,7 @@ export default class App extends Application {
// WebSocket server
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
+ this.use(new WebSocketServerComponent());
// Jobs
diff --git a/assets/img/logo.svg b/src/assets/img/logo.svg
similarity index 100%
rename from assets/img/logo.svg
rename to src/assets/img/logo.svg
diff --git a/assets/img/logox1024.png b/src/assets/img/logox1024.png
similarity index 100%
rename from assets/img/logox1024.png
rename to src/assets/img/logox1024.png
diff --git a/assets/img/logox128.png b/src/assets/img/logox128.png
similarity index 100%
rename from assets/img/logox128.png
rename to src/assets/img/logox128.png
diff --git a/src/assets/package.json b/src/assets/package.json
new file mode 100644
index 0000000..1cd945a
--- /dev/null
+++ b/src/assets/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "commonjs"
+}
diff --git a/public/.gitkeep b/src/assets/sass/.gitkeep
similarity index 100%
rename from public/.gitkeep
rename to src/assets/sass/.gitkeep
diff --git a/src/assets/ts/tsconfig.eslint.json b/src/assets/ts/tsconfig.eslint.json
new file mode 100644
index 0000000..a12ab8a
--- /dev/null
+++ b/src/assets/ts/tsconfig.eslint.json
@@ -0,0 +1,6 @@
+{
+ "extends": "./tsconfig.json",
+ "include": [
+ "./**/*"
+ ]
+}
diff --git a/src/assets/ts/tsconfig.json b/src/assets/ts/tsconfig.json
new file mode 100644
index 0000000..a9dfc8d
--- /dev/null
+++ b/src/assets/ts/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "CommonJS",
+ "baseUrl": "../../../intermediates/assets",
+ "rootDir": "../../../intermediates/assets/ts-source",
+ "sourceRoot": "../../../intermediates/assets/ts-source",
+ "outDir": "../../../intermediates/assets/ts",
+ "declaration": false,
+
+ "typeRoots": [],
+ "resolveJsonModule": false,
+ "lib": [
+ "es2020",
+ "DOM"
+ ]
+ },
+ "include": [
+ "../../../intermediates/assets/ts-source/**/*"
+ ],
+ "references": [
+ {
+ "path": "../../common"
+ }
+ ]
+}
diff --git a/src/assets/views/tsconfig.json b/src/assets/views/tsconfig.json
new file mode 100644
index 0000000..bc808e8
--- /dev/null
+++ b/src/assets/views/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "public/js",
+ "rootDir": "../../../intermediates/assets",
+ },
+ "include": [
+ "src/assets/ts/**/*"
+ ],
+ "references": [
+ {
+ "path": "../../common"
+ }
+ ]
+}
diff --git a/src/common/dummy.ts b/src/common/dummy.ts
new file mode 100644
index 0000000..4f6d24e
--- /dev/null
+++ b/src/common/dummy.ts
@@ -0,0 +1 @@
+console.log('common code between back and front');
diff --git a/src/common/package.json b/src/common/package.json
new file mode 100644
index 0000000..1cd945a
--- /dev/null
+++ b/src/common/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "commonjs"
+}
diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json
new file mode 100644
index 0000000..60705a9
--- /dev/null
+++ b/src/common/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+
+ "module": "CommonJS",
+
+ "baseUrl": "../../dist/common-local",
+ "rootDir": "./",
+ "sourceRoot": "./",
+ "outDir": "../../dist/common-local",
+
+ "typeRoots": [
+ "src/types"
+ ],
+ },
+ "include": [
+ "./**/*"
+ ]
+}
diff --git a/src/controllers/HomeController.ts b/src/controllers/HomeController.ts
index b5e1740..825b619 100644
--- a/src/controllers/HomeController.ts
+++ b/src/controllers/HomeController.ts
@@ -1,5 +1,6 @@
-import Controller from "swaf/Controller";
import {Request, Response} from "express";
+import {route} from "swaf/common/Routing";
+import Controller from "swaf/Controller";
export default class HomeController extends Controller {
public routes(): void {
@@ -20,6 +21,6 @@ export default class HomeController extends Controller {
* This is to test and assert that swaf extended types are available
*/
protected async goBack(req: Request, res: Response): Promise {
- res.redirect(req.getPreviousUrl() || Controller.route('home'));
+ res.redirect(req.getPreviousUrl() || route('home'));
}
}
diff --git a/src/main.ts b/src/main.ts
index babc7df..8bf4ab4 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,14 +6,22 @@ process.env['NODE_CONFIG_DIR'] =
+ delimiter
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../config/');
-import {logger} from "swaf/Logger";
-import App from "./App";
import config from "config";
+import {promises as fs} from "fs";
+import {logger} from "swaf/Logger";
+
+import App from "./App.js";
(async () => {
logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
- const app = new App(config.get('listen_addr'), config.get('port'));
+ const packageJson = JSON.parse((await fs.readFile('package.json')).toString());
+
+ const app = new App(
+ packageJson.version,
+ config.get('app.listen_addr'),
+ config.get('app.port'),
+ );
await app.start();
})().catch(err => {
logger.error(err);
diff --git a/src/tsconfig.json b/src/tsconfig.json
new file mode 100644
index 0000000..890991d
--- /dev/null
+++ b/src/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "composite": true,
+
+ "module": "CommonJS",
+
+ "baseUrl": "../dist",
+ "rootDir": "./",
+ "sourceRoot": "./",
+ "outDir": "../dist",
+
+ "typeRoots": [
+ "src/types"
+ ]
+ },
+ "include": [
+ "./**/*",
+ "../node_modules/swaf/types"
+ ],
+ "exclude": [
+ "./assets/**/*",
+ "./common/**/*"
+ ],
+ "references": [
+ {
+ "path": "./common"
+ }
+ ]
+}
diff --git a/src/types/.gitkeep b/src/types/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tsconfig.frontend.json b/tsconfig.frontend.json
deleted file mode 100644
index ad74720..0000000
--- a/tsconfig.frontend.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "outDir": "public/js",
- "rootDir": "./assets",
- "target": "ES6",
- "strict": true,
- "lib": [
- "es2020",
- "DOM"
- ],
- "typeRoots": [
- "./node_modules/@types"
- ]
- },
- "include": [
- "assets/ts/**/*"
- ]
-}
diff --git a/tsconfig.json b/tsconfig.json
index 0b19cec..6a82f14 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,23 +1,43 @@
{
"compilerOptions": {
- "module": "CommonJS",
- "esModuleInterop": true,
- "outDir": "dist",
- "rootDir": "./src",
- "target": "ES6",
+ "target": "ESNext",
+ "module": "ESNext",
+ "declaration": true,
+ "stripInternal": true,
+
"strict": true,
+ "allowSyntheticDefaultImports": true,
+ "strictNullChecks": true,
+
+ "moduleResolution": "Node",
+ "esModuleInterop": true,
+ "baseUrl": "dist",
+ "inlineSourceMap": true,
+ "inlineSources": true,
+ "outDir": "dist",
+
+ "typeRoots": [
+ "node_modules/@types",
+ "src/types"
+ ],
"lib": [
"es2020",
- "DOM"
- ],
- "typeRoots": [
- "./node_modules/@types"
+ "dom"
],
"resolveJsonModule": true,
- "skipLibCheck": true
+ "skipLibCheck": true,
+ "allowJs": true
},
- "include": [
- "src/**/*",
- "node_modules/swaf/types"
+ "include": [],
+ "references": [
+ {
+ "path": "src",
+ },
+ {
+ "path": "src/assets/ts",
+ },
+ {
+ "path": "src/assets/views",
+ }
]
}
diff --git a/tsconfig.test.json b/tsconfig.test.json
index 897a6b4..fce2399 100644
--- a/tsconfig.test.json
+++ b/tsconfig.test.json
@@ -11,4 +11,4 @@
"src/types/**/*",
"test/**/*"
]
-}
\ No newline at end of file
+}
diff --git a/webpack.config.js b/webpack.config.js
deleted file mode 100644
index 598acfd..0000000
--- a/webpack.config.js
+++ /dev/null
@@ -1,100 +0,0 @@
-const path = require('path');
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
-const TerserPlugin = require('terser-webpack-plugin');
-
-const dev = process.env.NODE_ENV === 'development';
-
-const userConfig = require('./assets/config.json');
-for (const b in userConfig.bundles) {
- if (userConfig.bundles.hasOwnProperty(b)) {
- userConfig.bundles[b] = `./assets/${userConfig.bundles[b]}`;
- }
-}
-
-const config = {
- entry: userConfig.bundles,
- output: {
- path: path.resolve(__dirname, 'public/js'),
- filename: '[name].js'
- },
- devtool: dev ? 'eval-source-map' : undefined,
- module: {
- rules: [
- {
- test: /\.js$/i,
- use: [
- {
- loader: 'babel-loader',
- options: {
- presets: ['@babel/preset-env'],
- }
- }
- ]
- },
- {
- test: /\.s[ac]ss$/i,
- use: [
- {
- loader: MiniCssExtractPlugin.loader,
- options: {
- publicPath: '/',
- }
- },
- 'css-loader',
- 'sass-loader',
- ]
- },
- {
- test: /\.(woff2?|eot|ttf|otf)$/i,
- use: 'file-loader?name=../fonts/[name].[ext]',
- },
- {
- test: /\.tsx?$/i,
- use: {
- loader: 'ts-loader',
- options: {
- configFile: 'tsconfig.frontend.json',
- }
- },
- exclude: '/node_modules/'
- },
- {
- test: /\.(png|jpe?g|gif|svg)$/i,
- use: [
- 'file-loader?name=../img/[name].[ext]',
- {
- loader: 'img-loader',
- options: {
- enabled: !dev,
- plugins: [
- require('imagemin-gifsicle')({}),
- require('imagemin-mozjpeg')({}),
- require('imagemin-pngquant')({}),
- require('imagemin-svgo')({}),
- ]
- }
- }
- ]
- }
- ],
- },
- resolve: {
- extensions: ['.tsx', '.ts', '.js'],
- },
- plugins: [
- new MiniCssExtractPlugin({
- filename: '../css/[name].css',
- }),
- ]
-};
-
-if (!dev) {
- config.optimization = {
- minimize: true,
- minimizer: [
- new TerserPlugin(),
- ]
- };
-}
-
-module.exports = config;
\ No newline at end of file