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