This commit is contained in:
parent
f665fcbb54
commit
148182c680
42
.dockerignore
Normal file
42
.dockerignore
Normal file
@ -0,0 +1,42 @@
|
||||
# Git
|
||||
**/.git/
|
||||
|
||||
# Backup
|
||||
**/*.bak
|
||||
|
||||
# NodeJS
|
||||
**/node_modules/
|
||||
|
||||
# coverage test output
|
||||
.nyc_output
|
||||
coverage/
|
||||
|
||||
# log files
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Yarn2 (PnP)
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/releases/
|
||||
|
||||
# tsc buildinfo
|
||||
*.tsbuildinfo
|
||||
|
||||
# VScode
|
||||
.vscode/
|
||||
|
||||
# Test output
|
||||
**/.nyc_output
|
||||
|
||||
# Volumes (for Docker deployment)
|
||||
volumes/*
|
||||
|
||||
# App specific
|
||||
.env
|
||||
dist/
|
||||
scripts/
|
||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
||||
[*]
|
||||
tab_width = 2
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
42
.eslintrc.js
Normal file
42
.eslintrc.js
Normal file
@ -0,0 +1,42 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es2021: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'prettier',
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': { typescript: {} },
|
||||
'import/external-module-folders': ['node_modules', '.yarn'],
|
||||
},
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-underscore-dangle': 'off',
|
||||
'require-await': 'warn',
|
||||
eqeqeq: 'warn',
|
||||
'import/extensions': 'off',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
named: true,
|
||||
alphabetize: { order: 'asc' },
|
||||
warnOnUnassignedImports: true,
|
||||
},
|
||||
],
|
||||
'import/no-cycle': ['error', { ignoreExternal: true }],
|
||||
'import/no-self-import': 'error',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
},
|
||||
};
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
34
.gitea/workflows/release.yaml
Normal file
34
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,34 @@
|
||||
name: Release Docker Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to Gitea
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ vars._CI_REGISTRY }}
|
||||
username: ${{ vars._CI_REGISTRY_USER }}
|
||||
password: ${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
# Remarks: Setting up QEMU and Buildx are useful for cross-platform build but it will slow the CI pipeline.
|
||||
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
secrets: JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN=${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||
push: true
|
||||
tags: ${{ vars._CI_REGISTRY }}/${{ gitea.repository }}:${{ gitea.ref_name }},${{ vars._CI_REGISTRY }}/${{ gitea.repository }}:latest
|
||||
27
.gitea/workflows/test.yaml
Normal file
27
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
name: Test
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .gitea/workflows/test.yaml
|
||||
- src/**/*
|
||||
- test/**/*
|
||||
- .mocharc.js
|
||||
- tsconfig.json
|
||||
- package.json
|
||||
- .yarnrc.yml
|
||||
- yarn.lock
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:lts
|
||||
env:
|
||||
JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN: ${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install packages
|
||||
run: corepack enable && yarn
|
||||
- name: Test code
|
||||
run: yarn test:coverage
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
node_modules
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# npm settings
|
||||
.npmrc
|
||||
|
||||
# coverage test output
|
||||
.nyc_output
|
||||
/coverage/
|
||||
|
||||
# log files
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Backup
|
||||
*.bak
|
||||
|
||||
# yarn files (sdks are excluded as it depends on vscode version)
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# tsc buildinfo
|
||||
*.tsbuildinfo
|
||||
|
||||
# tsc build output
|
||||
/dist/
|
||||
|
||||
# Volumes (for Docker deployment)
|
||||
/volumes/*
|
||||
|
||||
# env
|
||||
/.env
|
||||
|
||||
# GitKeep
|
||||
!.gitkeep
|
||||
15
.mocharc.js
Normal file
15
.mocharc.js
Normal file
@ -0,0 +1,15 @@
|
||||
const [_major, _minor] = process.version.slice(1).split('.', 2);
|
||||
|
||||
const major = parseInt(_major, 10);
|
||||
const minor = parseInt(_minor, 10);
|
||||
|
||||
// Disable experimental-strip-types to handle enum transform with ts-node
|
||||
|
||||
module.exports = {
|
||||
require: 'ts-node/register',
|
||||
spec: ['test/**/*.spec.ts'],
|
||||
'node-option':
|
||||
major > 22 || (major === 22 && minor >= 6)
|
||||
? ['no-experimental-strip-types']
|
||||
: [],
|
||||
};
|
||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@ -0,0 +1,5 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
.vscode/*
|
||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".yarnrc.yml",
|
||||
"options": {
|
||||
"singleQuote": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"arcanis.vscode-zipfs",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/.yarn": true,
|
||||
"**/.pnp.*": true
|
||||
},
|
||||
"eslint.nodePath": ".yarn/sdks",
|
||||
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||
}
|
||||
BIN
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
BIN
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
Binary file not shown.
32
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
32
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real eslint/bin/eslint.js your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));
|
||||
32
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
32
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`eslint`));
|
||||
32
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
32
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/use-at-your-own-risk
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real eslint/use-at-your-own-risk your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));
|
||||
14
.yarn/sdks/eslint/package.json
vendored
Normal file
14
.yarn/sdks/eslint/package.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "8.57.1-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"eslint": "./bin/eslint.js"
|
||||
},
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": "./lib/api.js",
|
||||
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
||||
}
|
||||
}
|
||||
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
||||
integrations:
|
||||
- vscode
|
||||
32
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
32
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier/bin/prettier.cjs
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real prettier/bin/prettier.cjs your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`));
|
||||
32
.yarn/sdks/prettier/index.cjs
vendored
Normal file
32
.yarn/sdks/prettier/index.cjs
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require prettier
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real prettier your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`prettier`));
|
||||
7
.yarn/sdks/prettier/package.json
vendored
Normal file
7
.yarn/sdks/prettier/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "prettier",
|
||||
"version": "3.7.4-sdk",
|
||||
"main": "./index.cjs",
|
||||
"type": "commonjs",
|
||||
"bin": "./bin/prettier.cjs"
|
||||
}
|
||||
32
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
32
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsc
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real typescript/bin/tsc your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));
|
||||
32
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
32
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsserver
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real typescript/bin/tsserver your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));
|
||||
32
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
32
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`));
|
||||
248
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
248
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
const moduleWrapper = exports => {
|
||||
return wrapWithUserWrapper(moduleWrapperFn(exports));
|
||||
};
|
||||
|
||||
const moduleWrapperFn = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
||||
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
||||
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
||||
if (major > 5 || (major === 5 && minor >= 5)) {
|
||||
moduleWrapper(absRequire(`typescript`));
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
||||
248
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
248
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
const moduleWrapper = exports => {
|
||||
return wrapWithUserWrapper(moduleWrapperFn(exports));
|
||||
};
|
||||
|
||||
const moduleWrapperFn = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
} else {
|
||||
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
||||
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
||||
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
||||
if (major > 5 || (major === 5 && minor >= 5)) {
|
||||
moduleWrapper(absRequire(`typescript`));
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
||||
32
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
32
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire, register} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
const {pathToFileURL} = require(`url`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript
|
||||
require(absPnpApiPath).setup();
|
||||
if (isPnpLoaderEnabled && register) {
|
||||
register(pathToFileURL(absPnpLoaderPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||
? exports => absRequire(absUserWrapperPath)(exports)
|
||||
: exports => exports;
|
||||
|
||||
// Defer to the real typescript your application uses
|
||||
module.exports = wrapWithUserWrapper(absRequire(`typescript`));
|
||||
10
.yarn/sdks/typescript/package.json
vendored
Normal file
10
.yarn/sdks/typescript/package.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "5.9.3-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"tsc": "./bin/tsc",
|
||||
"tsserver": "./bin/tsserver"
|
||||
}
|
||||
}
|
||||
12
.yarnrc.yml
Normal file
12
.yarnrc.yml
Normal file
@ -0,0 +1,12 @@
|
||||
enableGlobalCache: false
|
||||
|
||||
nodeLinker: pnp
|
||||
|
||||
npmScopes:
|
||||
jig-software:
|
||||
npmAlwaysAuth: false
|
||||
npmAuthToken: "${JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN:-NONE}"
|
||||
npmPublishRegistry: "https://gitea.jig.com.hk/api/packages/jig-software/npm/"
|
||||
npmRegistryServer: "https://gitea.jig.com.hk/api/packages/jig-software/npm/"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY .yarnrc.yml package.json yarn.lock ./
|
||||
COPY .yarn/releases ./.yarn/releases
|
||||
RUN --mount=type=secret,id=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN,env=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN yarn
|
||||
COPY tsconfig.json tsconfig.build.json ./
|
||||
COPY src ./src
|
||||
RUN yarn build
|
||||
RUN yarn workspaces focus --all --production
|
||||
|
||||
FROM node:22-alpine
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
COPY .yarnrc.yml package.json yarn.lock ./
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/.yarn ./.yarn
|
||||
COPY --from=build /app/.pnp.* ./
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
|
||||
# Uncomment if healthcheck is needed
|
||||
# HEALTHCHECK --interval=1m --timeout=10s --start-period=10s --retries=3 \
|
||||
# CMD [ "yarn", "health-check"]
|
||||
BIN
asset/fast_logo.jpg
Normal file
BIN
asset/fast_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
asset/jec_logo.jpg
Normal file
BIN
asset/jec_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
10
config.example.json
Normal file
10
config.example.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"port": 3000,
|
||||
"apiPrefix": "/api",
|
||||
"databaseURL": "mongodb://mongo:27017/?replicaSet=rs0",
|
||||
"databaseName": "example",
|
||||
"logDirectory": "volumes/logs",
|
||||
"printLogLevel": "verbose",
|
||||
"persistLogLevel": "info",
|
||||
"defaultSettings": {}
|
||||
}
|
||||
BIN
event_report.xlsx
Normal file
BIN
event_report.xlsx
Normal file
Binary file not shown.
83
init.sh
Executable file
83
init.sh
Executable file
@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Workaround to nullify yarn 2+ auto git init with master branch
|
||||
git init --initial-branch=main
|
||||
yarn init --yes
|
||||
|
||||
yarn set version berry --yarn-path
|
||||
|
||||
yarn add \
|
||||
@jig-software/sdk \
|
||||
@jig-software/trest-core \
|
||||
@jig-software/trest-client \
|
||||
@jig-software/zod-bson \
|
||||
@jig-software/logger \
|
||||
@jig-software/logger-winston \
|
||||
debug \
|
||||
bson \
|
||||
koa \
|
||||
koa-body \
|
||||
@koa/router \
|
||||
zod \
|
||||
mongodb \
|
||||
eventemitter3 \
|
||||
reflect-metadata
|
||||
|
||||
yarn add --dev \
|
||||
@types/koa \
|
||||
@types/koa__router \
|
||||
@types/debug \
|
||||
@types/node \
|
||||
commitizen cz-emoji \
|
||||
ts-node-dev \
|
||||
@typescript-eslint/eslint-plugin \
|
||||
@typescript-eslint/parser \
|
||||
eslint@8 \
|
||||
eslint-import-resolver-typescript \
|
||||
eslint-plugin-import \
|
||||
eslint-config-prettier \
|
||||
typescript \
|
||||
prettier \
|
||||
mocha \
|
||||
@types/mocha \
|
||||
chai@^4.3.10 \
|
||||
@types/chai@^4.3.11 \
|
||||
nyc \
|
||||
ts-node \
|
||||
@types/node \
|
||||
rimraf
|
||||
|
||||
yarn dlx @yarnpkg/sdks vscode
|
||||
|
||||
# Format by prettier
|
||||
yarn prettier . --write
|
||||
|
||||
# Set npm environment to skip confirmation prompt
|
||||
orig_npm_config_yes=$npm_config_yes
|
||||
export npm_config_yes=yes
|
||||
|
||||
# Add lint script and package config to the project
|
||||
./patch-package-json.sh
|
||||
|
||||
# Enable .gitea/workflows
|
||||
mv .gitea/workflows.template .gitea/workflows
|
||||
|
||||
|
||||
# Restore the original env
|
||||
export npm_config_yes=$orig_npm_config_yes
|
||||
|
||||
for (( ; ; )); do
|
||||
read -p "The initialization process is done. Do you want to remove this script file now? (Y/n): " remove
|
||||
remove=${remove:-y}
|
||||
echo $remove
|
||||
if [ "$remove" = "y" ] || [ "$remove" = "Y" ]; then
|
||||
rm ./package.patch.json
|
||||
rm ./patch-package-json.sh
|
||||
rm -- "$0"
|
||||
break
|
||||
else
|
||||
if [ "$remove" = "n" ] || [ "$remove" = "N" ]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "report-generation",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "yarn clean && tsc -b tsconfig.build.json",
|
||||
"clean": "rimraf dist",
|
||||
"commit": "cz",
|
||||
"dev": "ts-node-dev --respawn --project tsconfig.json --watch ./volumes/config.json ./src/bootstrap.ts",
|
||||
"health-check": "node ./dist/healthCheck.js",
|
||||
"lint": "eslint '{src,test}/**/*.ts'",
|
||||
"lint:src": "eslint 'src/**/*.ts'",
|
||||
"lint:test": "eslint 'test/**/*.ts'",
|
||||
"start": "node ./dist/bootstrap.js",
|
||||
"test": "nyc mocha",
|
||||
"test:coverage": "nyc --reporter cobertura mocha"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-emoji"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@jig-software/logger": "^1.1.0",
|
||||
"@jig-software/logger-winston": "^1.0.0",
|
||||
"@jig-software/sdk": "^7.0.0",
|
||||
"@jig-software/trest-client": "^3.0.0",
|
||||
"@jig-software/trest-core": "^3.0.0",
|
||||
"@jig-software/zod-bson": "^2.0.0",
|
||||
"@koa/router": "^15.2.0",
|
||||
"bson": "^7.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"exceljs": "^4.4.0",
|
||||
"koa": "^3.1.1",
|
||||
"koa-body": "^7.0.1",
|
||||
"mongodb": "^7.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.11",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/koa": "^3.0.1",
|
||||
"@types/koa__router": "^12.0.5",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^25.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||
"@typescript-eslint/parser": "^8.52.0",
|
||||
"chai": "^4.3.10",
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-emoji": "^1.3.2-canary.2",
|
||||
"eslint": "8",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"mocha": "^11.7.5",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "^3.7.4",
|
||||
"rimraf": "^6.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "yarn@4.12.0"
|
||||
}
|
||||
22
package.patch.json
Normal file
22
package.patch.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"files": ["dist"],
|
||||
"main": "dist/index.js",
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "cz-emoji"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn clean && tsc -b tsconfig.build.json",
|
||||
"clean": "rimraf dist",
|
||||
"commit": "cz",
|
||||
"dev": "ts-node-dev --respawn --project tsconfig.json --watch ./volumes/config.json ./src/bootstrap.ts",
|
||||
"lint": "eslint '{src,test}/**/*.ts'",
|
||||
"lint:src": "eslint 'src/**/*.ts'",
|
||||
"lint:test": "eslint 'test/**/*.ts'",
|
||||
"start": "node ./dist/bootstrap.js",
|
||||
"test": "nyc mocha",
|
||||
"test:coverage": "nyc --reporter cobertura mocha",
|
||||
"health-check": "node ./dist/healthCheck.js"
|
||||
}
|
||||
}
|
||||
6
patch-package-json.sh
Executable file
6
patch-package-json.sh
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Workaround for yarn workspace
|
||||
cat ./package.json ./package.patch.json | npx json --deep-merge | tee ./package.json.temp
|
||||
mv ./package.json.temp ./package.json
|
||||
npx sort-package-json
|
||||
4
scripts/build_image.sh
Executable file
4
scripts/build_image.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
NAME=$(cat ./package.json | npx json name)
|
||||
echo "Build image: $NAME"
|
||||
docker build . -t $NAME --secret id=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN,env=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN
|
||||
12
src/Main.ts
Normal file
12
src/Main.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Inject, KOA_APP, KoaApp, LC, Lifecycle } from '@jig-software/sdk';
|
||||
import { ActiveAppSettings } from './providers';
|
||||
|
||||
export class Main extends Lifecycle {
|
||||
@LC.bind
|
||||
@Inject()
|
||||
activeAppSettings!: ActiveAppSettings;
|
||||
|
||||
@LC.bind
|
||||
@Inject(KOA_APP())
|
||||
koaApp!: KoaApp;
|
||||
}
|
||||
37
src/bootstrap.ts
Normal file
37
src/bootstrap.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { WinstonLogger } from '@jig-software/logger-winston';
|
||||
import {
|
||||
ConfigDI,
|
||||
MongoDI,
|
||||
KoaDI,
|
||||
container,
|
||||
LoggerDI,
|
||||
} from '@jig-software/sdk';
|
||||
import { Main } from './Main';
|
||||
import {
|
||||
AppConfig,
|
||||
KoaAppOptionsFactory,
|
||||
MongoDIOptionsFactory,
|
||||
WinstonLoggerParamsFactory,
|
||||
} from './config';
|
||||
import { HealthCheckController, ReadingReportController } from './controllers';
|
||||
import { ActiveAppSettings } from './providers';
|
||||
import { AppSettingsRepository } from './repositories';
|
||||
async function bootstrap() {
|
||||
container.use(
|
||||
ConfigDI.config(AppConfig, { configPath: './volumes/config.json' }),
|
||||
LoggerDI.logger(WinstonLogger, {
|
||||
paramsFactory: WinstonLoggerParamsFactory,
|
||||
}),
|
||||
MongoDI.root({ optionsFactory: MongoDIOptionsFactory }),
|
||||
KoaDI.root({ optionsFactory: KoaAppOptionsFactory }),
|
||||
MongoDI.repository('appSettings', AppSettingsRepository),
|
||||
ActiveAppSettings,
|
||||
);
|
||||
await container.create(KoaDI.controller(HealthCheckController));
|
||||
await container.create(KoaDI.controller(ReadingReportController));
|
||||
const main = await container.create(Main);
|
||||
await main.start();
|
||||
process.on('unhandledRejection', console.error);
|
||||
}
|
||||
|
||||
bootstrap().catch(console.error);
|
||||
74
src/config.ts
Normal file
74
src/config.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { WinstonLoggerOptions } from '@jig-software/logger-winston';
|
||||
import {
|
||||
Config,
|
||||
Inject,
|
||||
KoaAppOptions,
|
||||
MongoDIOptions,
|
||||
} from '@jig-software/sdk';
|
||||
import { z } from 'zod';
|
||||
import { AppSettingsInit } from './shared';
|
||||
|
||||
// Note: AppConfig is for Static props.
|
||||
|
||||
const LogLevel = z.enum([
|
||||
'error',
|
||||
'warn',
|
||||
'info',
|
||||
'http',
|
||||
'verbose',
|
||||
'debug',
|
||||
'silly',
|
||||
]);
|
||||
|
||||
const _AppConfig = z.object({
|
||||
port: z.number().optional(),
|
||||
apiPrefix: z.string().optional(),
|
||||
databaseURL: z.string(),
|
||||
databaseName: z.string(),
|
||||
logDirectory: z.string().optional(),
|
||||
printLogLevel: LogLevel.optional(),
|
||||
persistLogLevel: LogLevel.optional(),
|
||||
defaultSettings: AppSettingsInit,
|
||||
});
|
||||
|
||||
export const AppConfig = Config.extend('AppConfig', _AppConfig);
|
||||
export type AppConfig = Config.infer<typeof AppConfig>;
|
||||
|
||||
export class KoaAppOptionsFactory {
|
||||
@Inject()
|
||||
private appConfig!: AppConfig;
|
||||
create(): KoaAppOptions {
|
||||
return {
|
||||
port: this.appConfig.port ?? 3000,
|
||||
prefix: this.appConfig.apiPrefix,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MongoDIOptionsFactory {
|
||||
@Inject()
|
||||
private appConfig!: AppConfig;
|
||||
|
||||
create(): MongoDIOptions {
|
||||
return {
|
||||
uri: this.appConfig.databaseURL,
|
||||
database: this.appConfig.databaseName,
|
||||
clientOptions: { ignoreUndefined: true },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class WinstonLoggerParamsFactory {
|
||||
@Inject()
|
||||
private appConfig!: AppConfig;
|
||||
|
||||
create(): [WinstonLoggerOptions] {
|
||||
return [
|
||||
{
|
||||
logDirname: this.appConfig.logDirectory,
|
||||
printLogLevel: this.appConfig.printLogLevel,
|
||||
persistLogLevel: this.appConfig.persistLogLevel,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
22
src/controllers/HealthCheckController.ts
Normal file
22
src/controllers/HealthCheckController.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Logger } from '@jig-software/logger';
|
||||
import { Controller, Inject } from '@jig-software/sdk';
|
||||
import { healthCheckContract } from '../shared/contracts';
|
||||
|
||||
export class HealthCheckController extends Controller {
|
||||
@Inject()
|
||||
protected logger!: Logger;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger.setTag(this.constructor.name);
|
||||
this.implement(healthCheckContract, (r) =>
|
||||
r.handlers({
|
||||
checkHealth: {
|
||||
handler: () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
184
src/controllers/ReadingReportController.ts
Normal file
184
src/controllers/ReadingReportController.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { Logger } from '@jig-software/logger';
|
||||
import { Controller, Inject } from '@jig-software/sdk';
|
||||
import ExcelJS from 'exceljs';
|
||||
import path from 'path';
|
||||
import { readingReportContract } from '../shared';
|
||||
import { createFileOutput } from '@jig-software/trest-core';
|
||||
import fs from 'fs';
|
||||
|
||||
interface ReadingStats {
|
||||
min?: string;
|
||||
max?: string;
|
||||
avg?: string;
|
||||
}
|
||||
|
||||
interface DeviceData {
|
||||
building: string;
|
||||
floor: string;
|
||||
device_name: string;
|
||||
min_max_avg: Record<string, ReadingStats>;
|
||||
readings: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
interface ReportParams {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
time_step?: string;
|
||||
}
|
||||
|
||||
export class ReadingReportController extends Controller {
|
||||
@Inject()
|
||||
protected logger!: Logger;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger.setTag(this.constructor.name);
|
||||
this.implement(readingReportContract, (r) =>
|
||||
r.handlers({
|
||||
generateReadingsReport: {
|
||||
handler: async (FullReportSchema) => {
|
||||
const data: DeviceData[] = FullReportSchema.data;
|
||||
const reportParams: ReportParams = FullReportSchema.report_params;
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Event Records');
|
||||
|
||||
// ---------------- Logos ----------------
|
||||
const jecLogoId = workbook.addImage({
|
||||
filename: path.resolve('asset/jec_logo.jpg'),
|
||||
extension: 'jpeg',
|
||||
});
|
||||
|
||||
const fastLogoId = workbook.addImage({
|
||||
filename: path.resolve('asset/fast_logo.jpg'),
|
||||
extension: 'jpeg',
|
||||
});
|
||||
|
||||
worksheet.addImage(jecLogoId, {
|
||||
tl: { col: 0, row: 0 },
|
||||
ext: { width: 120, height: 60 },
|
||||
});
|
||||
worksheet.addImage(fastLogoId, {
|
||||
tl: { col: 7, row: 0 },
|
||||
ext: { width: 120, height: 60 },
|
||||
});
|
||||
|
||||
// ---------------- Title ----------------
|
||||
worksheet.mergeCells('B1:G2');
|
||||
const titleCell = worksheet.getCell('B1');
|
||||
titleCell.value = 'Event Report';
|
||||
titleCell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||
titleCell.font = { bold: true, size: 18 };
|
||||
|
||||
// ---------------- Fixed params ----------------
|
||||
const fixedParams = [
|
||||
['Start Date:', reportParams.start_date ?? ''],
|
||||
['End Date:', reportParams.end_date ?? ''],
|
||||
['Time Step:', reportParams.time_step ?? ''],
|
||||
];
|
||||
|
||||
fixedParams.forEach(([label, value], idx) => {
|
||||
worksheet.getCell(4 + idx, 2).value = label;
|
||||
worksheet.getCell(4 + idx, 3).value = value;
|
||||
});
|
||||
|
||||
// ---------------- Headers ----------------
|
||||
const allDates = Array.from(
|
||||
new Set(
|
||||
data.flatMap((device) =>
|
||||
Object.values(device.readings).flatMap((r) => Object.keys(r)),
|
||||
),
|
||||
),
|
||||
).sort();
|
||||
|
||||
const headers = [
|
||||
'Building',
|
||||
'Floor',
|
||||
'Device Name',
|
||||
'Reading Type',
|
||||
'Minimum',
|
||||
'Maximum',
|
||||
'Average',
|
||||
...allDates,
|
||||
];
|
||||
|
||||
const headerRowIndex = 7;
|
||||
const headerRow = worksheet.getRow(headerRowIndex);
|
||||
headerRow.values = headers;
|
||||
headerRow.eachCell((cell) => {
|
||||
cell.font = { bold: true };
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFD9D9D9' },
|
||||
};
|
||||
});
|
||||
|
||||
// ---------------- Data ----------------
|
||||
let currentRow = headerRowIndex + 1;
|
||||
|
||||
for (const device of data) {
|
||||
const types = Object.keys(device.readings);
|
||||
const startRow = currentRow;
|
||||
const endRow = currentRow + types.length - 1;
|
||||
|
||||
if (types.length > 1) {
|
||||
worksheet.mergeCells(startRow, 1, endRow, 1);
|
||||
worksheet.mergeCells(startRow, 2, endRow, 2);
|
||||
worksheet.mergeCells(startRow, 3, endRow, 3);
|
||||
}
|
||||
|
||||
worksheet.getCell(startRow, 1).value = device.building;
|
||||
worksheet.getCell(startRow, 2).value = device.floor;
|
||||
worksheet.getCell(startRow, 3).value = device.device_name;
|
||||
|
||||
for (const type of types) {
|
||||
const row = worksheet.getRow(currentRow);
|
||||
|
||||
row.getCell(4).value = type;
|
||||
|
||||
const stats = device.min_max_avg?.[type] ?? {};
|
||||
row.getCell(5).value = stats.min ?? '-';
|
||||
row.getCell(6).value = stats.max ?? '-';
|
||||
row.getCell(7).value = stats.avg ?? '-';
|
||||
|
||||
allDates.forEach((date, idx) => {
|
||||
row.getCell(8 + idx).value =
|
||||
device.readings[type]?.[date] ?? '-';
|
||||
});
|
||||
|
||||
currentRow++;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Processed device '${device.device_name}' rows ${startRow}–${endRow}`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------- Auto-width ----------------
|
||||
worksheet.columns.forEach((col) => {
|
||||
let max = 10;
|
||||
col.eachCell!({ includeEmpty: true }, (cell) => {
|
||||
const len = String(cell.value ?? '').length;
|
||||
max = Math.max(max, len);
|
||||
});
|
||||
col.width = max + 2;
|
||||
});
|
||||
|
||||
worksheet.autoFilter = {
|
||||
from: { row: headerRowIndex, column: 1 },
|
||||
to: { row: currentRow - 1, column: headers.length },
|
||||
};
|
||||
|
||||
// ---------------- Save file ----------------
|
||||
const outputPath = path.resolve('event_report.xlsx');
|
||||
await workbook.xlsx.writeFile(outputPath);
|
||||
console.log(`Excel report generated: ${outputPath}`);
|
||||
const readStream = fs.createReadStream(outputPath);
|
||||
|
||||
return createFileOutput(readStream, 'event_report.xlsx');
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
2
src/controllers/index.ts
Normal file
2
src/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './HealthCheckController';
|
||||
export * from './ReadingReportController';
|
||||
24
src/healthCheck.ts
Normal file
24
src/healthCheck.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ConfigDI, container } from '@jig-software/sdk';
|
||||
import { TRestClient } from '@jig-software/trest-client';
|
||||
import { AppConfig } from './config';
|
||||
import { healthCheckContract } from './shared';
|
||||
|
||||
async function healthCheck() {
|
||||
const appConfig = await container.create(
|
||||
ConfigDI.config(AppConfig, {
|
||||
configPath: './volumes/config.json',
|
||||
}),
|
||||
);
|
||||
const { port, apiPrefix } = appConfig;
|
||||
const baseUrl = apiPrefix
|
||||
? `http://localhost:${port}/${apiPrefix.replace(/^\//, '')}`
|
||||
: `http://localhost:${port}/`;
|
||||
const healthCheckClient = new TRestClient(healthCheckContract, baseUrl);
|
||||
await healthCheckClient.actions.checkHealth().unwrap();
|
||||
console.info('OK');
|
||||
}
|
||||
|
||||
healthCheck().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
3
src/middlewares/index.ts
Normal file
3
src/middlewares/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// * Export middlewares here
|
||||
// * export * from './someMiddleware'
|
||||
export {};
|
||||
170
src/providers/ActiveAppSettings.ts
Normal file
170
src/providers/ActiveAppSettings.ts
Normal file
@ -0,0 +1,170 @@
|
||||
import { Logger } from '@jig-software/logger';
|
||||
import { Inject, Lifecycle, MongoOrmValidationError } from '@jig-software/sdk';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { ChangeStream } from 'mongodb';
|
||||
import { AppConfig } from '../config';
|
||||
import { AppSettingsRepository } from '../repositories';
|
||||
import { AppSettings, AppSettingsInit } from '../shared';
|
||||
|
||||
export type AppSettingsChangeEmitter = EventEmitter<{
|
||||
change: (appSettings: AppSettingsInit) => void;
|
||||
error: (err: unknown) => void;
|
||||
}>;
|
||||
|
||||
export class ActiveAppSettings extends Lifecycle {
|
||||
@Inject()
|
||||
protected logger!: Logger;
|
||||
|
||||
@Inject()
|
||||
private appConfig!: AppConfig;
|
||||
|
||||
@Inject()
|
||||
private appSettingsRepo!: AppSettingsRepository;
|
||||
|
||||
private _changeStream: ChangeStream | null = null;
|
||||
|
||||
private _appSettings: AppSettings | null = null;
|
||||
|
||||
private _changeEmitter: AppSettingsChangeEmitter = new EventEmitter();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger.setTag(this.constructor.name);
|
||||
}
|
||||
|
||||
get changeEmitter() {
|
||||
return this._changeEmitter;
|
||||
}
|
||||
|
||||
get(): Readonly<AppSettingsInit> {
|
||||
return (
|
||||
(this._appSettings as unknown as AppSettingsInit) ??
|
||||
this.appConfig.defaultSettings
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify appSettings in-place.
|
||||
* Will persist modification. If fails will rollback to existing one.
|
||||
*/
|
||||
async set(modify: (appSettings: AppSettingsInit) => void) {
|
||||
if (!this._appSettings) {
|
||||
throw new Error('AppSettings is not ready.'); // This should not happens.
|
||||
}
|
||||
const _id = this._appSettings._id;
|
||||
modify(this._appSettings as unknown as AppSettingsInit);
|
||||
try {
|
||||
await this.appSettingsRepo.persist(this._appSettings);
|
||||
} catch (err) {
|
||||
this._appSettings = await this.appSettingsRepo.get(_id);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async onStart() {
|
||||
await this.ensureAppSettings();
|
||||
this.setUpChangeStream();
|
||||
}
|
||||
|
||||
onStop() {
|
||||
this._appSettings = null;
|
||||
this.cleanUpChangeStream();
|
||||
}
|
||||
|
||||
private async ensureAppSettings() {
|
||||
const count = await this.appSettingsRepo.count({});
|
||||
if (count === 0) {
|
||||
this.logger.info('AppSettings is not found. Create one with default.');
|
||||
this._appSettings = await this.appSettingsRepo.create(
|
||||
this.appConfig.defaultSettings,
|
||||
);
|
||||
} else {
|
||||
if (count > 1)
|
||||
this.logger.warn(
|
||||
`Found ${count} AppSettings. Only the latest will be used.`,
|
||||
);
|
||||
else this.logger.info('Found AppSettings.');
|
||||
this._appSettings = await this.appSettingsRepo.findOne(
|
||||
{},
|
||||
{ sort: { updatedAt: -1 } },
|
||||
);
|
||||
try {
|
||||
this.appSettingsRepo.validatePersisted(this._appSettings);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
'Invalid AppSettings. AppSettings may not be properly migrated!',
|
||||
);
|
||||
if (err instanceof MongoOrmValidationError)
|
||||
this.logger.error(err.message);
|
||||
}
|
||||
}
|
||||
this._changeEmitter.emit(
|
||||
'change',
|
||||
this._appSettings! as unknown as AppSettingsInit,
|
||||
);
|
||||
}
|
||||
|
||||
private setUpChangeStream() {
|
||||
if (this._changeStream) {
|
||||
this.logger.warn(`ChangeStream already setUp. Skip.`);
|
||||
return;
|
||||
}
|
||||
this._changeStream = this.appSettingsRepo
|
||||
.watch(
|
||||
[
|
||||
{
|
||||
$match: {
|
||||
operationType: { $in: ['replace', 'update', 'delete'] },
|
||||
},
|
||||
},
|
||||
],
|
||||
{ fullDocument: 'updateLookup' },
|
||||
)
|
||||
.on('change', (change) => {
|
||||
switch (change.operationType) {
|
||||
case 'update':
|
||||
case 'replace': {
|
||||
const appSettings = change.fullDocument!;
|
||||
this.logger.debug(`Detect ${change.operationType}.`);
|
||||
if (this._appSettings?._id.equals(appSettings._id)) {
|
||||
this.logger.info('Current AppSettings is updated.');
|
||||
this._appSettings = appSettings;
|
||||
this._changeEmitter.emit(
|
||||
'change',
|
||||
this._appSettings! as unknown as AppSettingsInit,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const { _id: deletedId } = change.documentKey;
|
||||
this.logger.debug(`Detect delete (${deletedId}).`);
|
||||
if (this._appSettings?._id.equals(deletedId)) {
|
||||
this.logger.info('Current AppSettings is deleted. Reinitialize.');
|
||||
this._appSettings = null;
|
||||
this.ensureAppSettings().catch((err) => {
|
||||
this.logger.error('Fail to reinitialize.');
|
||||
this.logger.error(err);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.once('error', (err) => {
|
||||
this.logger.error(`Error in ChangeStream. Restart.`);
|
||||
this.logger.error(err.message);
|
||||
this.launch(() => {
|
||||
throw err;
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
private cleanUpChangeStream() {
|
||||
this._changeStream?.close().catch((err) => {
|
||||
this.logger.error('Fail to close ChangeStream.');
|
||||
this.logger.error(err);
|
||||
});
|
||||
this._changeStream = null;
|
||||
}
|
||||
}
|
||||
1
src/providers/index.ts
Normal file
1
src/providers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './ActiveAppSettings';
|
||||
16
src/repositories/AppSettingsRepository.ts
Normal file
16
src/repositories/AppSettingsRepository.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Repository } from '@jig-software/sdk';
|
||||
import { AppSettings } from '../shared';
|
||||
|
||||
export const AppSettingsRepository = Repository.extend(
|
||||
'AppSettingsRepository',
|
||||
{
|
||||
schema: AppSettings,
|
||||
autoTimestamps: {
|
||||
createTimestampKey: 'createdAt',
|
||||
updateTimestampKey: 'updatedAt',
|
||||
},
|
||||
},
|
||||
);
|
||||
export type AppSettingsRepository = Repository.infer<
|
||||
typeof AppSettingsRepository
|
||||
>;
|
||||
1
src/repositories/index.ts
Normal file
1
src/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './AppSettingsRepository';
|
||||
5
src/shared/constants/index.ts
Normal file
5
src/shared/constants/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// * Export constants here
|
||||
// * export * from './someConstant'
|
||||
// * export const SOME_OTHER_CONSTANT = 'constant value';
|
||||
export {};
|
||||
export * from './reading';
|
||||
53
src/shared/constants/reading.ts
Normal file
53
src/shared/constants/reading.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import z from 'zod';
|
||||
|
||||
export const FullReportInput = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
building: z.string(),
|
||||
floor: z.string(),
|
||||
device_name: z.string(),
|
||||
min_max_avg: z.object({
|
||||
Temperature: z.object({
|
||||
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||
}),
|
||||
Humidity: z.object({
|
||||
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||
}),
|
||||
Battery: z.object({
|
||||
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||
}),
|
||||
}),
|
||||
readings: z.object({
|
||||
Temperature: z.record(
|
||||
z.string(),
|
||||
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||
),
|
||||
Humidity: z.record(
|
||||
z.string(),
|
||||
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||
),
|
||||
Battery: z.record(
|
||||
z.string(),
|
||||
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
report_params: z.object({
|
||||
start_date: z.string().refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: 'Invalid start_date',
|
||||
}),
|
||||
end_date: z.string().refine((val) => !isNaN(Date.parse(val)), {
|
||||
message: 'Invalid end_date',
|
||||
}),
|
||||
time_step: z.enum(['daily', 'weekly', 'monthly']),
|
||||
}),
|
||||
});
|
||||
|
||||
export type FullReportInput = z.infer<typeof FullReportInput>;
|
||||
10
src/shared/contracts/healthCheckContract.ts
Normal file
10
src/shared/contracts/healthCheckContract.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { buildContract } from '@jig-software/trest-core';
|
||||
|
||||
export const healthCheckContract = buildContract((c) =>
|
||||
c
|
||||
.prefix('/health-check')
|
||||
.parse((p) => p.error.text(400))
|
||||
.endpoints({
|
||||
checkHealth: (e) => e.method('GET').parse((p) => p.success.null(200)),
|
||||
}),
|
||||
);
|
||||
2
src/shared/contracts/index.ts
Normal file
2
src/shared/contracts/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './healthCheckContract';
|
||||
export * from './readingReportContract';
|
||||
16
src/shared/contracts/readingReportContract.ts
Normal file
16
src/shared/contracts/readingReportContract.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { A, buildContract } from '@jig-software/trest-core';
|
||||
import { FullReportInput } from '../constants';
|
||||
|
||||
export const readingReportContract = buildContract((c) =>
|
||||
c
|
||||
.prefix('/reports')
|
||||
.parse((p) => p.error.text(400).error.text(401))
|
||||
.endpoints({
|
||||
generateReadingsReport: (e) =>
|
||||
e
|
||||
.method('POST')
|
||||
.path('/reading-report')
|
||||
.prepare((p) => p.body.json(FullReportInput).annotate<[query: A]>())
|
||||
.parse((p) => p.success.file(200)),
|
||||
}),
|
||||
);
|
||||
3
src/shared/index.ts
Normal file
3
src/shared/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './constants';
|
||||
export * from './contracts';
|
||||
export * from './schemas';
|
||||
21
src/shared/schemas/AppSettings.ts
Normal file
21
src/shared/schemas/AppSettings.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { objectId } from '@jig-software/zod-bson';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Note: AppSettings is for Dynamic props.
|
||||
|
||||
export const AppSettings = z.object({
|
||||
_id: objectId(),
|
||||
// Include other properties here.
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
});
|
||||
|
||||
export type AppSettings = z.infer<typeof AppSettings>;
|
||||
|
||||
export const AppSettingsInit = AppSettings.omit({
|
||||
_id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
|
||||
export type AppSettingsInit = z.infer<typeof AppSettingsInit>;
|
||||
2
src/shared/schemas/index.ts
Normal file
2
src/shared/schemas/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './AppSettings';
|
||||
export * from './readingReport';
|
||||
24
src/shared/schemas/readingReport.ts
Normal file
24
src/shared/schemas/readingReport.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import z from 'zod';
|
||||
import { objectId } from '@jig-software/zod-bson';
|
||||
|
||||
export const ReadingReport = z.object({
|
||||
reportId: objectId(), // ✅ call the function
|
||||
generatedAt: z
|
||||
.string()
|
||||
.refine((date) => !isNaN(Date.parse(date)), {
|
||||
message: 'Invalid date format',
|
||||
}),
|
||||
generatedBy: z.string(),
|
||||
readings: z.array(
|
||||
z.object({
|
||||
sensorId: objectId(), // ✅ call the function here too
|
||||
timestamp: z
|
||||
.string()
|
||||
.refine((date) => !isNaN(Date.parse(date)), {
|
||||
message: 'Invalid date format',
|
||||
}),
|
||||
value: z.number(),
|
||||
unit: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
8
test/sample.spec.ts
Normal file
8
test/sample.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { expect } from 'chai';
|
||||
|
||||
describe('sample', () => {
|
||||
it('should test all critical logic of the library', () => {
|
||||
const it = true;
|
||||
expect(it).to.be.eq(true);
|
||||
});
|
||||
});
|
||||
15
tsconfig.build.json
Normal file
15
tsconfig.build.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"incremental": false,
|
||||
"noEmit": false,
|
||||
"sourceMap": false,
|
||||
"removeComments": true,
|
||||
"declaration": false,
|
||||
"module": "CommonJS",
|
||||
"rootDir": "./src",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts", "test"]
|
||||
}
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2019",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"include": ["src", "test", ".eslintrc.js"]
|
||||
}
|
||||
0
volumes/.gitkeep
Normal file
0
volumes/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user