Initial commit
This commit is contained in:
commit
6bdf7a32a9
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"directory": ["./src/controllers/rest", "./src/controllers/pages"],
|
||||||
|
"exclude": ["__mock__", "__mocks__", ".spec.ts"],
|
||||||
|
"delete": true
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
Dockerfile
|
||||||
|
.env.local
|
||||||
|
.env.development
|
|
@ -0,0 +1,9 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -0,0 +1,4 @@
|
||||||
|
dist
|
||||||
|
processes.config.js
|
||||||
|
node_modules
|
||||||
|
coverage
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"prettier",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-inferrable-types": 0,
|
||||||
|
"@typescript-eslint/no-unused-vars": 2,
|
||||||
|
"@typescript-eslint/no-var-requires": 0
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
### Node template
|
||||||
|
.DS_Store
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
.npmrc
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Typings
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Typescript
|
||||||
|
src/**/*.js
|
||||||
|
src/**/*.js.map
|
||||||
|
test/**/*.js
|
||||||
|
test/**/*.js.map
|
||||||
|
|
||||||
|
# Test
|
||||||
|
/.tmp
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Project
|
||||||
|
/public
|
||||||
|
/dist
|
||||||
|
|
||||||
|
#env
|
||||||
|
.env.local
|
||||||
|
.env.development
|
||||||
|
|
||||||
|
database.sqlite
|
|
@ -0,0 +1,5 @@
|
||||||
|
dist
|
||||||
|
docs
|
||||||
|
node_modules
|
||||||
|
*-lock.json
|
||||||
|
*.lock
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"printWidth": 140,
|
||||||
|
"singleQuote": false,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
nodejs 20.17.0
|
||||||
|
sqlite 3.46.1
|
|
@ -0,0 +1,48 @@
|
||||||
|
###############################################################################
|
||||||
|
###############################################################################
|
||||||
|
## _______ _____ ______ _____ ##
|
||||||
|
## |__ __/ ____| ____| __ \ ##
|
||||||
|
## | | | (___ | |__ | | | | ##
|
||||||
|
## | | \___ \| __| | | | | ##
|
||||||
|
## | | ____) | |____| |__| | ##
|
||||||
|
## |_| |_____/|______|_____/ ##
|
||||||
|
## ##
|
||||||
|
## description : Dockerfile for TsED Application ##
|
||||||
|
## author : TsED team ##
|
||||||
|
## date : 2023-12-11 ##
|
||||||
|
## version : 3.0 ##
|
||||||
|
## ##
|
||||||
|
###############################################################################
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
ARG NODE_VERSION=20.10.0
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine as build
|
||||||
|
WORKDIR /opt
|
||||||
|
|
||||||
|
COPY package.json package-lock.json tsconfig.json tsconfig.compile.json .barrelsby.json ./
|
||||||
|
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY ./src ./src
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:${NODE_VERSION}-alpine as runtime
|
||||||
|
ENV WORKDIR /opt
|
||||||
|
WORKDIR $WORKDIR
|
||||||
|
|
||||||
|
RUN apk update && apk add build-base git curl
|
||||||
|
RUN npm install -g pm2
|
||||||
|
|
||||||
|
COPY --from=build /opt .
|
||||||
|
|
||||||
|
RUN npm ci --omit=dev --ignore-scripts
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8081
|
||||||
|
ENV PORT 8081
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
|
CMD ["pm2-runtime", "start", "processes.config.js", "--env", "production"]
|
|
@ -0,0 +1,60 @@
|
||||||
|
<p style="text-align: center" align="center">
|
||||||
|
<a href="https://tsed.io" target="_blank"><img src="https://tsed.io/tsed-og.png" width="200" alt="Ts.ED logo"/></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h1>Ts.ED - intended-server</h1>
|
||||||
|
<br />
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://cli.tsed.io/">Website</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://cli.tsed.io/getting-started.html">Getting started</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://api.tsed.io/rest/slack/tsedio/tsed">Slack</a>
|
||||||
|
<span> • </span>
|
||||||
|
<a href="https://twitter.com/TsED_io">Twitter</a>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
> An awesome project based on Ts.ED framework
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
> **Important!** Ts.ED requires Node >= 14, Express >= 4 and TypeScript >= 4.
|
||||||
|
|
||||||
|
```batch
|
||||||
|
# install dependencies
|
||||||
|
$ npm install
|
||||||
|
|
||||||
|
# serve
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# build for production
|
||||||
|
$ npm run build
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
# build docker image
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
# start docker image
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Barrelsby
|
||||||
|
|
||||||
|
This project uses [barrelsby](https://www.npmjs.com/package/barrelsby) to generate index files to import the controllers.
|
||||||
|
|
||||||
|
Edit `.barreslby.json` to customize it:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"directory": ["./src/controllers/rest", "./src/controllers/pages"],
|
||||||
|
"exclude": ["__mock__", "__mocks__", ".spec.ts"],
|
||||||
|
"delete": true
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: "3.5"
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
args:
|
||||||
|
- http_proxy
|
||||||
|
- https_proxy
|
||||||
|
- no_proxy
|
||||||
|
image: intended-server/server:latest
|
||||||
|
ports:
|
||||||
|
- "8081:8081"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,100 @@
|
||||||
|
{
|
||||||
|
"name": "intended-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run barrels && tsc --project tsconfig.compile.json",
|
||||||
|
"barrels": "barrelsby --config .barrelsby.json",
|
||||||
|
"start": "rimraf ./database.sqlite && npm run barrels && tsnd --inspect --exit-child --cls --ignore-watch node_modules --respawn --transpile-only src/index.ts",
|
||||||
|
"start:prod": "cross-env NODE_ENV=production node dist/index.js",
|
||||||
|
"test": "npm run test:lint && npm run test:coverage ",
|
||||||
|
"test:unit": "cross-env NODE_ENV=test vitest run",
|
||||||
|
"test:watch": "cross-env NODE_ENV=test vitest",
|
||||||
|
"test:coverage": "rimraf ./database.sqlite && cross-env NODE_ENV=test vitest run --coverage",
|
||||||
|
"typeorm": "typeorm-ts-node-commonjs",
|
||||||
|
"test:lint": "eslint '**/*.{ts,js}'",
|
||||||
|
"test:lint:fix": "eslint '**/*.{ts,js}' --fix",
|
||||||
|
"prettier": "prettier '**/*.{ts,js,json,md,yml,yaml}' --write"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tsed/ajv": "^7.83.0",
|
||||||
|
"@tsed/common": "^7.83.0",
|
||||||
|
"@tsed/core": "^7.83.0",
|
||||||
|
"@tsed/di": "^7.83.0",
|
||||||
|
"@tsed/engines": "^7.83.0",
|
||||||
|
"@tsed/exceptions": "^7.83.0",
|
||||||
|
"@tsed/json-mapper": "^7.83.0",
|
||||||
|
"@tsed/logger": "^6.7.5",
|
||||||
|
"@tsed/logger-file": "^6.7.5",
|
||||||
|
"@tsed/openspec": "^7.83.0",
|
||||||
|
"@tsed/passport": "^7.83.0",
|
||||||
|
"@tsed/platform-cache": "^7.83.0",
|
||||||
|
"@tsed/platform-exceptions": "^7.83.0",
|
||||||
|
"@tsed/platform-express": "^7.83.0",
|
||||||
|
"@tsed/platform-log-middleware": "^7.83.0",
|
||||||
|
"@tsed/platform-middlewares": "^7.83.0",
|
||||||
|
"@tsed/platform-params": "^7.83.0",
|
||||||
|
"@tsed/platform-response-filter": "^7.83.0",
|
||||||
|
"@tsed/platform-views": "^7.83.0",
|
||||||
|
"@tsed/schema": "^7.83.0",
|
||||||
|
"@tsed/swagger": "^7.83.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"barrelsby": "^2.8.1",
|
||||||
|
"body-parser": "^1.20.3",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"dotenv-expand": "^11.0.6",
|
||||||
|
"dotenv-flow": "^4.1.0",
|
||||||
|
"express": "^4.20.0",
|
||||||
|
"express-session": "^1.18.0",
|
||||||
|
"method-override": "^3.0.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@swc/core": "^1.7.24",
|
||||||
|
"@tsed/cli-plugin-eslint": "5.2.10",
|
||||||
|
"@tsed/cli-plugin-passport": "5.2.10",
|
||||||
|
"@tsed/cli-plugin-typeorm": "5.2.10",
|
||||||
|
"@tsed/cli-plugin-vitest": "5.2.10",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/cookie-parser": "^1.4.7",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/method-override": "^0.0.35",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
|
"@types/node": "^22.5.4",
|
||||||
|
"@types/passport": "^1.0.16",
|
||||||
|
"@types/sinon": "^17.0.3",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||||
|
"@typescript-eslint/parser": "^8.5.0",
|
||||||
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"sinon": "^18.0.1",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"tslib": "^2.7.0",
|
||||||
|
"typeorm": "^0.3.20",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"unplugin-swc": "^1.5.1",
|
||||||
|
"vitest": "^2.0.5"
|
||||||
|
},
|
||||||
|
"tsed": {
|
||||||
|
"convention": "conv_default",
|
||||||
|
"architecture": "arc_default",
|
||||||
|
"packageManager": "npm",
|
||||||
|
"runtime": "node"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const defaultLogFile = path.join(__dirname, "/logs/project-server.log");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: "api",
|
||||||
|
script: `./dist/index.js`,
|
||||||
|
cwd: "./",
|
||||||
|
exec_mode: "cluster",
|
||||||
|
instances: process.env.NODE_ENV === "test" ? 1 : process.env.NB_INSTANCES || 2,
|
||||||
|
autorestart: true,
|
||||||
|
max_memory_restart: process.env.MAX_MEMORY_RESTART || "750M",
|
||||||
|
out_file: defaultLogFile,
|
||||||
|
error_file: defaultLogFile,
|
||||||
|
merge_logs: true,
|
||||||
|
kill_timeout: 30000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { expect, describe, it, beforeAll, afterAll } from "vitest";
|
||||||
|
import { PlatformTest } from "@tsed/common";
|
||||||
|
import SuperTest from "supertest";
|
||||||
|
import { Server } from "./Server";
|
||||||
|
|
||||||
|
describe("Server", () => {
|
||||||
|
beforeAll(PlatformTest.bootstrap(Server));
|
||||||
|
afterAll(PlatformTest.reset);
|
||||||
|
|
||||||
|
it("should call GET /rest", async () => {
|
||||||
|
const request = SuperTest(PlatformTest.callback());
|
||||||
|
const response = await request.get("/rest").expect(404);
|
||||||
|
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
errors: [],
|
||||||
|
message: 'Resource "/rest" not found',
|
||||||
|
name: "NOT_FOUND",
|
||||||
|
status: 404
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { join } from "path";
|
||||||
|
import { Configuration, Inject } from "@tsed/di";
|
||||||
|
import { PlatformApplication } from "@tsed/common";
|
||||||
|
import "@tsed/platform-express"; // /!\ keep this import
|
||||||
|
import "@tsed/ajv";
|
||||||
|
import "@tsed/swagger";
|
||||||
|
import "@tsed/passport";
|
||||||
|
import { config } from "./config/index";
|
||||||
|
import * as rest from "./controllers/rest/index";
|
||||||
|
import * as pages from "./controllers/pages/index";
|
||||||
|
|
||||||
|
@Configuration({
|
||||||
|
...config,
|
||||||
|
acceptMimes: ["application/json"],
|
||||||
|
httpPort: process.env.PORT || 8083,
|
||||||
|
httpsPort: false, // CHANGE
|
||||||
|
disableComponentsScan: true,
|
||||||
|
ajv: {
|
||||||
|
returnsCoercedValues: true
|
||||||
|
},
|
||||||
|
mount: {
|
||||||
|
"/rest": [...Object.values(rest)],
|
||||||
|
"/": [...Object.values(pages)]
|
||||||
|
},
|
||||||
|
swagger: [
|
||||||
|
{
|
||||||
|
path: "/doc",
|
||||||
|
specVersion: "3.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
middlewares: [
|
||||||
|
"cors",
|
||||||
|
"cookie-parser",
|
||||||
|
"compression",
|
||||||
|
"method-override",
|
||||||
|
"json-parser",
|
||||||
|
{ use: "urlencoded-parser", options: { extended: true } },
|
||||||
|
{
|
||||||
|
use: "express-session",
|
||||||
|
options: {
|
||||||
|
secret: process.env.PASSPORT_SESSION_KEY || "mysecretkey",
|
||||||
|
resave: true,
|
||||||
|
saveUninitialized: true,
|
||||||
|
// maxAge: 36000,
|
||||||
|
cookie: {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
secure: false
|
||||||
|
// maxAge: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
views: {
|
||||||
|
root: join(process.cwd(), "../views"),
|
||||||
|
extensions: {
|
||||||
|
ejs: "ejs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exclude: ["**/*.spec.ts"]
|
||||||
|
})
|
||||||
|
export class Server {
|
||||||
|
@Inject()
|
||||||
|
protected app: PlatformApplication;
|
||||||
|
|
||||||
|
@Configuration()
|
||||||
|
protected settings: Configuration;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import dotenv from "dotenv-flow";
|
||||||
|
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || "development";
|
||||||
|
|
||||||
|
export const config = dotenv.config();
|
||||||
|
export const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
export const envs = process.env;
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { envs } from "./envs/index";
|
||||||
|
import loggerConfig from "./logger/index";
|
||||||
|
const pkg = JSON.parse(readFileSync("./package.json", { encoding: "utf8" }));
|
||||||
|
|
||||||
|
export const config: Partial<TsED.Configuration> = {
|
||||||
|
version: pkg.version,
|
||||||
|
envs,
|
||||||
|
logger: loggerConfig
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { $log, DILoggerOptions } from "@tsed/common";
|
||||||
|
import { isProduction } from "../envs/index";
|
||||||
|
|
||||||
|
if (isProduction) {
|
||||||
|
$log.appenders.set("stdout", {
|
||||||
|
type: "stdout",
|
||||||
|
levels: ["info", "debug"],
|
||||||
|
layout: {
|
||||||
|
type: "json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$log.appenders.set("stderr", {
|
||||||
|
levels: ["trace", "fatal", "error", "warn"],
|
||||||
|
type: "stderr",
|
||||||
|
layout: {
|
||||||
|
type: "json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default <DILoggerOptions>{
|
||||||
|
disableRoutesSummary: isProduction
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Constant, Controller } from "@tsed/di";
|
||||||
|
import { HeaderParams } from "@tsed/platform-params";
|
||||||
|
import { View } from "@tsed/platform-views";
|
||||||
|
import { SwaggerSettings } from "@tsed/swagger";
|
||||||
|
import { Hidden, Get, Returns } from "@tsed/schema";
|
||||||
|
|
||||||
|
@Hidden()
|
||||||
|
@Controller("/")
|
||||||
|
export class IndexController {
|
||||||
|
@Constant("swagger", [])
|
||||||
|
private swagger: SwaggerSettings[];
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@View("swagger.ejs")
|
||||||
|
@(Returns(200, String).ContentType("text/html"))
|
||||||
|
get(@HeaderParams("x-forwarded-proto") protocol: string, @HeaderParams("host") host: string) {
|
||||||
|
const hostUrl = `${protocol || "http"}://${host}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
BASE_URL: hostUrl,
|
||||||
|
docs: this.swagger.map((conf) => {
|
||||||
|
return {
|
||||||
|
url: hostUrl + conf.path,
|
||||||
|
...conf
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/**
|
||||||
|
* @file Automatically generated by barrelsby.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./IndexController";
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { expect, describe, it, afterAll, beforeAll } from "vitest";
|
||||||
|
import { PlatformTest } from "@tsed/common";
|
||||||
|
import SuperTest from "supertest";
|
||||||
|
import { LinkController } from "./LinkController";
|
||||||
|
import { Server } from "../../Server";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
|
describe("LinkController", () => {
|
||||||
|
beforeAll(
|
||||||
|
PlatformTest.bootstrap(Server, {
|
||||||
|
mount: {
|
||||||
|
"/rest": [LinkController]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
afterAll(PlatformTest.reset);
|
||||||
|
|
||||||
|
it("should call POST /rest/links and GET /rest/links/:id", async () => {
|
||||||
|
const request = SuperTest(PlatformTest.callback());
|
||||||
|
const username = `silentsilas-${randomUUID()}`;
|
||||||
|
const response = await request
|
||||||
|
.post("/rest/links")
|
||||||
|
.send({
|
||||||
|
service: "github",
|
||||||
|
serviceUsername: username
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
|
||||||
|
const response2 = await request.get(`/rest/users/${response.body.userId}`).expect(200);
|
||||||
|
|
||||||
|
expect(response.body.id).toBeTruthy();
|
||||||
|
expect(response.body.service).toEqual("github");
|
||||||
|
expect(response.body.serviceUsername).toEqual(username);
|
||||||
|
|
||||||
|
expect(response2.body.id).toEqual(response.body.userId);
|
||||||
|
expect(response2.body.service).toEqual("github");
|
||||||
|
expect(response2.body.serviceUsername).toEqual(username);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { BodyParams, PathParams } from "@tsed/platform-params";
|
||||||
|
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||||
|
import { Controller, Inject } from "@tsed/di";
|
||||||
|
import { Link } from "../../entities/link/Link";
|
||||||
|
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { User } from "../../entities/User";
|
||||||
|
import { CreateLinkDto } from "../../entities/link/CreateLinkDTO";
|
||||||
|
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
||||||
|
|
||||||
|
@Controller("/links")
|
||||||
|
export class LinkController {
|
||||||
|
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
||||||
|
|
||||||
|
@Post("/")
|
||||||
|
@Summary("Create a new link")
|
||||||
|
@Description("Creates a new link and associates it with a user")
|
||||||
|
@Returns(201, Link)
|
||||||
|
async create(@BodyParams() linkData: CreateLinkDto): Promise<Link> {
|
||||||
|
return executeWithRetry(async (queryRunner) => {
|
||||||
|
const userRepository = queryRunner.manager.getRepository(User);
|
||||||
|
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||||
|
|
||||||
|
let user = await userRepository.findOne({
|
||||||
|
where: {
|
||||||
|
service: linkData.service,
|
||||||
|
serviceUsername: linkData.serviceUsername
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = userRepository.create({
|
||||||
|
service: linkData.service,
|
||||||
|
serviceUsername: linkData.serviceUsername
|
||||||
|
});
|
||||||
|
user = await queryRunner.manager.save(User, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
const link = linkRepository.create({
|
||||||
|
...linkData,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
return queryRunner.manager.save(Link, link);
|
||||||
|
}, this.sqliteDataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Summary("Get all links")
|
||||||
|
@(Returns(200, Array).Of(Link))
|
||||||
|
async getList(): Promise<Link[]> {
|
||||||
|
return executeWithRetry(async (queryRunner) => {
|
||||||
|
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||||
|
return linkRepository.find({ relations: ["user"] });
|
||||||
|
}, this.sqliteDataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:id")
|
||||||
|
@Summary("Get a link by ID")
|
||||||
|
@Returns(200, Link)
|
||||||
|
async getOne(@PathParams("id") id: string): Promise<Link | null> {
|
||||||
|
return executeWithRetry(async (queryRunner) => {
|
||||||
|
const linkRepository = queryRunner.manager.getRepository(Link);
|
||||||
|
return linkRepository.findOne({ where: { id }, relations: ["user"] });
|
||||||
|
}, this.sqliteDataSource);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { expect, describe, it, beforeEach, afterEach, beforeAll } from "vitest";
|
||||||
|
import { PlatformTest } from "@tsed/common";
|
||||||
|
import { UserController } from "./UserController";
|
||||||
|
import { User } from "../../entities/User";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { Server } from "../../Server";
|
||||||
|
import { sqliteDatasource } from "src/datasources/SqliteDatasource";
|
||||||
|
|
||||||
|
describe("UserController", () => {
|
||||||
|
let controller: UserController;
|
||||||
|
|
||||||
|
beforeAll(PlatformTest.bootstrap(Server));
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
controller = await PlatformTest.invoke(UserController, [
|
||||||
|
{
|
||||||
|
token: DataSource,
|
||||||
|
use: sqliteDatasource
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
PlatformTest.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should get a user by ID", async () => {
|
||||||
|
const userId = uuidv4();
|
||||||
|
const user = new User();
|
||||||
|
user.id = userId;
|
||||||
|
user.service = "github";
|
||||||
|
user.serviceUsername = `silentsilas-${userId}`;
|
||||||
|
const repo = sqliteDatasource.getRepository(User);
|
||||||
|
await repo.save(user);
|
||||||
|
|
||||||
|
const result = await controller.getOne(userId);
|
||||||
|
|
||||||
|
expect(result).toEqual(user);
|
||||||
|
expect(result?.id).toEqual(userId);
|
||||||
|
expect(result?.service).toEqual("github");
|
||||||
|
expect(result?.serviceUsername).toEqual(`silentsilas-${userId}`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { expect, describe, it, beforeEach, afterEach } from "vitest";
|
||||||
|
import { PlatformTest } from "@tsed/common";
|
||||||
|
import { UserController } from "./UserController";
|
||||||
|
|
||||||
|
describe("UserController", () => {
|
||||||
|
beforeEach(PlatformTest.create);
|
||||||
|
afterEach(PlatformTest.reset);
|
||||||
|
|
||||||
|
it("works", () => {
|
||||||
|
const instance = PlatformTest.get<UserController>(UserController);
|
||||||
|
// const instance = PlatformTest.invoke<HelloWorldController>(HelloWorldController); // get fresh instance
|
||||||
|
|
||||||
|
expect(instance).toBeInstanceOf(UserController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets a list of users", async () => {
|
||||||
|
const instance = PlatformTest.get<UserController>(UserController);
|
||||||
|
|
||||||
|
const users = await instance.getList();
|
||||||
|
|
||||||
|
expect(users).toBeInstanceOf(Array);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { PathParams } from "@tsed/platform-params";
|
||||||
|
import { Description, Get, Post, Returns, Summary } from "@tsed/schema";
|
||||||
|
import { Controller, Inject } from "@tsed/di";
|
||||||
|
import { User } from "../../entities/User";
|
||||||
|
import { SqliteDatasource } from "../../datasources/SqliteDatasource";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { executeWithRetry } from "../../datasources/SqliteDatasource";
|
||||||
|
import { Forbidden } from "@tsed/exceptions";
|
||||||
|
|
||||||
|
@Controller("/users")
|
||||||
|
export class UserController {
|
||||||
|
constructor(@Inject(SqliteDatasource) private sqliteDataSource: DataSource) {}
|
||||||
|
|
||||||
|
// disable the create method and endpoint
|
||||||
|
@Post("/")
|
||||||
|
@Summary("Create a new user")
|
||||||
|
@Description("This endpoint can not be used. Users are created automatically when a new link is created.")
|
||||||
|
@Returns(403)
|
||||||
|
async create(): Promise<void> {
|
||||||
|
throw new Forbidden("Users are created automatically when a new link is created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/")
|
||||||
|
@Summary("Get all users")
|
||||||
|
@(Returns(200, Array).Of(User))
|
||||||
|
async getList(): Promise<User[]> {
|
||||||
|
return executeWithRetry(async (queryRunner) => {
|
||||||
|
return queryRunner.manager.find(User);
|
||||||
|
}, this.sqliteDataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("/:id")
|
||||||
|
@Summary("Get a user by ID")
|
||||||
|
@Returns(200, User)
|
||||||
|
async getOne(@PathParams("id") id: string): Promise<User | null> {
|
||||||
|
return executeWithRetry(async (queryRunner) => {
|
||||||
|
return queryRunner.manager.findOne(User, {
|
||||||
|
where: {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, this.sqliteDataSource);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
/**
|
||||||
|
* @file Automatically generated by barrelsby.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./LinkController";
|
||||||
|
export * from "./UserController";
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { expect, describe, it, beforeEach, afterEach } from "vitest";
|
||||||
|
import { PlatformTest } from "@tsed/common";
|
||||||
|
import { SqliteDatasource } from "./SqliteDatasource";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
|
||||||
|
describe("SqliteDatasource", () => {
|
||||||
|
beforeEach(PlatformTest.create);
|
||||||
|
afterEach(PlatformTest.reset);
|
||||||
|
|
||||||
|
it("should do something", () => {
|
||||||
|
const instance = PlatformTest.get<SqliteDatasource>(SqliteDatasource);
|
||||||
|
// const instance = PlatformTest.invoke<SqliteDatasource>(SqliteDatasource); // get fresh instance
|
||||||
|
|
||||||
|
expect(instance).toBeInstanceOf(DataSource);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { registerProvider } from "@tsed/di";
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import { Logger } from "@tsed/logger";
|
||||||
|
import { User } from "../entities/User";
|
||||||
|
import { Link } from "../entities/link/Link";
|
||||||
|
import { QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export const SqliteDatasource = Symbol.for("SqliteDatasource");
|
||||||
|
export type SqliteDatasource = DataSource;
|
||||||
|
export const sqliteDatasource =
|
||||||
|
process.env.NODE_ENV === "test"
|
||||||
|
? new DataSource({
|
||||||
|
type: "sqlite",
|
||||||
|
entities: [User, Link],
|
||||||
|
database: ":memory:",
|
||||||
|
synchronize: true,
|
||||||
|
extra: {
|
||||||
|
connectionLimit: 1,
|
||||||
|
busyTimeout: 4000,
|
||||||
|
busyErrorRetry: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: new DataSource({
|
||||||
|
type: "sqlite",
|
||||||
|
entities: [User, Link],
|
||||||
|
database: "database.sqlite",
|
||||||
|
synchronize: true,
|
||||||
|
extra: {
|
||||||
|
connectionLimit: 1,
|
||||||
|
busyTimeout: 4000,
|
||||||
|
busyErrorRetry: 10
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
registerProvider<DataSource>({
|
||||||
|
provide: SqliteDatasource,
|
||||||
|
type: "typeorm:datasource",
|
||||||
|
deps: [Logger],
|
||||||
|
async useAsyncFactory(logger: Logger) {
|
||||||
|
await sqliteDatasource.initialize();
|
||||||
|
|
||||||
|
logger.info("Connected with typeorm to database: Sqlite");
|
||||||
|
|
||||||
|
return sqliteDatasource;
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
$onDestroy(dataSource) {
|
||||||
|
return dataSource.isInitialized && dataSource.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function executeWithRetry<T>(
|
||||||
|
operation: (queryRunner: QueryRunner) => Promise<T>,
|
||||||
|
dataSource: DataSource,
|
||||||
|
maxRetries = 10,
|
||||||
|
delay = 1000
|
||||||
|
): Promise<T> {
|
||||||
|
let retries = 0;
|
||||||
|
while (true) {
|
||||||
|
const queryRunner = dataSource.createQueryRunner();
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
const result = await operation(queryRunner);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
|
||||||
|
if (error.code === "SQLITE_BUSY" && retries < maxRetries) {
|
||||||
|
retries++;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { CollectionOf, MaxLength, Property, Required } from "@tsed/schema";
|
||||||
|
import { BeforeInsert, Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
|
||||||
|
import { Link } from "./link/Link";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class User {
|
||||||
|
@PrimaryColumn("uuid")
|
||||||
|
@Property()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
@MaxLength(100)
|
||||||
|
@Required()
|
||||||
|
service: string;
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
@MaxLength(100)
|
||||||
|
@Required()
|
||||||
|
serviceUsername: string;
|
||||||
|
|
||||||
|
@OneToMany(() => Link, (link) => link.user)
|
||||||
|
@CollectionOf(() => Link)
|
||||||
|
links: Link[];
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateId() {
|
||||||
|
if (!this.id) {
|
||||||
|
this.id = uuidv4();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Property, Required, MaxLength } from "@tsed/schema";
|
||||||
|
|
||||||
|
export class CreateLinkDto {
|
||||||
|
@Property()
|
||||||
|
@Required()
|
||||||
|
@MaxLength(100)
|
||||||
|
service: string;
|
||||||
|
|
||||||
|
@Property()
|
||||||
|
@Required()
|
||||||
|
@MaxLength(100)
|
||||||
|
serviceUsername: string;
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { MaxLength, Property, Required } from "@tsed/schema";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryColumn, JoinColumn, BeforeInsert } from "typeorm";
|
||||||
|
import { User } from "../User";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Link {
|
||||||
|
@PrimaryColumn("uuid")
|
||||||
|
@Property()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
@MaxLength(100)
|
||||||
|
@Required()
|
||||||
|
service: string;
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
@MaxLength(100)
|
||||||
|
@Required()
|
||||||
|
serviceUsername: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.links, { onDelete: "SET NULL", onUpdate: "CASCADE" })
|
||||||
|
@JoinColumn({ name: "userId" })
|
||||||
|
user: User;
|
||||||
|
|
||||||
|
@Column({ type: "uuid", nullable: true })
|
||||||
|
@Property()
|
||||||
|
userId: string | null;
|
||||||
|
|
||||||
|
@BeforeInsert()
|
||||||
|
generateId() {
|
||||||
|
if (!this.id) {
|
||||||
|
this.id = uuidv4();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { $log } from "@tsed/common";
|
||||||
|
import { PlatformExpress } from "@tsed/platform-express";
|
||||||
|
import { Server } from "./Server";
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
try {
|
||||||
|
const platform = await PlatformExpress.bootstrap(Server);
|
||||||
|
await platform.listen();
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
platform.stop();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
$log.error({ event: "SERVER_BOOTSTRAP_ERROR", message: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap();
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"noResolve": false,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
|
"inlineSources": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "esnext",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"isolatedModules": false,
|
||||||
|
"suppressImplicitAnyIndexErrors": false,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"importHelpers": true,
|
||||||
|
"newLine": "LF",
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"lib": ["es7", "dom", "ESNext.AsyncIterable"],
|
||||||
|
"typeRoots": ["./node_modules/@types"]
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"linterOptions": {
|
||||||
|
"exclude": []
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>client</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body, h1 {
|
||||||
|
font-family: Source Sans Pro,sans-serif;
|
||||||
|
}
|
||||||
|
body:after {
|
||||||
|
content: "";
|
||||||
|
background-image: radial-gradient(#eef2f5 0,#f4f7f8 40%,transparent 75%);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 60%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.container-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 60px;
|
||||||
|
}
|
||||||
|
.container-logo img {
|
||||||
|
max-width: 150px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
ul li a {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
padding-top: .25rem;
|
||||||
|
padding-bottom: .25rem;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
border: 2px solid #504747;
|
||||||
|
min-width: 110px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: #504747;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all ease-in-out 0.5s;
|
||||||
|
}
|
||||||
|
ul li a:hover {
|
||||||
|
color: #14a5c2;
|
||||||
|
border-color: #14a5c2;
|
||||||
|
}
|
||||||
|
ul li a span {
|
||||||
|
margin: .25rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div>
|
||||||
|
<div class="container-logo">
|
||||||
|
<img src="https://tsed.io/tsed-og.png" alt="Ts.ED">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<% docs.forEach((doc) => { %>
|
||||||
|
|
||||||
|
<li><a href="<%= doc.path %>"><span>OpenSpec <%= doc.specVersion %></span></a></li>
|
||||||
|
|
||||||
|
<% }) %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import swc from "unplugin-swc";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
root: "./"
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// This is required to build the test files with SWC
|
||||||
|
swc.vite({
|
||||||
|
// Explicitly set the module type to avoid inheriting this value from a `.swcrc` config file
|
||||||
|
module: { type: "es6" },
|
||||||
|
jsc: {
|
||||||
|
transform: {
|
||||||
|
useDefineForClassFields: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
Loading…
Reference in New Issue