Initial commit
						commit
						8a47730a48
					
				| @ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  | 	"phabricator.uri": "https://hub.sealcode.org/", | ||||||
|  | 	"arc.land.onto.default": "master", | ||||||
|  | 	"load": ["arcanist-linters", "arc-unit-mocha/src"], | ||||||
|  | 	"unit.engine": "MochaEngine", | ||||||
|  | 	"unit.mocha.include": ["./lib/**/*.test.js"], | ||||||
|  | 	"unit.mocha.dockerRoot": "/opt/sealious-app" | ||||||
|  | } | ||||||
| @ -0,0 +1,13 @@ | |||||||
|  | { | ||||||
|  | 	"linters": { | ||||||
|  | 		"prettier": { | ||||||
|  | 			"type": "prettier", | ||||||
|  | 			"bin": "./node_modules/.bin/prettier", | ||||||
|  | 			"include": ["(\\.[tj]s$)", "(\\.s?css$)"] | ||||||
|  | 		}, | ||||||
|  | 		"eslint": { | ||||||
|  | 			"type": "eslint", | ||||||
|  | 			"include": ["(src/.*\\.ts$)", "(src/.*\\.js$)"] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,43 @@ | |||||||
|  | module.exports = { | ||||||
|  | 	env: { node: true }, | ||||||
|  | 	parser: "@typescript-eslint/parser", | ||||||
|  | 	plugins: ["@typescript-eslint", "prettier", "with-tsc-error"], | ||||||
|  | 	extends: [ | ||||||
|  | 		"eslint:recommended", | ||||||
|  | 		"plugin:@typescript-eslint/recommended", | ||||||
|  | 		"plugin:@typescript-eslint/recommended-requiring-type-checking", | ||||||
|  | 		"plugin:prettier/recommended", | ||||||
|  | 	], | ||||||
|  | 	parserOptions: { | ||||||
|  | 		sourceType: "module", | ||||||
|  | 		ecmaFeatures: { | ||||||
|  | 			modules: true, | ||||||
|  | 		}, | ||||||
|  | 		project: [ | ||||||
|  | 			"./src/back/tsconfig.json", | ||||||
|  | 			"./src/front/tsconfig.json", | ||||||
|  | 			"./src/scripts/tsconfig.json", | ||||||
|  | 		], | ||||||
|  | 	}, | ||||||
|  | 	rules: { | ||||||
|  | 		"@typescript-eslint/require-await": 0, | ||||||
|  | 		/* "jsdoc/require-description": 2, */ | ||||||
|  | 		"no-await-in-loop": 2, | ||||||
|  | 		"@typescript-eslint/consistent-type-assertions": [1, { assertionStyle: "never" }], | ||||||
|  | 		"no-console": 1, | ||||||
|  | 	}, | ||||||
|  | 	settings: { jsdoc: { mode: "typescript" } }, | ||||||
|  | 	overrides: [ | ||||||
|  | 		{ | ||||||
|  | 			files: ["*.subtest.ts", "*.test.ts"], | ||||||
|  | 			rules: { | ||||||
|  | 				"@typescript-eslint/no-unsafe-member-access": 0, | ||||||
|  | 				"prefer-const": 0, | ||||||
|  | 				"@typescript-eslint/no-unsafe-call": 0, | ||||||
|  | 				"@typescript-eslint/no-unsafe-return": 0, | ||||||
|  | 				"@typescript-eslint/no-unsafe-assignment": 0, | ||||||
|  | 				"no-await-in-loop": 1, // sometimes it's easier to debug when requests run sequentially
 | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	], | ||||||
|  | }; | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | .DS_Store | ||||||
|  | .idea | ||||||
|  | *.log | ||||||
|  | tmp/ | ||||||
|  | *~ | ||||||
|  | *.sublime-workspace | ||||||
|  | npm-debug.log | ||||||
|  | node_modules | ||||||
|  | db | ||||||
|  | coverage.html | ||||||
|  | \#* | ||||||
|  | .\#* | ||||||
|  | .npm | ||||||
|  | .config | ||||||
|  | .ash_history | ||||||
|  | cosealious | ||||||
|  | node_modules* | ||||||
|  | .cache | ||||||
|  | lib | ||||||
|  | @types | ||||||
|  | .xunit | ||||||
|  | coverage | ||||||
|  | .nyc_output | ||||||
|  | /dist/ | ||||||
|  | public/dist | ||||||
|  | /log.html | ||||||
|  | /hint-report/ | ||||||
|  | .vscode | ||||||
|  | .env | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | { | ||||||
|  | 	"connector": { | ||||||
|  | 		"name": "jsdom" | ||||||
|  | 	}, | ||||||
|  | 	"formatters": ["codeframe"], | ||||||
|  | 	"hintsTimeout": 20000, | ||||||
|  | 	"extends": ["web-recommended", "accessibility"], | ||||||
|  | 	"hints": { | ||||||
|  | 		"no-friendly-error-pages": "off", | ||||||
|  | 		"no-broken-links": "warning", | ||||||
|  | 		"doctype": "error", | ||||||
|  | 		"apple-touch-icons": "error", | ||||||
|  | 		"button-type": "error", | ||||||
|  | 		"compat-api/css": "error", | ||||||
|  | 		"compat-api/html": [ | ||||||
|  | 			"error", | ||||||
|  | 			{ | ||||||
|  | 				"ignore": ["img[loading]"] | ||||||
|  | 			} | ||||||
|  | 		], | ||||||
|  | 		"create-element-svg": "error", | ||||||
|  | 		"css-prefix-order": "error", | ||||||
|  | 		"disown-opener": "error", | ||||||
|  | 		"highest-available-document-mode": "error", | ||||||
|  | 		"leading-dot-classlist": "error", | ||||||
|  | 		"manifest-exists": "error", | ||||||
|  | 		"meta-charset-utf-8": "error", | ||||||
|  | 		"meta-viewport": "error", | ||||||
|  | 		"no-bom": "error", | ||||||
|  | 		"no-inline-styles": "error", | ||||||
|  | 		"no-protocol-relative-urls": "error", | ||||||
|  | 		"html-checker": "error", | ||||||
|  | 		"scoped-svg-styles": "error", | ||||||
|  | 		"sri": "error", | ||||||
|  | 		"axe/aria": "error", | ||||||
|  | 		"axe/color": "error", | ||||||
|  | 		"axe/forms": "error", | ||||||
|  | 		"axe/keyboard": "error", | ||||||
|  | 		"axe/language": "error", | ||||||
|  | 		"axe/name-role-value": "error", | ||||||
|  | 		"axe/parsing": "error", | ||||||
|  | 		"axe/semantics": "error", | ||||||
|  | 		"axe/sensory-and-visual-cues": "error", | ||||||
|  | 		"axe/structure": "error", | ||||||
|  | 		"axe/tables": "error", | ||||||
|  | 		"axe/text-alternatives": "error", | ||||||
|  | 		"axe/time-and-media": "error" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,68 @@ | |||||||
|  | { | ||||||
|  | 	"connector": "local", | ||||||
|  | 	"extends": ["web-recommended", "accessibility"], | ||||||
|  | 	"formatters": ["codeframe"], | ||||||
|  | 	"hints": { | ||||||
|  | 		"apple-touch-icons": "error", | ||||||
|  | 		"button-type": "error", | ||||||
|  | 		"compat-api/css": "error", | ||||||
|  | 		"compat-api/html": "error", | ||||||
|  | 		"create-element-svg": "error", | ||||||
|  | 		"css-prefix-order": "error", | ||||||
|  | 		"disown-opener": "error", | ||||||
|  | 		"highest-available-document-mode": "error", | ||||||
|  | 		"leading-dot-classlist": "error", | ||||||
|  | 		"manifest-exists": "error", | ||||||
|  | 		"meta-charset-utf-8": "off", | ||||||
|  | 		"meta-viewport": "error", | ||||||
|  | 		"no-bom": "error", | ||||||
|  | 		"no-inline-styles": "error", | ||||||
|  | 		"no-protocol-relative-urls": "error", | ||||||
|  | 		"scoped-svg-styles": "error", | ||||||
|  | 		"sri": "error", | ||||||
|  | 		"axe/aria": "error", | ||||||
|  | 		"axe/color": "error", | ||||||
|  | 		"axe/forms": "error", | ||||||
|  | 		"axe/keyboard": "error", | ||||||
|  | 		"axe/language": "error", | ||||||
|  | 		"axe/name-role-value": "error", | ||||||
|  | 		"axe/parsing": "error", | ||||||
|  | 		"axe/semantics": "error", | ||||||
|  | 		"axe/sensory-and-visual-cues": "error", | ||||||
|  | 		"axe/structure": "error", | ||||||
|  | 		"axe/tables": "error", | ||||||
|  | 		"axe/text-alternatives": "error", | ||||||
|  | 		"axe/time-and-media": "error", | ||||||
|  | 		"no-friendly-error-pages": "off", | ||||||
|  | 		"content-type": "off", | ||||||
|  | 		"http-cache": "off", | ||||||
|  | 		"http-compression": "off", | ||||||
|  | 		"no-disallowed-headers": "off", | ||||||
|  | 		"no-html-only-headers": "off", | ||||||
|  | 		"no-http-redirects": "off", | ||||||
|  | 		"no-vulnerable-javascript-libraries": "off", | ||||||
|  | 		"ssllabs": "off", | ||||||
|  | 		"strict-transport-security": "off", | ||||||
|  | 		"stylesheet-limits": "off", | ||||||
|  | 		"validate-set-cookie-header": "off", | ||||||
|  | 		"x-content-type-options": "off", | ||||||
|  | 		"no-broken-links": "off", | ||||||
|  | 		"typescript-config/consistent-casing": "off", | ||||||
|  | 		"typescript-config/is-valid": "off", | ||||||
|  | 		"typescript-config/strict": "off", | ||||||
|  | 		"typescript-config/target": "off" | ||||||
|  | 	}, | ||||||
|  | 	"hintsTimeout": 10000, | ||||||
|  | 	"parsers": [ | ||||||
|  | 		"babel-config", | ||||||
|  | 		"css", | ||||||
|  | 		"html", | ||||||
|  | 		"javascript", | ||||||
|  | 		"jsx", | ||||||
|  | 		"less", | ||||||
|  | 		"sass", | ||||||
|  | 		"typescript", | ||||||
|  | 		"typescript-config", | ||||||
|  | 		"webpack-config" | ||||||
|  | 	] | ||||||
|  | } | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | { | ||||||
|  | 	"useTabs": true, | ||||||
|  | 	"tabWidth": 4, | ||||||
|  | 	"trailingComma": "es5", | ||||||
|  | 	"printWidth": 90, | ||||||
|  | 	"overrides": [ | ||||||
|  | 		{ | ||||||
|  | 			"files": "*.yml", | ||||||
|  | 			"options": { | ||||||
|  | 				"tabWidth": 2, | ||||||
|  | 				"useTabs": false | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	] | ||||||
|  | } | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | # Sealious app | ||||||
|  | 
 | ||||||
|  | ## Requirements | ||||||
|  | 
 | ||||||
|  | -   docker | ||||||
|  | -   docker-compose (version 2.6 or up) | ||||||
|  | 
 | ||||||
|  | ## Installation | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | ./npm.sh install | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | Always use ./npm.sh when installing dependencies. | ||||||
|  | 
 | ||||||
|  | ## Running the app in development mode | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | npm run build && SEALIOUS_DB_DELAY=1500 NETWORK_DELAY=1500 npm start | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ## Testing | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | ./npm.sh run test | ||||||
|  | ``` | ||||||
											
												Binary file not shown.
											
										
									
								| After Width: | Height: | Size: 33 KiB | 
| @ -0,0 +1,20 @@ | |||||||
|  | version: "3.2" | ||||||
|  | services: | ||||||
|  |   db: | ||||||
|  |     image: mongo:4.4-bionic | ||||||
|  |     ports: | ||||||
|  |       - "127.0.0.1:${PORT:-2074}7:27017" | ||||||
|  |   test: | ||||||
|  |     image: sealious-app:latest | ||||||
|  |     build: | ||||||
|  |       context: ./docker | ||||||
|  |       dockerfile: ./test.Dockerfile | ||||||
|  |     volumes: | ||||||
|  |       - ./:/opt/sealious-app/ | ||||||
|  |       - ~/.npm_cacache:/opt/sealious-app/.npm_cacache | ||||||
|  |     user: ${UID:-1000}:${GID:-1000} | ||||||
|  |   mailcatcher: | ||||||
|  |     image: schickling/mailcatcher:latest | ||||||
|  |     ports: | ||||||
|  |       - "127.0.0.1:${PORT:-108}2:1080" | ||||||
|  |       - "127.0.0.1:${PORT:-102}6:1025" | ||||||
| @ -0,0 +1,24 @@ | |||||||
|  | FROM node:18-bullseye-slim | ||||||
|  | 
 | ||||||
|  | ENV HOME=/opt/sealious-app | ||||||
|  | 
 | ||||||
|  | # Tini will ensure that any orphaned processes get reaped properly. | ||||||
|  | ENV TINI_VERSION v0.19.0 | ||||||
|  | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini | ||||||
|  | 
 | ||||||
|  | RUN apt update | ||||||
|  | RUN apt install -y git | ||||||
|  | 
 | ||||||
|  | RUN chmod +x /tini | ||||||
|  | ENTRYPOINT ["/tini", "--"] | ||||||
|  | 
 | ||||||
|  | VOLUME $HOME | ||||||
|  | WORKDIR $HOME | ||||||
|  | 
 | ||||||
|  | RUN npm install -g npm@latest | ||||||
|  | 
 | ||||||
|  | USER $UID:$GID | ||||||
|  | 
 | ||||||
|  | EXPOSE 8080 | ||||||
|  | 
 | ||||||
|  | CMD ["/usr/local/bin/node", "."] | ||||||
| @ -0,0 +1,27 @@ | |||||||
|  | #!/bin/bash -xe | ||||||
|  | set -e | ||||||
|  | 
 | ||||||
|  | docker-compose down | ||||||
|  | 
 | ||||||
|  | npx sealgen make-env | ||||||
|  | cp secrets.example.json secrets.json | ||||||
|  | 
 | ||||||
|  | export SEALIOUS_PORT="${PORT}0" | ||||||
|  | SEALIOUS_BASE_URL=$(cat .base_url) | ||||||
|  | export SEALIOUS_BASE_URL | ||||||
|  | echo "PORT=$PORT" >> .env | ||||||
|  | 
 | ||||||
|  | # Create the npm cache directory if it isn't present yet. If it is not present, it will be created | ||||||
|  | # when the docker image is being built with root:root as the owner. | ||||||
|  | mkdir -p ~/.npm_cacache | ||||||
|  | 
 | ||||||
|  | # https://github.com/docker/compose/issues/4725 | ||||||
|  | docker-compose build | ||||||
|  | # Create .npm directory in the container, since it is not yet present and we need it for next step. | ||||||
|  | docker-compose run --user="$UID" --rm --service-ports test mkdir -p /opt/sealious-app/.npm | ||||||
|  | # Link the host-bound npm cache directory into the container's npm cache directory. | ||||||
|  | docker-compose run --user="$UID" --rm --service-ports test ln -s /opt/sealious-app/.npm_cacache/ /opt/sealious-app/.npm/_cacache | ||||||
|  | docker-compose up -d db | ||||||
|  | ./npm.sh --no-TTY --user="$UID" ci && ./npm.sh --no-TTY --user="$UID" run build | ||||||
|  | 
 | ||||||
|  | rm -f log.html | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | #!/bin/bash -xe | ||||||
|  | 
 | ||||||
|  | export SEALIOUS_PORT=$PORT | ||||||
|  | SEALIOUS_BASE_URL=$(cat .base_url) | ||||||
|  | export SEALIOUS_BASE_URL | ||||||
|  | 
 | ||||||
|  | ./npm.sh --no-TTY --user="$UID" run typecheck:front | ||||||
|  | ./npm.sh --no-TTY --user="$UID" run typecheck:back | ||||||
|  | 
 | ||||||
|  | docker-compose run --user="$UID" \ | ||||||
|  | 	-e "SEALIOUS_MONGO_PORT=27017" \ | ||||||
|  | 	-e "SEALIOUS_MONGO_HOST=db" \ | ||||||
|  | 	-e "SEALIOUS_PORT=$SEALIOUS_PORT" \ | ||||||
|  | 	-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \ | ||||||
|  | 	-e "SEALIOUS_SANITY=true" \ | ||||||
|  | 	test | ||||||
|  | 
 | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | SEALIOUS_PORT="${PORT}0" | ||||||
|  | SEALIOUS_BASE_URL=$(cat .base_url) | ||||||
|  | export SEALIOUS_BASE_URL | ||||||
|  | 
 | ||||||
|  | ./npm.sh --no-TTY --user="$UID"  run build:front | ||||||
|  | 
 | ||||||
|  | docker-compose up -d mailcatcher | ||||||
|  | 
 | ||||||
|  | docker-compose run --user="$UID" \ | ||||||
|  | 	-e "SEALIOUS_MONGO_PORT=27017" \ | ||||||
|  | 	-e "SEALIOUS_MONGO_HOST=db" \ | ||||||
|  | 	-e "SEALIOUS_PORT=$SEALIOUS_PORT" \ | ||||||
|  | 	-e "SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL" \ | ||||||
|  | 	-e "SEALIOUS_MAILER=mailcatcher" \ | ||||||
|  | 	-p "${SEALIOUS_PORT}:${SEALIOUS_PORT}" \ | ||||||
|  | 	-d \ | ||||||
|  | 	test \ | ||||||
|  | 	/bin/sh -c "{ node . --color  2>&1; } | ./node_modules/.bin/ansi-html-stream > log.html" && | ||||||
|  | 	echo "App started on $SEALIOUS_PORT" | ||||||
|  | 
 | ||||||
|  | echo "Deployed app to https://${SEALIOUS_PORT}.dep.sealco.de" | ||||||
|  | echo "Mailcatcher available at https://${PORT}2.dep.sealco.de" | ||||||
|  | echo "Application logs should be available at https://jenkins.sealcode.org/job/Deploy%20to%20dep.sealco.de/ws%20v2/$PORT/log.html" | ||||||
| @ -0,0 +1,9 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | 
 | ||||||
|  | export SEALIOUS_PORT="${PORT}0" | ||||||
|  | SEALIOUS_BASE_URL=$(cat .base_url) | ||||||
|  | export SEALIOUS_BASE_URL | ||||||
|  | 
 | ||||||
|  | docker-compose down --volumes | ||||||
|  | rm -rf node_modules | ||||||
|  | 
 | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  | 	"delay": "100" | ||||||
|  | } | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | #!/usr/bin/env -S bash -x | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # the "--no-TTY" option is crucial - without it the output is not captured in Jenkins | ||||||
|  | 
 | ||||||
|  | docker compose run \ | ||||||
|  | 			   --rm \ | ||||||
|  | 			   --service-ports \ | ||||||
|  | 			   test \ | ||||||
|  | 			   npm "$@" | ||||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								| @ -0,0 +1,83 @@ | |||||||
|  | { | ||||||
|  | 	"name": "sealious-app", | ||||||
|  | 	"version": "0.1.0", | ||||||
|  | 	"description": "", | ||||||
|  | 	"main": "./dist/back/index.js", | ||||||
|  | 	"scripts": { | ||||||
|  | 		"start": "node .", | ||||||
|  | 		"typecheck:back": "tsc --noEmit -p src/back", | ||||||
|  | 		"typecheck:front": "tsc --noEmit -p src/front", | ||||||
|  | 		"build": "esbuild --outfile='./public/dist/react.js' src/back/routes/react.tsx && sealgen build", | ||||||
|  | 		"watch": "multiple-scripts-tmux \"npm run typecheck:back -- --watch\"  \"SEALIOUS_PORT=$SEALIOUS_PORT SEALIOUS_BASE_URL=$SEALIOUS_BASE_URL nodemon --enable-source-maps .\" \"npm run build -- --watch\" \"npm run typecheck:front -- --watch\" ", | ||||||
|  | 		"test": "TS_NODE_PROJECT='./src/back/tsconfig.json' mocha --recursive --require ts-node/register src/back/app.ts src/back/**/*.test.ts src/back/**/**/*.test.ts src/back/**/**/**/*.test.ts src/back/**/**/**/**/*.test.ts", | ||||||
|  | 		"coverage": "nyc npm run test --", | ||||||
|  | 		"test-reports": "docker-compose up -d && ./npm.sh run coverage -- --reporter xunit --reporter-option output=.xunit", | ||||||
|  | 		"show-coverage": "npm run test-reports;  xdg-open coverage/index.html" | ||||||
|  | 	}, | ||||||
|  | 	"author": "", | ||||||
|  | 	"license": "ISC", | ||||||
|  | 	"dependencies": { | ||||||
|  | 		"@babel/core": "^7.12.10", | ||||||
|  | 		"@hotwired/turbo": "^7.1.0", | ||||||
|  | 		"@koa/router": "^10.1.1", | ||||||
|  | 		"@sealcode/sealgen": "^0.8.39", | ||||||
|  | 		"@sealcode/ts-predicates": "^0.4.0", | ||||||
|  | 		"@types/kill-port": "^2.0.0", | ||||||
|  | 		"hint": "^7.0.1", | ||||||
|  | 		"ipsum": "^1.0.0", | ||||||
|  | 		"locreq": "^2.0.2", | ||||||
|  | 		"multiple-scripts-tmux": "^1.0.4", | ||||||
|  | 		"nodemon": "^2.0.7", | ||||||
|  | 		"sealious": "^0.17.29", | ||||||
|  | 		"stimulus": "^2.0.0", | ||||||
|  | 		"tempstream": "^0.0.21" | ||||||
|  | 	}, | ||||||
|  | 	"devDependencies": { | ||||||
|  | 		"@hint/connector-jsdom": "^4.1.20", | ||||||
|  | 		"@hint/formatter-codeframe": "^3.1.29", | ||||||
|  | 		"@hint/hint-doctype": "^3.3.19", | ||||||
|  | 		"@hint/hint-no-broken-links": "^4.2.19", | ||||||
|  | 		"@istanbuljs/nyc-config-typescript": "^1.0.2", | ||||||
|  | 		"@sealcode/ansi-html-stream": "^1.0.1", | ||||||
|  | 		"@types/koa__router": "^8.0.4", | ||||||
|  | 		"@types/mocha": "^9.1.0", | ||||||
|  | 		"@types/tedious": "^4.0.7", | ||||||
|  | 		"@typescript-eslint/eslint-plugin": "^5.10.0", | ||||||
|  | 		"@typescript-eslint/parser": "^5.10.2", | ||||||
|  | 		"axios": "^0.24.0", | ||||||
|  | 		"eslint": "^7.19.0", | ||||||
|  | 		"eslint-config-prettier": "^7.2.0", | ||||||
|  | 		"eslint-plugin-prettier": "^3.3.1", | ||||||
|  | 		"eslint-plugin-with-tsc-error": "^0.0.7", | ||||||
|  | 		"kill-port": "^1.6.1", | ||||||
|  | 		"mocha": "^8.4.0", | ||||||
|  | 		"mri": "^1.1.6", | ||||||
|  | 		"nyc": "^15.1.0", | ||||||
|  | 		"prettier": "^2.2.1", | ||||||
|  | 		"ts-loader": "^8.0.14", | ||||||
|  | 		"ts-node": "^10.4.0", | ||||||
|  | 		"typescript": "^4.1.3" | ||||||
|  | 	}, | ||||||
|  | 	"nyc": { | ||||||
|  | 		"extends": "@istanbuljs/nyc-config-typescript", | ||||||
|  | 		"check-coverage": false, | ||||||
|  | 		"all": true, | ||||||
|  | 		"include": [ | ||||||
|  | 			"src/**/!(*.test.*).[tj]s?(x)" | ||||||
|  | 		], | ||||||
|  | 		"exclude": [ | ||||||
|  | 			"src/_tests_/**/*.*" | ||||||
|  | 		], | ||||||
|  | 		"reporter": [ | ||||||
|  | 			"html", | ||||||
|  | 			"lcov", | ||||||
|  | 			"clover", | ||||||
|  | 			"text", | ||||||
|  | 			"text-summary" | ||||||
|  | 		], | ||||||
|  | 		"report-dir": "coverage" | ||||||
|  | 	}, | ||||||
|  | 	"engines": { | ||||||
|  | 		"node": ">=17.0.0" | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1 @@ | |||||||
|  | {} | ||||||
| @ -0,0 +1,87 @@ | |||||||
|  | import _locreq from "locreq"; | ||||||
|  | import { default as Sealious, App, LoggerMailer, SMTPMailer } from "sealious"; | ||||||
|  | import { LoggerLevel } from "sealious/@types/src/app/logger"; | ||||||
|  | import { collections } from "./collections/collections"; | ||||||
|  | import { sleep } from "./util"; | ||||||
|  | const locreq = _locreq(__dirname); | ||||||
|  | 
 | ||||||
|  | const PORT = process.env.SEALIOUS_PORT ? parseInt(process.env.SEALIOUS_PORT) : 8080; | ||||||
|  | const base_url = process.env.SEALIOUS_BASE_URL || `http://localhost:${PORT}`; | ||||||
|  | const MONGO_PORT = process.env.SEALIOUS_MONGO_PORT | ||||||
|  | 	? parseInt(process.env.SEALIOUS_MONGO_PORT) | ||||||
|  | 	: 20747; | ||||||
|  | const MONGO_HOST = process.env.SEALIOUS_MONGO_HOST || "127.0.0.1"; | ||||||
|  | 
 | ||||||
|  | declare module "koa" { | ||||||
|  | 	interface BaseContext { | ||||||
|  | 		$context: Sealious.Context; | ||||||
|  | 		$app: TheApp; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default class TheApp extends App { | ||||||
|  | 	config = { | ||||||
|  | 		upload_path: locreq.resolve("uploaded_files"), | ||||||
|  | 		datastore_mongo: { | ||||||
|  | 			host: MONGO_HOST, | ||||||
|  | 			port: MONGO_PORT, | ||||||
|  | 			db_name: "sealious-app", | ||||||
|  | 		}, | ||||||
|  | 		email: { | ||||||
|  | 			from_address: "sealious-app@example.com", | ||||||
|  | 			from_name: "sealious-app app", | ||||||
|  | 		}, | ||||||
|  | 		logger: { | ||||||
|  | 			// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
 | ||||||
|  | 			level: "info" as LoggerLevel, | ||||||
|  | 		}, | ||||||
|  | 		"www-server": { | ||||||
|  | 			port: PORT, | ||||||
|  | 		}, | ||||||
|  | 		core: { | ||||||
|  | 			environment: <const>"production", // to send the full html emails
 | ||||||
|  | 		}, | ||||||
|  | 	}; | ||||||
|  | 	manifest = { | ||||||
|  | 		name: "sealious-app", | ||||||
|  | 		logo: locreq.resolve("assets/logo.png"), | ||||||
|  | 		version: "0.0.1", | ||||||
|  | 		default_language: "en", | ||||||
|  | 		base_url, | ||||||
|  | 		admin_email: "admin@example.com", | ||||||
|  | 		colors: { | ||||||
|  | 			primary: "#5294a1", | ||||||
|  | 		}, | ||||||
|  | 	}; | ||||||
|  | 	collections = collections; | ||||||
|  | 	mailer = | ||||||
|  | 		process.env.SEALIOUS_MAILER === "mailcatcher" | ||||||
|  | 			? new SMTPMailer({ | ||||||
|  | 					host: "mailcatcher", | ||||||
|  | 					port: 1025, | ||||||
|  | 					user: "any", | ||||||
|  | 					password: "any", | ||||||
|  | 			  }) | ||||||
|  | 			: new LoggerMailer(); | ||||||
|  | 
 | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  | 
 | ||||||
|  | 	async start() { | ||||||
|  | 		await super.start(); | ||||||
|  | 		await this.collections.posts.populate(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async stop() { | ||||||
|  | 		await super.stop(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	initRouter() { | ||||||
|  | 		this.HTTPServer.router.use("/", async (_, next) => { | ||||||
|  | 			const ms = parseInt(process.env.NETWORK_DELAY || "0"); | ||||||
|  | 			console.log("NETWORK DELAY!", ms); | ||||||
|  | 			await sleep(ms); | ||||||
|  | 			await next(); | ||||||
|  | 		}); | ||||||
|  | 		super.initRouter(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | // DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
 | ||||||
|  | import { App } from "sealious"; | ||||||
|  | 
 | ||||||
|  | import _GroupsToUsers from "./groups-to-users"; | ||||||
|  | import _Groups from "./groups"; | ||||||
|  | import _PasswordResetIntents from "./password-reset-intents"; | ||||||
|  | import _Posts from "./posts"; | ||||||
|  | import _Secrets from "./secrets"; | ||||||
|  | import _UserRoles from "./user-roles"; | ||||||
|  | import _Users from "./users"; | ||||||
|  | 
 | ||||||
|  | export const GroupsToUsers = new _GroupsToUsers(); | ||||||
|  | export const Groups = new _Groups(); | ||||||
|  | export const PasswordResetIntents = new _PasswordResetIntents(); | ||||||
|  | export const Posts = new _Posts(); | ||||||
|  | export const Secrets = new _Secrets(); | ||||||
|  | export const UserRoles = new _UserRoles(); | ||||||
|  | export const Users = new _Users(); | ||||||
|  | 
 | ||||||
|  | export const collections = { | ||||||
|  | 	...App.BaseCollections, | ||||||
|  | 	"groups-to-users": GroupsToUsers, | ||||||
|  | 	groups: Groups, | ||||||
|  | 	"password-reset-intents": PasswordResetIntents, | ||||||
|  | 	posts: Posts, | ||||||
|  | 	secrets: Secrets, | ||||||
|  | 	"user-roles": UserRoles, | ||||||
|  | 	users: Users, | ||||||
|  | }; | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | import { Collection, FieldTypes, Policies } from "sealious"; | ||||||
|  | import { Roles } from "../policy-types/roles"; | ||||||
|  | 
 | ||||||
|  | export default class GroupsToUsers extends Collection { | ||||||
|  | 	fields = { | ||||||
|  | 		user: new FieldTypes.SingleReference("users"), | ||||||
|  | 		group: new FieldTypes.SingleReference("groups"), | ||||||
|  | 	}; | ||||||
|  | 	defaultPolicy = new Roles(["admin"]); | ||||||
|  | 	policies = { | ||||||
|  | 		show: new Policies.Or([ | ||||||
|  | 			new Roles(["admin"]), | ||||||
|  | 			new Policies.UserReferencedInField("user"), | ||||||
|  | 		]), | ||||||
|  | 		list: new Policies.Or([ | ||||||
|  | 			new Roles(["admin"]), | ||||||
|  | 			new Policies.UserReferencedInField("user"), | ||||||
|  | 		]), | ||||||
|  | 	}; | ||||||
|  | } | ||||||
| @ -0,0 +1,23 @@ | |||||||
|  | import { Collection, FieldTypes, Policies } from "sealious"; | ||||||
|  | import { Roles } from "../policy-types/roles"; | ||||||
|  | 
 | ||||||
|  | export default class Groups extends Collection { | ||||||
|  | 	fields = { | ||||||
|  | 		name: new FieldTypes.Text(), | ||||||
|  | 	}; | ||||||
|  | 	defaultPolicy = new Policies.LoggedIn(); | ||||||
|  | 	policies = { | ||||||
|  | 		create: new Roles(["admin"]), | ||||||
|  | 		edit: new Roles(["admin"]), | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	async populate(): Promise<void> { | ||||||
|  | 		if (await this.app.Metadata.get("groups_populated")) { | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		// eslint-disable-next-line no-console
 | ||||||
|  | 		console.log("### Populating groups"); | ||||||
|  | 		// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
 | ||||||
|  | 		await this.app.Metadata.set("groups_populated", "true"); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,89 @@ | |||||||
|  | import axios from "axios"; | ||||||
|  | import assert from "assert"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | 
 | ||||||
|  | describe("password-reset-intents", () => { | ||||||
|  | 	async function createAUser(app: TheApp) { | ||||||
|  | 		await app.collections.users.suCreate({ | ||||||
|  | 			username: "user", | ||||||
|  | 			email: "user@example.com", | ||||||
|  | 			password: "password", | ||||||
|  | 			roles: [], | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	it("tells you if the email address doesn't exist", async () => | ||||||
|  | 		withProdApp(async ({ app, base_url }) => { | ||||||
|  | 			const email = "fake@example.com"; | ||||||
|  | 			try { | ||||||
|  | 				await axios.post( | ||||||
|  | 					`${base_url}/api/v1/collections/password-reset-intents`, | ||||||
|  | 					{ | ||||||
|  | 						email: email, | ||||||
|  | 					} | ||||||
|  | 				); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				assert.equal( | ||||||
|  | 					e.response.data.data.field_messages.email.message, | ||||||
|  | 					app.i18n("invalid_existing_value", ["users", "email", email]) | ||||||
|  | 				); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			throw new Error("it didn't throw"); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("allows anyone to create an intent, if the email exists", async () => | ||||||
|  | 		withProdApp(async ({ app, base_url }) => { | ||||||
|  | 			await createAUser(app); | ||||||
|  | 			const { email, token } = ( | ||||||
|  | 				await axios.post( | ||||||
|  | 					`${base_url}/api/v1/collections/password-reset-intents`, | ||||||
|  | 					{ | ||||||
|  | 						email: "user@example.com", | ||||||
|  | 					} | ||||||
|  | 				) | ||||||
|  | 			).data; | ||||||
|  | 			assert.deepEqual( | ||||||
|  | 				{ email, token }, | ||||||
|  | 				{ | ||||||
|  | 					email: "user@example.com", | ||||||
|  | 					token: "it's a secret to everybody", | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("tells you if the email address is malformed", async () => | ||||||
|  | 		withProdApp(async ({ app, base_url }) => { | ||||||
|  | 			const email = "incorrect-address"; | ||||||
|  | 			try { | ||||||
|  | 				await axios.post( | ||||||
|  | 					`${base_url}/api/v1/collections/password-reset-intents`, | ||||||
|  | 					{ | ||||||
|  | 						email: email, | ||||||
|  | 					} | ||||||
|  | 				); | ||||||
|  | 			} catch (e) { | ||||||
|  | 				assert.equal( | ||||||
|  | 					e.response.data.data.field_messages.email.message, | ||||||
|  | 					app.i18n("invalid_email", [email]) | ||||||
|  | 				); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			throw new Error("it didn't throw"); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("sends an email with the reset password link", async () => | ||||||
|  | 		withProdApp(async ({ app, base_url, mail_api }) => { | ||||||
|  | 			await createAUser(app); | ||||||
|  | 			await axios.post(`${base_url}/api/v1/collections/password-reset-intents`, { | ||||||
|  | 				email: "user@example.com", | ||||||
|  | 			}); | ||||||
|  | 			const messages = (await mail_api.getMessages()).filter( | ||||||
|  | 				(message) => message.recipients[0] == "<user@example.com>" | ||||||
|  | 			); | ||||||
|  | 			assert.equal(messages.length, 1); | ||||||
|  | 			assert.equal(messages[0].recipients.length, 1); | ||||||
|  | 			assert.equal(messages[0].recipients[0], "<user@example.com>"); | ||||||
|  | 		})); | ||||||
|  | }); | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | import { App, Collection, CollectionItem, Context, FieldTypes, Policies } from "sealious"; | ||||||
|  | import assert from "assert"; | ||||||
|  | import PasswordResetTemplate from "../email-templates/password-reset"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | import { assertType, predicates } from "@sealcode/ts-predicates"; | ||||||
|  | 
 | ||||||
|  | export default class PasswordResetIntents extends Collection { | ||||||
|  | 	name = "password-reset-intents"; | ||||||
|  | 	fields = { | ||||||
|  | 		email: new FieldTypes.ValueExistingInCollection({ | ||||||
|  | 			field: "email", | ||||||
|  | 			collection: "users", | ||||||
|  | 			include_forbidden: true, | ||||||
|  | 		}), | ||||||
|  | 		token: new FieldTypes.SecretToken(), | ||||||
|  | 	}; | ||||||
|  | 	policies = { | ||||||
|  | 		create: new Policies.Public(), | ||||||
|  | 		edit: new Policies.Noone(), | ||||||
|  | 	}; | ||||||
|  | 	defaultPolicy = new Policies.Super(); | ||||||
|  | 	async init(app: App, name: string) { | ||||||
|  | 		assert(app instanceof TheApp); | ||||||
|  | 		await super.init(app, name); | ||||||
|  | 		app.collections["password-reset-intents"].on( | ||||||
|  | 			"after:create", | ||||||
|  | 			async ([, intent]: [ | ||||||
|  | 				Context, | ||||||
|  | 				CollectionItem<PasswordResetIntents>, | ||||||
|  | 				unknown | ||||||
|  | 			]) => { | ||||||
|  | 				const intent_as_super = await intent.fetchAs(new app.SuperContext()); | ||||||
|  | 				const message = await PasswordResetTemplate(app, { | ||||||
|  | 					email_address: assertType( | ||||||
|  | 						intent.get("email"), | ||||||
|  | 						predicates.string, | ||||||
|  | 						"email_address isn't a string" | ||||||
|  | 					), | ||||||
|  | 					token: assertType( | ||||||
|  | 						intent_as_super.get("token"), | ||||||
|  | 						predicates.string, | ||||||
|  | 						"token isn't a string" | ||||||
|  | 					), | ||||||
|  | 				}); | ||||||
|  | 				await message.send(app); | ||||||
|  | 			} | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,11 @@ | |||||||
|  | import { Collection, FieldTypes, Policies } from "sealious"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | const { Ipsum } = require("ipsum"); | ||||||
|  | 
 | ||||||
|  | export default class Posts extends Collection { | ||||||
|  | 	fields = { | ||||||
|  | 		title: new FieldTypes.Text(), | ||||||
|  | 		description: new FieldTypes.Text(), | ||||||
|  | 	}; | ||||||
|  | 	defaultPolicy = new Policies.Public(); | ||||||
|  | } | ||||||
| @ -0,0 +1,10 @@ | |||||||
|  | import { Collection, FieldTypes } from "sealious"; | ||||||
|  | import { Roles } from "../policy-types/roles"; | ||||||
|  | 
 | ||||||
|  | /* For testing the Roles policy */ | ||||||
|  | export default class Secrets extends Collection { | ||||||
|  | 	fields = { | ||||||
|  | 		content: new FieldTypes.Text(), | ||||||
|  | 	}; | ||||||
|  | 	defaultPolicy = new Roles(["admin"]); | ||||||
|  | } | ||||||
| @ -0,0 +1,70 @@ | |||||||
|  | import assert from "assert"; | ||||||
|  | import axios from "axios"; | ||||||
|  | import { Context, TestUtils } from "sealious"; | ||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { createAdmin, createAUser } from "../test_utils/users"; | ||||||
|  | import Users from "./users"; | ||||||
|  | 
 | ||||||
|  | describe("user-roles", () => { | ||||||
|  | 	it("rejects when given an empty role", async () => | ||||||
|  | 		withProdApp(async ({ app, rest_api }) => { | ||||||
|  | 			const [user, session] = await createAdmin(app, rest_api); | ||||||
|  | 			await TestUtils.assertThrowsAsync( | ||||||
|  | 				async () => { | ||||||
|  | 					return rest_api.post( | ||||||
|  | 						`/api/v1/collections/user-roles`, | ||||||
|  | 						{ | ||||||
|  | 							user: user.id, | ||||||
|  | 						}, | ||||||
|  | 						session | ||||||
|  | 					); | ||||||
|  | 				}, | ||||||
|  | 				(e: any) => { | ||||||
|  | 					assert.equal( | ||||||
|  | 						e?.response.data.data.field_messages.role?.message, | ||||||
|  | 						"Missing value for field 'role'." | ||||||
|  | 					); | ||||||
|  | 				} | ||||||
|  | 			); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("accepts correct dataset", async () => | ||||||
|  | 		withProdApp(async ({ app, base_url, rest_api }) => { | ||||||
|  | 			const [user, session] = await createAdmin(app, rest_api); | ||||||
|  | 			const response = await axios.post( | ||||||
|  | 				`${base_url}/api/v1/collections/user-roles`, | ||||||
|  | 				{ | ||||||
|  | 					user: user.id, | ||||||
|  | 					role: "admin", | ||||||
|  | 				}, | ||||||
|  | 				session | ||||||
|  | 			); | ||||||
|  | 			assert.equal(response.status, 201); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("get user roles with admin", async () => | ||||||
|  | 		withProdApp(async ({ app, rest_api }) => { | ||||||
|  | 			const [user] = await createAdmin(app, rest_api); | ||||||
|  | 			const roles = await Users.getRoles( | ||||||
|  | 				new Context(app, new Date().getTime(), user.id) | ||||||
|  | 			); | ||||||
|  | 			assert.ok(roles.includes("admin")); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("get user with no roles", async () => | ||||||
|  | 		withProdApp(async ({ app }) => { | ||||||
|  | 			const user = await createAUser(app, "normal"); | ||||||
|  | 			const roles = await Users.getRoles( | ||||||
|  | 				new Context(app, new Date().getTime(), user.id) | ||||||
|  | 			); | ||||||
|  | 			assert.ok(roles.length === 0); | ||||||
|  | 		})); | ||||||
|  | 
 | ||||||
|  | 	it("get no roles for no logged user", async () => | ||||||
|  | 		withProdApp(async ({ app }) => { | ||||||
|  | 			const roles = await Users.getRoles( | ||||||
|  | 				new Context(app, new Date().getTime(), null) | ||||||
|  | 			); | ||||||
|  | 			assert.ok(roles.length === 0); | ||||||
|  | 		})); | ||||||
|  | }); | ||||||
| @ -0,0 +1,36 @@ | |||||||
|  | import { App, Collection, FieldTypes, Policies, Policy } from "sealious"; | ||||||
|  | import { Roles } from "../policy-types/roles"; | ||||||
|  | 
 | ||||||
|  | export default class UserRoles extends Collection { | ||||||
|  | 	name = "user-roles"; | ||||||
|  | 	fields = { | ||||||
|  | 		role: new FieldTypes.Enum((app: App) => | ||||||
|  | 			app.ConfigManager.get("roles") | ||||||
|  | 		).setRequired(true), | ||||||
|  | 		user: new FieldTypes.SingleReference("users"), | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
 | ||||||
|  | 	policies = { | ||||||
|  | 		create: new Roles(["admin"]), | ||||||
|  | 		delete: new Policies.Public(), | ||||||
|  | 		show: new Policies.UserReferencedInField("user"), | ||||||
|  | 		edit: new Policies.Noone(), | ||||||
|  | 	} as { [policy: string]: Policy }; // this `as` statement allows the policies to be overwritten;
 | ||||||
|  | 
 | ||||||
|  | 	async init(app: App, collection_name: string) { | ||||||
|  | 		await super.init(app, collection_name); | ||||||
|  | 		app.on("started", async () => { | ||||||
|  | 			const roles = app.collections["user-roles"]; | ||||||
|  | 			for (const action of <const>["create", "delete"]) { | ||||||
|  | 				const policy = roles.getPolicy(action); | ||||||
|  | 				if (policy instanceof Policies.Public) { | ||||||
|  | 					app.Logger.warn( | ||||||
|  | 						"USER POLICY", | ||||||
|  | 						`<user-roles> collection is using <public> access strategy for ${action} action. Anyone can change anyone elses role. This is the default behavior and you should overwrite it with <set_policy>` | ||||||
|  | 					); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,49 @@ | |||||||
|  | import { App, Collections, Context, FieldTypes, Policies } from "sealious"; | ||||||
|  | import assert from "assert"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | 
 | ||||||
|  | export default class Users extends Collections.users { | ||||||
|  | 	fields = { | ||||||
|  | 		...App.BaseCollections.users.fields, | ||||||
|  | 		email: new FieldTypes.Email().setRequired(true), | ||||||
|  | 		roles: new FieldTypes.ReverseSingleReference({ | ||||||
|  | 			referencing_collection: "user-roles", | ||||||
|  | 			referencing_field: "user", | ||||||
|  | 		}), | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	defaultPolicy = new Policies.Themselves(); | ||||||
|  | 
 | ||||||
|  | 	async init(app: App, name: string) { | ||||||
|  | 		assert(app instanceof TheApp); | ||||||
|  | 		await super.init(app, name); | ||||||
|  | 		app.on("started", async () => { | ||||||
|  | 			const username = "admin"; | ||||||
|  | 			const users = await app.collections.users | ||||||
|  | 				.suList() | ||||||
|  | 				.filter({ username }) | ||||||
|  | 				.fetch(); | ||||||
|  | 			if (users.empty) { | ||||||
|  | 				app.Logger.warn( | ||||||
|  | 					"ADMIN", | ||||||
|  | 					`Creating an admin account for ${app.manifest.admin_email}` | ||||||
|  | 				); | ||||||
|  | 				await app.collections.users.suCreate({ | ||||||
|  | 					username, | ||||||
|  | 					password: "adminadmin", | ||||||
|  | 					email: "admin@example.com", | ||||||
|  | 					roles: [], | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static async getRoles(ctx: Context) { | ||||||
|  | 		const rolesEntries = await ctx.app.collections["user-roles"] | ||||||
|  | 			.list(ctx) | ||||||
|  | 			.filter({ user: ctx.user_id || "" }) | ||||||
|  | 			.fetch(); | ||||||
|  | 
 | ||||||
|  | 		return rolesEntries.items.map((item) => item.get("role")); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,31 @@ | |||||||
|  | import { App, EmailTemplates, Errors } from "sealious"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | 
 | ||||||
|  | export default async function PasswordResetTemplate( | ||||||
|  | 	app: TheApp, | ||||||
|  | 	{ email_address, token }: { email_address: string; token: string } | ||||||
|  | ) { | ||||||
|  | 	const matching_users = await app.collections["users"] | ||||||
|  | 		.suList() | ||||||
|  | 		.filter({ email: email_address }) | ||||||
|  | 		.fetch(); | ||||||
|  | 
 | ||||||
|  | 	if (!matching_users.items.length) { | ||||||
|  | 		throw new Errors.NotFound("No user with that email"); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const username = matching_users.items[0].get("username"); | ||||||
|  | 
 | ||||||
|  | 	return EmailTemplates.Simple(app, { | ||||||
|  | 		subject: app.i18n("password_reset_email_subject", [app.manifest.name]), | ||||||
|  | 		to: `${username}<${email_address}>`, | ||||||
|  | 		text: ` | ||||||
|  |          ${app.i18n("password_reset_email_text", [app.manifest.name, username])}`,
 | ||||||
|  | 		buttons: [ | ||||||
|  | 			{ | ||||||
|  | 				text: app.i18n("password_reset_cta"), | ||||||
|  | 				href: `${app.manifest.base_url}/confirm-password-reset?token=${token}&email=${email_address}`, | ||||||
|  | 			}, | ||||||
|  | 		], | ||||||
|  | 	}); | ||||||
|  | } | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | import { App, EmailTemplates } from "sealious"; | ||||||
|  | 
 | ||||||
|  | export default async function RegistrationIntentTemplate( | ||||||
|  | 	app: App, | ||||||
|  | 	{ email_address, token }: { email_address: string; token: string } | ||||||
|  | ) { | ||||||
|  | 	return EmailTemplates.Simple(app, { | ||||||
|  | 		subject: app.i18n("registration_intent_email_subject", [app.manifest.name]), | ||||||
|  | 		to: email_address, | ||||||
|  | 		text: ` | ||||||
|  |          ${app.i18n("registration_intent_email_text", [app.manifest.name])}`,
 | ||||||
|  | 		buttons: [ | ||||||
|  | 			{ | ||||||
|  | 				text: app.i18n("registration_intent_cta"), | ||||||
|  | 				href: `${app.manifest.base_url}/account/confirm-registration-email?token=${token}&email=${email_address}`, | ||||||
|  | 			}, | ||||||
|  | 		], | ||||||
|  | 	}); | ||||||
|  | } | ||||||
| @ -0,0 +1,3 @@ | |||||||
|  | export default function frame(id: string, body: string): string { | ||||||
|  | 	return /* HTML */ `<turbo-frame id="${id}"> ${body} </turbo-frame>`; | ||||||
|  | } | ||||||
| @ -0,0 +1,29 @@ | |||||||
|  | import { Templatable, tempstream } from "tempstream"; | ||||||
|  | import { Readable } from "stream"; | ||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | import navbar from "./routes/common/navbar"; | ||||||
|  | 
 | ||||||
|  | export const defaultHead = (ctx: BaseContext, title: string) => /* HTML */ `<title>
 | ||||||
|  | 		${title} · ${ctx.$app.manifest.name} | ||||||
|  | 	</title> | ||||||
|  | 	<meta name="viewport" content="width=device-width" /> | ||||||
|  | 	<script async src="/dist/bundle.js"></script> | ||||||
|  | 	<link href="/dist/style.css" rel="stylesheet" type="text/css" />`;
 | ||||||
|  | 
 | ||||||
|  | export default function html( | ||||||
|  | 	ctx: BaseContext, | ||||||
|  | 	title: string, | ||||||
|  | 	body: Templatable, | ||||||
|  | 	makeHead: (ctx: BaseContext, title: string) => Templatable = defaultHead | ||||||
|  | ): Readable { | ||||||
|  | 	ctx.set("content-type", "text/html;charset=utf-8"); | ||||||
|  | 	return tempstream/* HTML */ ` <!DOCTYPE html>
 | ||||||
|  | 		<html lang="pl"> | ||||||
|  | 			<head> | ||||||
|  | 				${makeHead(ctx, title)} | ||||||
|  | 			</head> | ||||||
|  | 			<body> | ||||||
|  | 				${navbar(ctx)} ${body} | ||||||
|  | 			</body> | ||||||
|  | 		</html>`;
 | ||||||
|  | } | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | import kill from "kill-port"; | ||||||
|  | import _locreq from "locreq"; | ||||||
|  | import TheApp from "./app"; | ||||||
|  | import { mainRouter } from "./routes"; | ||||||
|  | const locreq = _locreq(__dirname); | ||||||
|  | 
 | ||||||
|  | const app = new TheApp(); | ||||||
|  | 
 | ||||||
|  | kill(app.config["www-server"].port) | ||||||
|  | 	.then(() => app.start()) | ||||||
|  | 	.then(async () => { | ||||||
|  | 		if (process.env.SEALIOUS_SANITY === "true") { | ||||||
|  | 			console.log("Exiting with error code 0"); | ||||||
|  | 			process.exit(0); | ||||||
|  | 		} | ||||||
|  | 		mainRouter(app.HTTPServer.router); | ||||||
|  | 	}) | ||||||
|  | 	.catch((error) => { | ||||||
|  | 		console.error(error); | ||||||
|  | 		if (process.env.SEALIOUS_SANITY === "true") { | ||||||
|  | 			console.log("EXITING WITH STATUS 1"); | ||||||
|  | 			process.exit(1); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | app.HTTPServer.addStaticRoute("/", locreq.resolve("public")); | ||||||
| @ -0,0 +1,24 @@ | |||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | 
 | ||||||
|  | describe("roles", () => { | ||||||
|  | 	it("allows access to users with designated role and denies access to users without it", async () => | ||||||
|  | 		withProdApp(async ({ app }) => { | ||||||
|  | 			await app.collections.users.suCreate({ | ||||||
|  | 				username: "regular-user", | ||||||
|  | 				password: "password", | ||||||
|  | 				email: "regular@example.com", | ||||||
|  | 				roles: [], | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			const admin = await app.collections.users.suCreate({ | ||||||
|  | 				username: "someadmin", | ||||||
|  | 				password: "admin-password", | ||||||
|  | 				email: "admin@example.com", | ||||||
|  | 				roles: [], | ||||||
|  | 			}); | ||||||
|  | 			await app.collections["user-roles"].suCreate({ | ||||||
|  | 				user: admin.id, | ||||||
|  | 				role: "admin", | ||||||
|  | 			}); | ||||||
|  | 		})); | ||||||
|  | }); | ||||||
| @ -0,0 +1,55 @@ | |||||||
|  | import { Context, Policy, QueryTypes } from "sealious"; | ||||||
|  | 
 | ||||||
|  | export class Roles extends Policy { | ||||||
|  | 	static type_name = "roles"; | ||||||
|  | 	allowed_roles: string[]; | ||||||
|  | 	constructor(allowed_roles: string[]) { | ||||||
|  | 		super(allowed_roles); | ||||||
|  | 		this.allowed_roles = allowed_roles; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async countMatchingRoles(context: Context) { | ||||||
|  | 		const user_id = context.user_id; | ||||||
|  | 		context.app.Logger.debug2("ROLES", "Checking the roles for user", user_id); | ||||||
|  | 		const user_roles = await context.app.collections["user-roles"] | ||||||
|  | 			.list(context) | ||||||
|  | 			.filter({ user: user_id }) | ||||||
|  | 			.fetch(); | ||||||
|  | 		const roles = user_roles.items.map((user_role) => user_role.get("role")); | ||||||
|  | 
 | ||||||
|  | 		return this.allowed_roles.filter((allowed_role) => roles.includes(allowed_role)) | ||||||
|  | 			.length; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async _getRestrictingQuery(context: Context) { | ||||||
|  | 		if (context.is_super) { | ||||||
|  | 			return new QueryTypes.AllowAll(); | ||||||
|  | 		} | ||||||
|  | 		if (context.user_id === null) { | ||||||
|  | 			return new QueryTypes.DenyAll(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		const matching_roles_count = await this.countMatchingRoles(context); | ||||||
|  | 
 | ||||||
|  | 		return matching_roles_count > 0 | ||||||
|  | 			? new QueryTypes.AllowAll() | ||||||
|  | 			: new QueryTypes.DenyAll(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async checkerFunction(context: Context) { | ||||||
|  | 		if (context.user_id === null) { | ||||||
|  | 			return Policy.deny(context.app.i18n("policy_logged_in_deny")); | ||||||
|  | 		} | ||||||
|  | 		const matching_roles_count = await this.countMatchingRoles(context); | ||||||
|  | 
 | ||||||
|  | 		return matching_roles_count > 0 | ||||||
|  | 			? Policy.allow( | ||||||
|  | 					context.app.i18n("policy_roles_allow", [ | ||||||
|  | 						this.allowed_roles.join(", "), | ||||||
|  | 					]) | ||||||
|  | 			  ) | ||||||
|  | 			: Policy.deny( | ||||||
|  | 					context.app.i18n("policy_roles_deny", [this.allowed_roles.join(", ")]) | ||||||
|  | 			  ); | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,54 @@ | |||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | import { hasShape, is, predicates } from "@sealcode/ts-predicates"; | ||||||
|  | import { Collection, Errors } from "sealious"; | ||||||
|  | import { ItemFields } from "sealious/@types/src/chip-types/collection-item-body"; | ||||||
|  | 
 | ||||||
|  | export interface CollectionTiedFormData<C extends Collection> { | ||||||
|  | 	values: Partial<{ [field in keyof ItemFields<C>]: string }>; | ||||||
|  | 	errors?: Errors.FieldsError<C>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface FormFields<Fields extends string> { | ||||||
|  | 	values: Partial<{ [field in Fields]: string }>; | ||||||
|  | 	errors?: Partial<{ [field in Fields]: string }>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function formHasAllFields<Fields extends readonly string[]>( | ||||||
|  | 	ctx: BaseContext, | ||||||
|  | 	fields: Fields, | ||||||
|  | 	obj: unknown | ||||||
|  | ): obj is { [field in Fields[number]]: string } { | ||||||
|  | 	const valid = | ||||||
|  | 		is(obj, predicates.object) && | ||||||
|  | 		hasShape( | ||||||
|  | 			Object.fromEntries(fields.map((field) => [field, predicates.string])), | ||||||
|  | 			obj | ||||||
|  | 		); | ||||||
|  | 	if (!valid) { | ||||||
|  | 		ctx.status = 422; | ||||||
|  | 		if (is(obj, predicates.object)) { | ||||||
|  | 			ctx.body = `Missing params: ${fields | ||||||
|  | 				.filter((field) => !Object.keys(obj).includes(field)) | ||||||
|  | 				.join(", ")}`;
 | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return valid; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function formHasSomeFields<Fields extends readonly string[]>( | ||||||
|  | 	ctx: BaseContext, | ||||||
|  | 	fields: Fields, | ||||||
|  | 	obj: unknown | ||||||
|  | ): obj is Partial<{ [field in Fields[number]]: string }> { | ||||||
|  | 	const valid = | ||||||
|  | 		is(obj, predicates.object) && | ||||||
|  | 		hasShape( | ||||||
|  | 			Object.fromEntries(fields.map((field) => [field, predicates.string])), | ||||||
|  | 			obj | ||||||
|  | 		); | ||||||
|  | 	if (!valid) { | ||||||
|  | 		ctx.status = 422; | ||||||
|  | 		ctx.body = "Wrong type of params, expected string or undefined"; | ||||||
|  | 	} | ||||||
|  | 	return valid; | ||||||
|  | } | ||||||
| @ -0,0 +1,16 @@ | |||||||
|  | import html from "../../html"; | ||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | import { Readable } from "stream"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | 
 | ||||||
|  | export function MainView(ctx: BaseContext): Readable { | ||||||
|  | 	return html( | ||||||
|  | 		ctx, | ||||||
|  | 		"", | ||||||
|  | 		tempstream/* HTML */ ` | ||||||
|  | 			<title>My Own ToDo App</title> | ||||||
|  | 
 | ||||||
|  | 			<h1>Sealious App</h1> | ||||||
|  | 		` | ||||||
|  | 	); | ||||||
|  | } | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | 
 | ||||||
|  | export default async function navbar(ctx: BaseContext) { | ||||||
|  | 	return /* HTML */ ` <nav>
 | ||||||
|  | 		<a href="/" class="nav-logo"> | ||||||
|  | 			<img | ||||||
|  | 				src="/assets/logo" | ||||||
|  | 				alt="${ctx.$app.manifest.name} - logo" | ||||||
|  | 				width="50" | ||||||
|  | 				height="50" | ||||||
|  | 			/> | ||||||
|  | 			Sealious App | ||||||
|  | 		</a> | ||||||
|  | 		<ul> | ||||||
|  | 			<li><a href="/logowanie">Logowanie</a></li> | ||||||
|  | 		</ul> | ||||||
|  | 	</nav>`;
 | ||||||
|  | } | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | .input { | ||||||
|  | 	&__error { | ||||||
|  | 		color: red; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,42 @@ | |||||||
|  | export default function input({ | ||||||
|  | 	name, | ||||||
|  | 	id, | ||||||
|  | 	label, | ||||||
|  | 	type, | ||||||
|  | 	value, | ||||||
|  | 	placeholder, | ||||||
|  | 	error, | ||||||
|  | 	readonly, | ||||||
|  | 	required, | ||||||
|  | }: { | ||||||
|  | 	name: string; | ||||||
|  | 	id?: string; | ||||||
|  | 	label?: string; | ||||||
|  | 	type?: string; | ||||||
|  | 	value?: string; | ||||||
|  | 	placeholder?: string; | ||||||
|  | 	readonly?: boolean; | ||||||
|  | 	error: string; | ||||||
|  | 	required?: boolean; | ||||||
|  | }) { | ||||||
|  | 	id = id || name; | ||||||
|  | 	label = label || name; | ||||||
|  | 	type = type || "text"; | ||||||
|  | 	value = value || ""; | ||||||
|  | 	placeholder = placeholder || type; | ||||||
|  | 	readonly = readonly || false; | ||||||
|  | 	required = required || false; | ||||||
|  | 	return /* HTML */ `<div class="input">
 | ||||||
|  | 		<label for="${id}">${label}</label> | ||||||
|  | 		<input | ||||||
|  | 			id="${id}" | ||||||
|  | 			type="${type}" | ||||||
|  | 			name="${name}" | ||||||
|  | 			value="${value}" | ||||||
|  | 			placeholder="${placeholder}" | ||||||
|  | 			${readonly ? "readonly" : ""} | ||||||
|  | 			${required ? "required" : ""} | ||||||
|  | 		/> | ||||||
|  | 		${error ? `<div class="input__error">${error}</div>` : ""} | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Page } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "Hello"; | ||||||
|  | 
 | ||||||
|  | export default new (class HelloPage extends Page { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		return html(ctx, "Hello", tempstream/* HTML */ `<div></div>`); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | import Router from "@koa/router"; | ||||||
|  | import { sleep } from "@sealcode/sealgen"; | ||||||
|  | import { Middlewares } from "sealious"; | ||||||
|  | import { MainView } from "./common/main-view"; | ||||||
|  | import mountAutoRoutes from "./routes"; | ||||||
|  | 
 | ||||||
|  | export const mainRouter = (router: Router): void => { | ||||||
|  | 	router.get("/", Middlewares.extractContext(), async (ctx) => { | ||||||
|  | 		ctx.body = MainView(ctx); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	router.use(Middlewares.extractContext()); | ||||||
|  | 
 | ||||||
|  | 	mountAutoRoutes(router); | ||||||
|  | }; | ||||||
| @ -0,0 +1,34 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Mountable } from "@sealcode/sealgen"; | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | import { sleep } from "../util"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "motd"; | ||||||
|  | export function pick<T>(options: readonly T[]): T { | ||||||
|  | 	const random = Math.random(); | ||||||
|  | 	const ret = options[Math.floor(random * options.length)]; | ||||||
|  | 	return ret; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getMOTD() { | ||||||
|  | 	await sleep(parseInt(process.env.SEALIOUS_DB_DELAY || "0") / 2); | ||||||
|  | 	return pick([ | ||||||
|  | 		"Never don't give up", | ||||||
|  | 		"Say no to yes. Say pizza to drugs", | ||||||
|  | 		"Bądź kreatwyny", | ||||||
|  | 		"Inside you there are two wolves", | ||||||
|  | 	]); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new (class motdRedirect extends Mountable { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mount(router: Router, path: string) { | ||||||
|  | 		router.get(path, async (ctx) => { | ||||||
|  | 			ctx.body = { motd: await getMOTD() }; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,48 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ListPosts"; | ||||||
|  | 
 | ||||||
|  | const filterFields = {}; | ||||||
|  | 
 | ||||||
|  | export default new (class ListPostsPage extends SealiousItemListPage< | ||||||
|  | 	typeof Posts, | ||||||
|  | 	typeof BaseListPageFields | ||||||
|  | > { | ||||||
|  | 	fields = BaseListPageFields; | ||||||
|  | 
 | ||||||
|  | 	filterFields = filterFields; | ||||||
|  | 	filterControls = []; | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		return html( | ||||||
|  | 			ctx, | ||||||
|  | 			"Posts", | ||||||
|  | 			tempstream/* HTML */ `<div>
 | ||||||
|  | 				<h2>Posts List</h2> | ||||||
|  | 				<table> | ||||||
|  | 					<thead> | ||||||
|  | 						<th>id</th> | ||||||
|  | 						${Object.keys(Posts.fields).map( | ||||||
|  | 							(fieldname) => `<th>${fieldname}</th>` | ||||||
|  | 						)} | ||||||
|  | 					</thead> | ||||||
|  | 					<tbody> | ||||||
|  | 						${super.render(ctx)} | ||||||
|  | 					</tbody> | ||||||
|  | 				</table> | ||||||
|  | 			</div>` | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async renderItem(_: Context, item: CollectionItem<typeof Posts>) { | ||||||
|  | 		return tempstream`<tr><td>${item.id}</td>${Object.keys(Posts.fields).map( | ||||||
|  | 			(fieldname: keyof typeof Posts["fields"]) => | ||||||
|  | 				tempstream`<td>${item.get(fieldname)}</td>` | ||||||
|  | 		)}</tr>`;
 | ||||||
|  | 	} | ||||||
|  | })(Posts); | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | import { withProdApp } from "../test_utils/with-prod-app"; | ||||||
|  | import { LONG_TEST_TIMEOUT, webhintURL } from "../test_utils/webhint"; | ||||||
|  | import { ListPostsURL } from "./urls"; | ||||||
|  | 
 | ||||||
|  | describe("ListPosts", () => { | ||||||
|  | 	it("doesn't crash", async function () { | ||||||
|  | 		this.timeout(LONG_TEST_TIMEOUT); | ||||||
|  | 		return withProdApp(async ({ base_url, rest_api }) => { | ||||||
|  | 			await rest_api.get(ListPostsURL); | ||||||
|  | 			await webhintURL(base_url + ListPostsURL); | ||||||
|  | 			// alternatively you can use webhintHTML for faster but less precise scans
 | ||||||
|  | 			// or for scanning responses of requests that use some form of authorization:
 | ||||||
|  | 			// const response = await rest_api.get(ListPostsURL);
 | ||||||
|  | 			// await webhintHTML(response);
 | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -0,0 +1,11 @@ | |||||||
|  | "use strict"; | ||||||
|  | exports.__esModule = true; | ||||||
|  | var jsx_runtime_1 = require("react/jsx-runtime"); | ||||||
|  | var react_1 = require("react"); | ||||||
|  | var e = react_1["default"].createElement; | ||||||
|  | var domContainer = document.querySelector("#app"); | ||||||
|  | var root = ReactDOM.createRoot(domContainer); | ||||||
|  | function app() { | ||||||
|  |     return (0, jsx_runtime_1.jsx)("div", { children: "Hello!" }); | ||||||
|  | } | ||||||
|  | root.render(e(app)); | ||||||
| @ -0,0 +1,33 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Page } from "@sealcode/sealgen"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "React"; | ||||||
|  | 
 | ||||||
|  | export default new (class ReactPage extends Page { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		return /* HTML */ `<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>React version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 					<script | ||||||
|  | 						src="https://unpkg.com/react@18/umd/react.production.min.js" | ||||||
|  | 						crossorigin | ||||||
|  | 					></script> | ||||||
|  | 					<script | ||||||
|  | 						src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" | ||||||
|  | 						crossorigin | ||||||
|  | 					></script> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app"></div> | ||||||
|  | 				</body> | ||||||
|  | 				<script src="/dist/react.js"></script> | ||||||
|  | 			</html>`;
 | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,95 @@ | |||||||
|  | "use strict"; | ||||||
|  | 
 | ||||||
|  | const e = React.createElement; | ||||||
|  | 
 | ||||||
|  | const domContainer = document.querySelector("#app"); | ||||||
|  | const root = ReactDOM.createRoot(domContainer); | ||||||
|  | 
 | ||||||
|  | function footer() { | ||||||
|  | 	const [motd, setMotd] = React.useState(""); | ||||||
|  | 	React.useEffect(async () => { | ||||||
|  | 		const response = await fetch("/motd/"); | ||||||
|  | 		const { motd } = await response.json(); | ||||||
|  | 		setMotd(motd); | ||||||
|  | 	}, []); | ||||||
|  | 	return <div className="footer">Copyright 2022. {motd}</div>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="navbar"> | ||||||
|  | 			<div className="logo">My app</div> | ||||||
|  | 			<div> | ||||||
|  | 				<a href="/react/">react</a> <a href="/ssr1/">ssr1</a>{" "} | ||||||
|  | 				<a href="/ssr2/">ssr2</a> <a href="/ssr25/">ssr2.5</a>{" "} | ||||||
|  | 				<a href="/ssr3/">ssr3</a> <a href="/ssr4/">ssr4</a>{" "} | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return ( | ||||||
|  | 		<div className="sidebar"> | ||||||
|  | 			<ul className="filters"> | ||||||
|  | 				<li> | ||||||
|  | 					<input type="checkbox" /> Filtr1 | ||||||
|  | 				</li> | ||||||
|  | 				<li> | ||||||
|  | 					<input type="checkbox" /> Filtr2 | ||||||
|  | 				</li> | ||||||
|  | 				<li> | ||||||
|  | 					<input type="checkbox" /> Filtr3 | ||||||
|  | 				</li> | ||||||
|  | 				<li> | ||||||
|  | 					<input type="checkbox" /> Filtr4 | ||||||
|  | 				</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem({ item }) { | ||||||
|  | 	return ( | ||||||
|  | 		<li className="item"> | ||||||
|  | 			<h3>{item.title}</h3> | ||||||
|  | 			<p>{item.description}</p> | ||||||
|  | 		</li> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function content() { | ||||||
|  | 	const [isLoading, setIsLoading] = React.useState(true); | ||||||
|  | 	const [items, setItems] = React.useState([]); | ||||||
|  | 	React.useEffect(async function () { | ||||||
|  | 		const response = await fetch("/api/v1/collections/posts"); | ||||||
|  | 		const { items } = await response.json(); | ||||||
|  | 		setItems(items); | ||||||
|  | 		setIsLoading(false); | ||||||
|  | 	}, []); | ||||||
|  | 	if (isLoading) { | ||||||
|  | 		return ( | ||||||
|  | 			<div className="content"> | ||||||
|  | 				<div className="loading">Loading.....</div> | ||||||
|  | 			</div> | ||||||
|  | 		); | ||||||
|  | 	} | ||||||
|  | 	return ( | ||||||
|  | 		<div className="content"> | ||||||
|  | 			<ul className="items">{items.map((item) => e(renderItem, { item }))}</ul> | ||||||
|  | 		</div> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function app() { | ||||||
|  | 	return ( | ||||||
|  | 		<> | ||||||
|  | 			{e(navbar)} | ||||||
|  | 			{e(sidebar)} | ||||||
|  | 			{e(content)} | ||||||
|  | 			{e(footer)} | ||||||
|  | 		</> | ||||||
|  | 	); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | root.render(e(app)); | ||||||
| @ -0,0 +1,27 @@ | |||||||
|  | // DO NOT EDIT! This file is generated automaticaly with npm run generate-routes
 | ||||||
|  | 
 | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | import { mount } from "@sealcode/sealgen"; | ||||||
|  | import * as URLs from "./urls"; | ||||||
|  | 
 | ||||||
|  | import { default as Hello } from "./hello.page"; | ||||||
|  | import { default as motd } from "./motd.post"; | ||||||
|  | import { default as ListPosts } from "./posts.list"; | ||||||
|  | import { default as React } from "./react.page"; | ||||||
|  | import { default as ssr1 } from "./ssr1.page"; | ||||||
|  | import { default as ssr2 } from "./ssr2.post"; | ||||||
|  | import { default as ssr25 } from "./ssr25.page"; | ||||||
|  | import { default as ssr3 } from "./ssr3.page"; | ||||||
|  | import { default as ssr4 } from "./ssr4.page"; | ||||||
|  | 
 | ||||||
|  | export default function mountAutoRoutes(router: Router) { | ||||||
|  | 	mount(router, URLs.HelloURL, Hello); | ||||||
|  | 	mount(router, URLs.motdURL, motd); | ||||||
|  | 	mount(router, URLs.ListPostsURL, ListPosts); | ||||||
|  | 	mount(router, URLs.ReactURL, React); | ||||||
|  | 	mount(router, URLs.ssr1URL, ssr1); | ||||||
|  | 	mount(router, URLs.ssr2URL, ssr2); | ||||||
|  | 	mount(router, URLs.ssr25URL, ssr25); | ||||||
|  | 	mount(router, URLs.ssr3URL, ssr3); | ||||||
|  | 	mount(router, URLs.ssr4URL, ssr4); | ||||||
|  | } | ||||||
| @ -0,0 +1,80 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Page } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { getMOTD } from "./motd.post"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr1"; | ||||||
|  | 
 | ||||||
|  | async function footer() { | ||||||
|  | 	return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 		<div> | ||||||
|  | 			<a href="/react/">react</a> | ||||||
|  | 			<a href="/ssr1/">ssr1</a> | ||||||
|  | 			<a href="/ssr2/">ssr2</a> | ||||||
|  | 			<a href="/ssr25/">ssr2.5</a> | ||||||
|  | 			<a href="/ssr3/">ssr3</a> | ||||||
|  | 			<a href="/ssr4/">ssr4</a> | ||||||
|  | 		</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filtr1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() { | ||||||
|  | 	const { items } = await Posts.suList().fetch(); | ||||||
|  | 	return /* HTML */ `<div class="content">
 | ||||||
|  | 		<ul class="items"> | ||||||
|  | 			${items.map((item) => renderItem(item)).join("")} | ||||||
|  | 		</ul> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new (class ssr1Page extends Page { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		return /* HTML */ `<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR1 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app"> | ||||||
|  | 						${navbar()}${sidebar()}${await content()}${await footer()} | ||||||
|  | 					</div> | ||||||
|  | 				</body> | ||||||
|  | 			</html>`;
 | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,86 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Page, sleep } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { Readable, Transform } from "stream"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr2"; | ||||||
|  | 
 | ||||||
|  | function footer() { | ||||||
|  | 	return /* HTML */ `<div class="footer">Copyright 2022</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filtr1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() { | ||||||
|  | 	const { items } = await Posts.suList().fetch(); | ||||||
|  | 	console.log("Returning content!"); | ||||||
|  | 	return /* HTML */ `<div class="content">
 | ||||||
|  | 		<ul class="items"> | ||||||
|  | 			${items.map((item) => renderItem(item)).join("")} | ||||||
|  | 		</ul> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new (class ssr1Page extends Page { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		const stream = new Readable(); | ||||||
|  | 		stream.pause(); | ||||||
|  | 		ctx.respond = false; | ||||||
|  | 		return new Promise(async (resolve) => { | ||||||
|  | 			await sleep(1); | ||||||
|  | 			resolve(stream); | ||||||
|  | 			stream.resume(); | ||||||
|  | 			stream.push(`<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR2 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | <div id="app">`);
 | ||||||
|  | 			stream.push(navbar()); | ||||||
|  | 			stream.push(sidebar()); | ||||||
|  | 			stream.pause(); | ||||||
|  | 			await sleep(1); | ||||||
|  | 			console.log(stream); | ||||||
|  | 			stream.push(await content()); | ||||||
|  | 			stream.resume(); | ||||||
|  | 			stream.push(footer()); | ||||||
|  | 			stream.push(null); | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,84 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Mountable } from "@sealcode/sealgen"; | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | import { sleep } from "../util"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { getMOTD } from "./motd.post"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr2"; | ||||||
|  | 
 | ||||||
|  | async function footer() { | ||||||
|  | 	return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 		<div> | ||||||
|  | 			<a href="/react/">react</a> | ||||||
|  | 			<a href="/ssr1/">ssr1</a> | ||||||
|  | 			<a href="/ssr2/">ssr2</a> | ||||||
|  | 			<a href="/ssr25/">ssr2.5</a> | ||||||
|  | 			<a href="/ssr3/">ssr3</a> | ||||||
|  | 			<a href="/ssr4/">ssr4</a> | ||||||
|  | 		</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filtr1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() {} | ||||||
|  | 
 | ||||||
|  | export default new (class ssr2Redirect extends Mountable { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mount(router: Router, path: string) { | ||||||
|  | 		router.get(path, async (ctx) => { | ||||||
|  | 			ctx.res.write(`<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR2 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app">`);
 | ||||||
|  | 			ctx.res.write(navbar()); | ||||||
|  | 			ctx.res.write(sidebar()); | ||||||
|  | 			ctx.res.write(`<div class="content">`); | ||||||
|  | 			// ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`);
 | ||||||
|  | 			ctx.res.write(`<div class="loading">Loading.....</div>`); | ||||||
|  | 			const { items } = await Posts.suList().fetch(); | ||||||
|  | 			ctx.res.write(/* HTML */ ` <ul class="items">
 | ||||||
|  | 				${items.map((item) => renderItem(item)).join("")} | ||||||
|  | 			</ul>`);
 | ||||||
|  | 			ctx.res.write("</div>"); | ||||||
|  | 			ctx.res.write(await footer()); | ||||||
|  | 			ctx.res.end(); | ||||||
|  | 			ctx.body = ctx.res; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,84 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Mountable } from "@sealcode/sealgen"; | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | 
 | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { getMOTD } from "./motd.post"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr25"; | ||||||
|  | 
 | ||||||
|  | async function footer() { | ||||||
|  | 	return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 		<div> | ||||||
|  | 			<a href="/react/">react</a> | ||||||
|  | 			<a href="/ssr1/">ssr1</a> | ||||||
|  | 			<a href="/ssr2/">ssr2</a> | ||||||
|  | 			<a href="/ssr25/">ssr2.5</a> | ||||||
|  | 			<a href="/ssr3/">ssr3</a> | ||||||
|  | 			<a href="/ssr4/">ssr4</a> | ||||||
|  | 		</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filtr1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() {} | ||||||
|  | 
 | ||||||
|  | export default new (class ssr2Redirect extends Mountable { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mount(router: Router, path: string) { | ||||||
|  | 		router.get(path, async (ctx) => { | ||||||
|  | 			ctx.res.write(`<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR2.5 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app">`);
 | ||||||
|  | 			ctx.res.write(navbar()); | ||||||
|  | 			ctx.res.write(sidebar()); | ||||||
|  | 			ctx.res.write(`<div class="content">`); | ||||||
|  | 			ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`); | ||||||
|  | 			ctx.res.write(`<div class="loading">Loading.....</div>`); | ||||||
|  | 			const { items } = await Posts.suList().fetch(); | ||||||
|  | 			ctx.res.write(/* HTML */ ` <ul class="items">
 | ||||||
|  | 				${items.map((item) => renderItem(item)).join("")} | ||||||
|  | 			</ul>`);
 | ||||||
|  | 			ctx.res.write("</div>"); | ||||||
|  | 			ctx.res.write(await footer()); | ||||||
|  | 			ctx.res.end(); | ||||||
|  | 			ctx.body = ctx.res; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,83 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { Mountable } from "@sealcode/sealgen"; | ||||||
|  | import Router from "@koa/router"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { getMOTD } from "./motd.post"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr3"; | ||||||
|  | 
 | ||||||
|  | async function footer() { | ||||||
|  | 	return /* HTML */ `<div class="footer">Copyright 2022. ${await getMOTD()}</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 		<div> | ||||||
|  | 			<a href="/react/">react</a> | ||||||
|  | 			<a href="/ssr1/">ssr1</a> | ||||||
|  | 			<a href="/ssr2/">ssr2</a> | ||||||
|  | 			<a href="/ssr25/">ssr2.5</a> | ||||||
|  | 			<a href="/ssr3/">ssr3</a> | ||||||
|  | 			<a href="/ssr4/">ssr4</a> | ||||||
|  | 		</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filtr1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filtr4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() {} | ||||||
|  | 
 | ||||||
|  | export default new (class ssr2Redirect extends Mountable { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	mount(router: Router, path: string) { | ||||||
|  | 		router.get(path, async (ctx) => { | ||||||
|  | 			ctx.res.write(`<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR3 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app">`);
 | ||||||
|  | 			ctx.res.write(navbar()); | ||||||
|  | 			ctx.res.write(sidebar()); | ||||||
|  | 			ctx.res.write(await footer()); | ||||||
|  | 			ctx.res.write(`<div class="content">`); | ||||||
|  | 			ctx.res.write(`<style>.loading:not(:last-child){display: none}</style>`); | ||||||
|  | 			ctx.res.write(`<div class="loading">Loading.....</div>`); | ||||||
|  | 			const { items } = await Posts.suList().fetch(); | ||||||
|  | 			ctx.res.write(`<ul class="items">
 | ||||||
|  | 				${items.map((item) => renderItem(item)).join("")} | ||||||
|  | 			</ul>`);
 | ||||||
|  | 			ctx.res.write("</div>"); | ||||||
|  | 			ctx.res.end(); | ||||||
|  | 			ctx.body = ctx.res; | ||||||
|  | 		}); | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,90 @@ | |||||||
|  | import { Context } from "koa"; | ||||||
|  | import { tempstream } from "tempstream"; | ||||||
|  | import { Page } from "@sealcode/sealgen"; | ||||||
|  | import html from "../html"; | ||||||
|  | import { Posts } from "../collections/collections"; | ||||||
|  | import { CollectionItem } from "sealious"; | ||||||
|  | import { getMOTD } from "./motd.post"; | ||||||
|  | 
 | ||||||
|  | export const actionName = "ssr4"; | ||||||
|  | 
 | ||||||
|  | function footer(message: string | Promise<string> = getMOTD()) { | ||||||
|  | 	return tempstream/* HTML */ `<div class="footer">Copyright 2022. ${message}</div>`; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function navbar() { | ||||||
|  | 	return /* HTML */ ` <div class="navbar">
 | ||||||
|  | 		<div class="logo">My app</div> | ||||||
|  | 		<div> | ||||||
|  | 			<a href="/react/">react</a> | ||||||
|  | 			<a href="/ssr1/">ssr1</a> | ||||||
|  | 			<a href="/ssr2/">ssr2</a> | ||||||
|  | 			<a href="/ssr25/">ssr2.5</a> | ||||||
|  | 			<a href="/ssr3/">ssr3</a> | ||||||
|  | 			<a href="/ssr4/">ssr4</a> | ||||||
|  | 		</div> | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function sidebar() { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<div class="sidebar"> | ||||||
|  | 			<ul class="filters"> | ||||||
|  | 				<li><input type="checkbox" /> Filter1</li> | ||||||
|  | 				<li><input type="checkbox" /> Filter2</li> | ||||||
|  | 				<li><input type="checkbox" /> Filter3</li> | ||||||
|  | 				<li><input type="checkbox" /> Filter4</li> | ||||||
|  | 			</ul> | ||||||
|  | 		</div> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function renderItem(item: CollectionItem<typeof Posts>) { | ||||||
|  | 	return /* HTML */ ` | ||||||
|  | 		<li class="item"> | ||||||
|  | 			<h3>${item.get("title")}</h3> | ||||||
|  | 			<p>${item.get("description")}</p> | ||||||
|  | 		</li> | ||||||
|  | 	`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function content() { | ||||||
|  | 	const items_promise = Posts.suList().fetch(); | ||||||
|  | 	return tempstream/* HTML */ `<div class="content">
 | ||||||
|  | 		<style> | ||||||
|  | 			.loading:not(:last-child) { | ||||||
|  | 				display: none; | ||||||
|  | 			} | ||||||
|  | 		</style> | ||||||
|  | 		<div class="loading">Loading.....</div> | ||||||
|  | 
 | ||||||
|  | 		${items_promise.then( | ||||||
|  | 			({ items }) => | ||||||
|  | 				tempstream/* HTML */ `<ul class="items">
 | ||||||
|  | 					${items.map((item) => renderItem(item))} | ||||||
|  | 				</ul>` | ||||||
|  | 		)} | ||||||
|  | 	</div>`;
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default new (class ssr1Page extends Page { | ||||||
|  | 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||||
|  | 	async canAccess(_: Context) { | ||||||
|  | 		return { canAccess: true, message: "" }; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	async render(ctx: Context) { | ||||||
|  | 		return tempstream/* HTML */ `<!DOCTYPE html>
 | ||||||
|  | 			<html> | ||||||
|  | 				<head> | ||||||
|  | 					<title>SSR4 version</title> | ||||||
|  | 					<link href="/dist/style.css" rel="stylesheet" type="text/css" /> | ||||||
|  | 				</head> | ||||||
|  | 				<body> | ||||||
|  | 					<div id="app"> | ||||||
|  | 						${navbar()}${sidebar()}${footer("")}${content()}${footer()} | ||||||
|  | 					</div> | ||||||
|  | 				</body> | ||||||
|  | 			</html>`;
 | ||||||
|  | 	} | ||||||
|  | })(); | ||||||
| @ -0,0 +1,9 @@ | |||||||
|  | export const HelloURL = "/hello/"; | ||||||
|  | export const motdURL = "/motd/"; | ||||||
|  | export const ListPostsURL = "/posts/"; | ||||||
|  | export const ReactURL = "/react/"; | ||||||
|  | export const ssr1URL = "/ssr1/"; | ||||||
|  | export const ssr2URL = "/ssr2/"; | ||||||
|  | export const ssr25URL = "/ssr25/"; | ||||||
|  | export const ssr3URL = "/ssr3/"; | ||||||
|  | export const ssr4URL = "/ssr4/"; | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | describe("sample test", () => { | ||||||
|  | 	it("always passes", () => { | ||||||
|  | 		return true; | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
| @ -0,0 +1,32 @@ | |||||||
|  | import { Users } from "../collections/collections"; | ||||||
|  | import { CollectionItem, TestUtils } from "sealious"; | ||||||
|  | import TheApp from "../app"; | ||||||
|  | 
 | ||||||
|  | type Unpromisify<T> = T extends Promise<infer R> ? R : T; | ||||||
|  | 
 | ||||||
|  | export function createAUser(app: TheApp, username: string) { | ||||||
|  | 	return app.collections.users.suCreate({ | ||||||
|  | 		username, | ||||||
|  | 		email: `${username}@example.com`, | ||||||
|  | 		password: "password", | ||||||
|  | 		roles: [], | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function createAdmin( | ||||||
|  | 	app: TheApp, | ||||||
|  | 	rest_api: TestUtils.MockRestApi | ||||||
|  | ): Promise< | ||||||
|  | 	[CollectionItem<typeof Users>, Unpromisify<ReturnType<typeof rest_api.login>>] | ||||||
|  | > { | ||||||
|  | 	const user = await createAUser(app, "super_user"); | ||||||
|  | 	await app.collections["user-roles"].suCreate({ | ||||||
|  | 		user: user.id, | ||||||
|  | 		role: "admin", | ||||||
|  | 	}); | ||||||
|  | 	const session = await rest_api.login({ | ||||||
|  | 		username: "super_user", | ||||||
|  | 		password: "password", | ||||||
|  | 	}); | ||||||
|  | 	return [user, session]; | ||||||
|  | } | ||||||
| @ -0,0 +1,38 @@ | |||||||
|  | import _locreq from "locreq"; | ||||||
|  | const locreq = _locreq(__dirname); | ||||||
|  | import { spawn } from "child_process"; | ||||||
|  | import { hasShape, is, predicates } from "@sealcode/ts-predicates"; | ||||||
|  | import { promises as fs } from "fs"; | ||||||
|  | 
 | ||||||
|  | export const LONG_TEST_TIMEOUT = 30 * 1000; | ||||||
|  | 
 | ||||||
|  | export async function webhintURL(url: string, config = locreq.resolve(".hintrc")) { | ||||||
|  | 	// eslint-disable-next-line no-console
 | ||||||
|  | 	console.log("scanning with webhint....", url); | ||||||
|  | 	try { | ||||||
|  | 		const subprocess = spawn( | ||||||
|  | 			"node", | ||||||
|  | 			[locreq.resolve("node_modules/.bin/hint"), "--config", config, url], | ||||||
|  | 			{ | ||||||
|  | 				stdio: "inherit", | ||||||
|  | 				shell: true, | ||||||
|  | 			} | ||||||
|  | 		); | ||||||
|  | 		await new Promise<void>((resolve, reject) => { | ||||||
|  | 			subprocess.on("close", (code) => | ||||||
|  | 				code === 0 ? resolve() : reject(new Error("Webhint tests failed")) | ||||||
|  | 			); | ||||||
|  | 		}); | ||||||
|  | 	} catch (e) { | ||||||
|  | 		if (is(e, predicates.object) && hasShape({ stdout: predicates.string }, e)) { | ||||||
|  | 			throw new Error(e.stdout); | ||||||
|  | 		} else { | ||||||
|  | 			throw e; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function webhintHTML(html: string) { | ||||||
|  | 	await fs.writeFile("/tmp/index.html", html); | ||||||
|  | 	await webhintURL("/tmp/index.html", locreq.resolve(".hintrc.local.json")); | ||||||
|  | } | ||||||
| @ -0,0 +1,61 @@ | |||||||
|  | import TheApp from "../app"; | ||||||
|  | import { mainRouter } from "../routes"; | ||||||
|  | import _locreq from "locreq"; | ||||||
|  | 
 | ||||||
|  | const locreq = _locreq(__dirname); | ||||||
|  | import Sealious, { SMTPMailer } from "sealious"; | ||||||
|  | import { TestUtils } from "sealious"; | ||||||
|  | 
 | ||||||
|  | export async function withProdApp( | ||||||
|  | 	callback: (args: { | ||||||
|  | 		app: TheApp; | ||||||
|  | 		base_url: string; | ||||||
|  | 		rest_api: TestUtils.MockRestApi; | ||||||
|  | 		mail_api: TestUtils.MailcatcherAPI; | ||||||
|  | 	}) => Promise<void> | ||||||
|  | ) { | ||||||
|  | 	const app = new TheApp(); | ||||||
|  | 	const port = 9999; | ||||||
|  | 
 | ||||||
|  | 	app.config["www-server"].port = port; | ||||||
|  | 	app.config.datastore_mongo = { | ||||||
|  | 		host: "db", | ||||||
|  | 		port: 27017, | ||||||
|  | 		db_name: "sealious-app-test", | ||||||
|  | 	}; | ||||||
|  | 	app.config.logger.level = <const>"none"; | ||||||
|  | 	app.mailer = new SMTPMailer({ | ||||||
|  | 		host: "mailcatcher", | ||||||
|  | 		port: 1025, | ||||||
|  | 		user: "any", | ||||||
|  | 		password: "any", | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	await app.start(); | ||||||
|  | 	mainRouter(app.HTTPServer.router); | ||||||
|  | 
 | ||||||
|  | 	app.HTTPServer.addStaticRoute("/", locreq.resolve("public")); | ||||||
|  | 
 | ||||||
|  | 	const base_url = `http://127.0.0.1:${port}`; | ||||||
|  | 	const mail_api = new TestUtils.MailcatcherAPI("http://mailcatcher:1080", app); | ||||||
|  | 	await mail_api.deleteAllInstanceEmails(); | ||||||
|  | 
 | ||||||
|  | 	async function stop() { | ||||||
|  | 		await app.removeAllData(); | ||||||
|  | 		await app.stop(); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	try { | ||||||
|  | 		await callback({ | ||||||
|  | 			app, | ||||||
|  | 			base_url, | ||||||
|  | 			rest_api: new TestUtils.MockRestApi(base_url), | ||||||
|  | 			mail_api, | ||||||
|  | 		}); | ||||||
|  | 		await stop(); | ||||||
|  | 	} catch (e) { | ||||||
|  | 		await stop(); | ||||||
|  | 		console.error(e); | ||||||
|  | 		throw e; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,20 @@ | |||||||
|  | { | ||||||
|  | 	"compilerOptions": { | ||||||
|  | 		"module": "CommonJS", | ||||||
|  | 		"moduleResolution": "node", | ||||||
|  | 		"noImplicitAny": true, | ||||||
|  | 		"noImplicitThis": true, | ||||||
|  | 		"strictNullChecks": true, | ||||||
|  | 		"target": "ES2019", | ||||||
|  | 		"esModuleInterop": true, | ||||||
|  | 		"lib": ["es6", "esnext"], | ||||||
|  | 		"outDir": "../../dist/back", | ||||||
|  | 		"keyofStringsOnly": true, | ||||||
|  | 		"checkJs": true, | ||||||
|  | 		"allowJs": true, | ||||||
|  | 		"resolveJsonModule": true, | ||||||
|  | 		"sourceMap": true, | ||||||
|  | 		"skipLibCheck": true | ||||||
|  | 	}, | ||||||
|  | 	"include": ["./**/*", "./*"] | ||||||
|  | } | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | import { BaseContext } from "koa"; | ||||||
|  | import qs from "qs"; | ||||||
|  | 
 | ||||||
|  | export async function sleep(time: number) { | ||||||
|  | 	return new Promise((resolve) => setTimeout(resolve, time)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type Awaited<T> = T extends Promise<infer U> ? U : T; | ||||||
|  | export type UnwrapArray<T> = T extends Array<infer U> ? U : T; | ||||||
|  | 
 | ||||||
|  | export function* naturalNumbers(min: number, max: number) { | ||||||
|  | 	for (let i = min; i <= max; i++) { | ||||||
|  | 		yield i; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function UrlWithNewParams( | ||||||
|  | 	ctx: BaseContext, | ||||||
|  | 	query_params: Record<string, unknown> | ||||||
|  | ): string { | ||||||
|  | 	return `${ctx.path}?${qs.stringify(query_params)}`; | ||||||
|  | } | ||||||
| @ -0,0 +1,6 @@ | |||||||
|  | import * as Turbo from "@hotwired/turbo"; | ||||||
|  | import { Application } from "stimulus"; | ||||||
|  | 
 | ||||||
|  | export { Turbo }; | ||||||
|  | 
 | ||||||
|  | const application = Application.start(); | ||||||
| @ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  | 	"compilerOptions": { | ||||||
|  | 		"module": "CommonJS", | ||||||
|  | 		"target": "ES6", | ||||||
|  | 		"lib": ["dom"] | ||||||
|  | 	}, | ||||||
|  | 	"include": ["./**/*", "./index.ts"] | ||||||
|  | } | ||||||
| @ -0,0 +1,4 @@ | |||||||
|  | // DO NOT EDIT! This file is generated automaticaly with npx sealgen generate-scss-includes | ||||||
|  | 
 | ||||||
|  | @import "../node_modules/@sealcode/sealgen/src/forms/forms.scss"; | ||||||
|  | @import "back/routes/common/ui/input.scss"; | ||||||
| @ -0,0 +1,78 @@ | |||||||
|  | html { | ||||||
|  | 	background: #eee; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  | 	max-width: 1024px; | ||||||
|  | 	margin: 1rem auto; | ||||||
|  | 	background: white; | ||||||
|  | 	padding: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .delete-button { | ||||||
|  | 	height: 1rem; | ||||||
|  | 	padding: 0; | ||||||
|  | 	line-height: 0; | ||||||
|  | 	padding: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .nav-logo { | ||||||
|  | 	display: flex; | ||||||
|  | 	align-items: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @import "includes.scss"; | ||||||
|  | 
 | ||||||
|  | #app { | ||||||
|  | 	grid-template-areas: | ||||||
|  | 		"navbar navbar" | ||||||
|  | 		"sidebar content" | ||||||
|  | 		"footer footer"; | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-template-columns: 200px 1fr; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .footer { | ||||||
|  | 	grid-area: footer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .navbar { | ||||||
|  | 	grid-area: navbar; | ||||||
|  | 	.logo { | ||||||
|  | 		font-size: 60px; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .sidebar { | ||||||
|  | 	grid-area: sidebar; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @keyframes enter { | ||||||
|  | 	from { | ||||||
|  | 		opacity: 0%; | ||||||
|  | 	} | ||||||
|  | 	to { | ||||||
|  | 		opacity: 100%; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .items { | ||||||
|  | 	display: grid; | ||||||
|  | 	grid-gap: 20px; | ||||||
|  | 	grid-template-columns: repeat(auto-fit, minmax(161px, 241px)); | ||||||
|  | 
 | ||||||
|  | 	li { | ||||||
|  | 		background-color: white; | ||||||
|  | 		box-shadow: 1px 1px 5px 0px #00000059; | ||||||
|  | 		list-style: none; | ||||||
|  | 		padding: 0 20px; | ||||||
|  | 
 | ||||||
|  | 		// animation: enter 200ms; | ||||||
|  | 		// animation-fill-mode: both; | ||||||
|  | 		// @for $i from 1 through 20 { | ||||||
|  | 		// 	&:nth-child(#{$i}) { | ||||||
|  | 		// 		animation-delay: #{$i * 50}ms; | ||||||
|  | 		// 	} | ||||||
|  | 		// } | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -0,0 +1,37 @@ | |||||||
|  | const path = require("path"); | ||||||
|  | 
 | ||||||
|  | module.exports = [ | ||||||
|  | 	{ | ||||||
|  | 		name: "front-end-components", | ||||||
|  | 		entry: { | ||||||
|  | 			bundle: "./src/front/index.ts", | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		output: { | ||||||
|  | 			filename: "[name].js", | ||||||
|  | 			path: path.resolve(__dirname, "public/dist"), | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		mode: "production", | ||||||
|  | 		devtool: "source-map", | ||||||
|  | 
 | ||||||
|  | 		resolve: { | ||||||
|  | 			extensions: [".ts", ".js"], | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		module: { | ||||||
|  | 			rules: [ | ||||||
|  | 				{ | ||||||
|  | 					test: /\.js$/, | ||||||
|  | 					exclude: [/node_modules/], | ||||||
|  | 					use: [{ loader: "babel-loader" }], | ||||||
|  | 				}, | ||||||
|  | 				{ | ||||||
|  | 					test: /\.ts$/, | ||||||
|  | 					exclude: [/node_modules/], | ||||||
|  | 					use: [{ loader: "ts-loader" }], | ||||||
|  | 				}, | ||||||
|  | 			], | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | ]; | ||||||
					Loading…
					
					
				
		Reference in New Issue