mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-18 20:59:42 +02:00
[Feature] Static pages rendering 🤩 (#274)
This commit is contained in:
parent
8c794304b6
commit
4ad37abed0
20 changed files with 554 additions and 705 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -75,7 +75,7 @@ db/
|
|||
.DS_Store
|
||||
|
||||
# Uploads
|
||||
/public/uploads
|
||||
/uploads
|
||||
/public/uploads_test
|
||||
|
||||
# Compiled files
|
||||
|
@ -83,3 +83,5 @@ db/
|
|||
/public/dist/*
|
||||
|
||||
*.local.yaml
|
||||
|
||||
static-build
|
||||
|
|
8
.npmignore
Normal file
8
.npmignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
*
|
||||
!public/**/*
|
||||
public/uploads
|
||||
!dist/**/*
|
||||
!package.json
|
||||
!README.md
|
||||
!yarn.lock
|
||||
!LICENSE
|
|
@ -18,7 +18,7 @@ yarn install
|
|||
### 3. Create separate config file for local overrides
|
||||
|
||||
```shell
|
||||
touch app-config.local.yaml
|
||||
touch docs-config.local.yaml
|
||||
```
|
||||
|
||||
### 4. Run the application
|
||||
|
@ -39,7 +39,7 @@ In order to use MongoDB, follow these steps:
|
|||
docker-compose up mongodb
|
||||
```
|
||||
|
||||
### 2. Setup MongoDB driver in app-config.local.yaml
|
||||
### 2. Setup MongoDB driver in docs-config.local.yaml
|
||||
|
||||
```yaml
|
||||
database:
|
||||
|
@ -73,7 +73,7 @@ By default, the application uses local filesystem to store files, but S3 driver
|
|||
### 1. Get credentials for S3 bucket
|
||||
Create a S3 bucket and get access key and secret key (or use existing ones)
|
||||
|
||||
### 2. Setup S3 driver in app-config.local.yaml
|
||||
### 2. Setup S3 driver in docs-config.local.yaml
|
||||
|
||||
```yaml
|
||||
uploads:
|
||||
|
|
|
@ -9,8 +9,8 @@ services:
|
|||
volumes:
|
||||
- ./public/uploads:/uploads
|
||||
- ./db:/usr/src/app/db
|
||||
- ./app-config.yaml:/usr/src/app/app-config.yaml
|
||||
- ./app-config.local.yaml:/usr/src/app/app-config.local.yaml
|
||||
- ./docs-config.yaml:/usr/src/app/docs-config.yaml
|
||||
- ./docs-config.local.yaml:/usr/src/app/docs-config.local.yaml
|
||||
mongodb:
|
||||
image: mongo:6.0.1
|
||||
ports:
|
||||
|
|
|
@ -16,9 +16,7 @@ RUN yarn install
|
|||
|
||||
COPY . .
|
||||
|
||||
RUN yarn build-frontend
|
||||
|
||||
RUN yarn compile
|
||||
RUN yarn build-all
|
||||
|
||||
# Stage 2 - make final image
|
||||
FROM node:16.14.0-alpine3.15
|
||||
|
@ -32,4 +30,4 @@ COPY --from=build /usr/src/app/public ./public
|
|||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
CMD ["node", "dist/bin/server.js"]
|
||||
CMD ["node", "dist/backend/server.js"]
|
||||
|
|
|
@ -3,7 +3,7 @@ host: "localhost"
|
|||
uploads:
|
||||
driver: "local"
|
||||
local:
|
||||
path: "./public/uploads"
|
||||
path: "./uploads"
|
||||
s3:
|
||||
bucket: "my-bucket"
|
||||
region: "eu-central-1"
|
14
package.json
14
package.json
|
@ -1,7 +1,11 @@
|
|||
{
|
||||
"name": "codex.docs",
|
||||
"license": "Apache-2.0",
|
||||
"version": "0.0.1-alpha.8",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"codex.docs": "dist/backend/app.js"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"> 1%"
|
||||
|
@ -9,8 +13,10 @@
|
|||
"scripts": {
|
||||
"start": "concurrently \"yarn start-backend\" \"yarn build-frontend\"",
|
||||
"dev": "concurrently \"yarn start-backend\" \"yarn build-frontend:dev\"",
|
||||
"start-backend": "cross-env NODE_ENV=development npx nodemon --config nodemon.json ./src/bin/server.ts -c app-config.yaml -c app-config.local.yaml",
|
||||
"compile": "tsc && copyfiles -u 3 ./src/**/*.twig ./dist/backend/views && copyfiles -u 1 ./src/**/*.svg ./dist/",
|
||||
"build-all": "yarn build-frontend && yarn build-backend",
|
||||
"build-static": "ts-node src/backend/app.ts build-static -c docs-config.yaml -c docs-config.local.yaml",
|
||||
"start-backend": "cross-env NODE_ENV=development npx nodemon --config nodemon.json src/backend/app.ts -c docs-config.yaml -c docs-config.local.yaml",
|
||||
"build-backend": "tsc && copyfiles -u 3 ./src/**/*.twig ./dist/backend/views && copyfiles -u 1 ./src/**/*.svg ./dist/",
|
||||
"build-frontend": "webpack --mode=production",
|
||||
"build-frontend:dev": "webpack --mode=development --watch",
|
||||
"test:js": "cross-env NODE_ENV=testing mocha --recursive ./dist/test --exit",
|
||||
|
@ -25,6 +31,7 @@
|
|||
"@hawk.so/javascript": "^3.0.1",
|
||||
"@hawk.so/nodejs": "^3.1.4",
|
||||
"@types/multer-s3": "^3.0.0",
|
||||
"@types/yargs": "^17.0.13",
|
||||
"arg": "^5.0.2",
|
||||
"config": "^3.3.6",
|
||||
"cookie-parser": "^1.4.5",
|
||||
|
@ -33,6 +40,7 @@
|
|||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.1",
|
||||
"file-type": "^16.5.4",
|
||||
"fs-extra": "^10.1.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mime": "^3.0.0",
|
||||
|
@ -47,6 +55,7 @@
|
|||
"open-graph-scraper": "^4.9.0",
|
||||
"twig": "^1.15.4",
|
||||
"uuid4": "^2.0.2",
|
||||
"yargs": "^17.6.0",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -78,6 +87,7 @@
|
|||
"@types/debug": "^4.1.7",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/file-type": "^10.9.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jsonwebtoken": "^8.5.4",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/mkdirp": "^1.0.2",
|
||||
|
|
|
@ -1,99 +1,20 @@
|
|||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import morgan from 'morgan';
|
||||
import routes from './routes/index.js';
|
||||
import HttpException from './exceptions/httpException.js';
|
||||
import * as dotenv from 'dotenv';
|
||||
import HawkCatcher from '@hawk.so/nodejs';
|
||||
import os from 'os';
|
||||
import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js';
|
||||
import appConfig from './utils/appConfig.js';
|
||||
import yargs from 'yargs';
|
||||
import { hideBin } from 'yargs/helpers';
|
||||
import runHttpServer from './server.js';
|
||||
import buildStatic from './build-static.js';
|
||||
|
||||
/**
|
||||
* The __dirname CommonJS variables are not available in ES modules.
|
||||
* https://nodejs.org/api/esm.html#no-__filename-or-__dirname
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
const localConfig = appConfig.frontend;
|
||||
|
||||
// Initialize the backend error tracking catcher.
|
||||
if (appConfig.hawk?.backendToken) {
|
||||
HawkCatcher.init(appConfig.hawk.backendToken);
|
||||
}
|
||||
|
||||
// Get url to upload favicon from config
|
||||
const favicon = appConfig.favicon;
|
||||
|
||||
app.locals.config = localConfig;
|
||||
// Set client error tracking token as app local.
|
||||
if (appConfig.hawk?.frontendToken) {
|
||||
app.locals.config.hawkClientToken = appConfig.hawk.frontendToken;
|
||||
}
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, './', 'views'));
|
||||
app.set('view engine', 'twig');
|
||||
import('./utils/twig.js');
|
||||
|
||||
const downloadedFaviconFolder = os.tmpdir();
|
||||
|
||||
// Check if favicon is not empty
|
||||
if (favicon) {
|
||||
// Upload favicon by url, it's path on server is '/temp/favicon.{format}'
|
||||
downloadFavicon(favicon, downloadedFaviconFolder).then((res) => {
|
||||
app.locals.favicon = res;
|
||||
console.log('Favicon successfully uploaded');
|
||||
yargs(hideBin(process.argv))
|
||||
.option('config', {
|
||||
alias: 'c',
|
||||
type: 'string',
|
||||
default: './docs-config.yaml',
|
||||
description: 'Config files paths',
|
||||
})
|
||||
.catch( (err) => {
|
||||
console.log(err);
|
||||
console.log('Favicon has not uploaded');
|
||||
});
|
||||
} else {
|
||||
console.log('Favicon is empty, using default path');
|
||||
app.locals.favicon = {
|
||||
destination: '/favicon.png',
|
||||
type: 'image/png',
|
||||
} as FaviconData;
|
||||
}
|
||||
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, '../../public')));
|
||||
|
||||
if (appConfig.uploads.driver === 'local') {
|
||||
app.use('/uploads', express.static(appConfig.uploads.local.path));
|
||||
}
|
||||
|
||||
app.use('/favicon', express.static(downloadedFaviconFolder));
|
||||
|
||||
app.use('/', routes);
|
||||
|
||||
|
||||
// global error handler
|
||||
app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) {
|
||||
// send any type of error to hawk server.
|
||||
if (appConfig.hawk?.backendToken && err instanceof Error) {
|
||||
HawkCatcher.send(err);
|
||||
}
|
||||
// only send Http based exception to client.
|
||||
if (err instanceof HttpException) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
|
||||
|
||||
export default app;
|
||||
.help('h')
|
||||
.alias('h', 'help')
|
||||
.command('$0', 'start the server', () => {/* empty */}, runHttpServer)
|
||||
.command('build-static', 'build files from database', () => {/* empty */}, async () => {
|
||||
await buildStatic();
|
||||
process.exit(0);
|
||||
})
|
||||
.parse();
|
||||
|
|
125
src/backend/build-static.ts
Normal file
125
src/backend/build-static.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import twig from 'twig';
|
||||
import Page from './models/page.js';
|
||||
import PagesFlatArray from './models/pagesFlatArray.js';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import('./utils/twig.js');
|
||||
import fs from 'fs/promises';
|
||||
import mkdirp from 'mkdirp';
|
||||
import { createMenuTree } from './utils/menu.js';
|
||||
import { EntityId } from './database/types.js';
|
||||
import PagesOrder from './controllers/pagesOrder.js';
|
||||
import fse from 'fs-extra';
|
||||
import appConfig from './utils/appConfig.js';
|
||||
import Aliases from './controllers/aliases.js';
|
||||
import Pages from './controllers/pages.js';
|
||||
|
||||
/**
|
||||
* Build static pages from database
|
||||
*/
|
||||
export default async function buildStatic(): Promise<void> {
|
||||
const config = appConfig.staticBuild;
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Static build config not found');
|
||||
}
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const cwd = process.cwd();
|
||||
const distPath = path.resolve(cwd, config.outputDir);
|
||||
|
||||
/**
|
||||
* Render template with twig by path
|
||||
*
|
||||
* @param filePath - path to template
|
||||
* @param data - data to render template
|
||||
*/
|
||||
function renderTemplate(filePath: string, data: Record<string, unknown>): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
twig.renderFile(path.resolve(dirname, filePath), data, (err, html) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(html);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Removing old static files');
|
||||
await fse.remove(distPath);
|
||||
|
||||
console.log('Building static files');
|
||||
const pagesOrder = await PagesOrder.getAll();
|
||||
const allPages = await Page.getAll();
|
||||
|
||||
await mkdirp(distPath);
|
||||
|
||||
/**
|
||||
* Renders single page
|
||||
*
|
||||
* @param page - page to render
|
||||
* @param isIndex - is this page index page
|
||||
*/
|
||||
async function renderPage(page: Page, isIndex?: boolean): Promise<void> {
|
||||
console.log(`Rendering page ${page.uri}`);
|
||||
const pageParent = await page.getParent();
|
||||
const pageId = page._id;
|
||||
|
||||
if (!pageId) {
|
||||
throw new Error('Page id is not defined');
|
||||
}
|
||||
const parentIdOfRootPages = '0' as EntityId;
|
||||
const previousPage = await PagesFlatArray.getPageBefore(pageId);
|
||||
const nextPage = await PagesFlatArray.getPageAfter(pageId);
|
||||
const menu = createMenuTree(parentIdOfRootPages, allPages, pagesOrder, 2);
|
||||
const result = await renderTemplate('./views/pages/page.twig', {
|
||||
page,
|
||||
pageParent,
|
||||
previousPage,
|
||||
nextPage,
|
||||
menu,
|
||||
config: appConfig.frontend,
|
||||
});
|
||||
|
||||
const filename = (isIndex || page.uri === '') ? 'index.html' : `${page.uri}.html`;
|
||||
|
||||
await fs.writeFile(path.resolve(distPath, filename), result);
|
||||
console.log(`Page ${page.uri} rendered`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render index page
|
||||
*
|
||||
* @param indexPageUri - uri of index page
|
||||
*/
|
||||
async function renderIndexPage(indexPageUri: string): Promise<void> {
|
||||
const alias = await Aliases.get(indexPageUri);
|
||||
|
||||
if (!alias.id) {
|
||||
throw new Error(`Alias ${indexPageUri} not found`);
|
||||
}
|
||||
|
||||
const page = await Pages.get(alias.id);
|
||||
|
||||
await renderPage(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all pages
|
||||
*/
|
||||
for (const page of allPages) {
|
||||
await renderPage(page);
|
||||
}
|
||||
|
||||
await renderIndexPage(config.indexPageUri);
|
||||
console.log('Static files built');
|
||||
|
||||
console.log('Copy public directory');
|
||||
await fse.copy(path.resolve(dirname, '../../public'), distPath);
|
||||
|
||||
if (appConfig.uploads.driver === 'local') {
|
||||
console.log('Copy uploads directory');
|
||||
await fse.copy(path.resolve(cwd, appConfig.uploads.local.path), path.resolve(distPath, 'uploads'));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +1,10 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import Pages from '../../controllers/pages.js';
|
||||
import PagesOrder from '../../controllers/pagesOrder.js';
|
||||
import Page from '../../models/page.js';
|
||||
import asyncMiddleware from '../../utils/asyncMiddleware.js';
|
||||
import PageOrder from '../../models/pageOrder.js';
|
||||
import { EntityId } from '../../database/types.js';
|
||||
import { isEqualIds } from '../../database/index.js';
|
||||
import { createMenuTree } from '../../utils/menu.js';
|
||||
|
||||
/**
|
||||
* Process one-level pages list to parent-children list
|
||||
*
|
||||
* @param {string} parentPageId - parent page id
|
||||
* @param {Page[]} pages - list of all available pages
|
||||
* @param {PagesOrder[]} pagesOrder - list of pages order
|
||||
* @param {number} level - max level recursion
|
||||
* @param {number} currentLevel - current level of element
|
||||
* @returns {Page[]}
|
||||
*/
|
||||
function createMenuTree(parentPageId: EntityId, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
|
||||
const childrenOrder = pagesOrder.find(order => isEqualIds(order.data.page, parentPageId));
|
||||
|
||||
/**
|
||||
* branch is a page children in tree
|
||||
* if we got some children order on parents tree, then we push found pages in order sequence
|
||||
* otherwise just find all pages includes parent tree
|
||||
*/
|
||||
let ordered: any[] = [];
|
||||
|
||||
if (childrenOrder) {
|
||||
ordered = childrenOrder.order.map((pageId: EntityId) => {
|
||||
return pages.find(page => isEqualIds(page._id, pageId));
|
||||
});
|
||||
}
|
||||
|
||||
const unordered = pages.filter(page => isEqualIds(page._parent, parentPageId));
|
||||
const branch = Array.from(new Set([...ordered, ...unordered]));
|
||||
|
||||
/**
|
||||
* stop recursion when we got the passed max level
|
||||
*/
|
||||
if (currentLevel === level + 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Each parents children can have subbranches
|
||||
*/
|
||||
return branch.filter(page => page && page._id).map(page => {
|
||||
return Object.assign({
|
||||
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
|
||||
}, page.data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware for all /page/... routes
|
||||
|
|
210
src/backend/server.ts
Normal file
210
src/backend/server.ts
Normal file
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
import http from 'http';
|
||||
import Debug from 'debug';
|
||||
import appConfig from './utils/appConfig.js';
|
||||
import { drawBanner } from './utils/banner.js';
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import HawkCatcher from '@hawk.so/nodejs';
|
||||
import os from 'os';
|
||||
import { downloadFavicon, FaviconData } from './utils/downloadFavicon.js';
|
||||
import morgan from 'morgan';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import routes from './routes/index.js';
|
||||
import HttpException from './exceptions/httpException.js';
|
||||
|
||||
const debug = Debug.debug('codex.docs:server');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
const port = normalizePort(appConfig.port.toString() || '3000');
|
||||
|
||||
/**
|
||||
* Create Express server
|
||||
*/
|
||||
function createApp(): express.Express {
|
||||
/**
|
||||
* The __dirname CommonJS variables are not available in ES modules.
|
||||
* https://nodejs.org/api/esm.html#no-__filename-or-__dirname
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const app = express();
|
||||
const localConfig = appConfig.frontend;
|
||||
|
||||
// Initialize the backend error tracking catcher.
|
||||
if (appConfig.hawk?.backendToken) {
|
||||
HawkCatcher.init(appConfig.hawk.backendToken);
|
||||
}
|
||||
|
||||
// Get url to upload favicon from config
|
||||
const favicon = appConfig.favicon;
|
||||
|
||||
app.locals.config = localConfig;
|
||||
// Set client error tracking token as app local.
|
||||
if (appConfig.hawk?.frontendToken) {
|
||||
app.locals.config.hawkClientToken = appConfig.hawk.frontendToken;
|
||||
}
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, './', 'views'));
|
||||
app.set('view engine', 'twig');
|
||||
import('./utils/twig.js');
|
||||
|
||||
const downloadedFaviconFolder = os.tmpdir();
|
||||
|
||||
// Check if favicon is not empty
|
||||
if (favicon) {
|
||||
// Upload favicon by url, it's path on server is '/temp/favicon.{format}'
|
||||
downloadFavicon(favicon, downloadedFaviconFolder).then((res) => {
|
||||
app.locals.favicon = res;
|
||||
console.log('Favicon successfully uploaded');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.log('Favicon has not uploaded');
|
||||
});
|
||||
} else {
|
||||
console.log('Favicon is empty, using default path');
|
||||
app.locals.favicon = {
|
||||
destination: '/favicon.png',
|
||||
type: 'image/png',
|
||||
} as FaviconData;
|
||||
}
|
||||
|
||||
app.use(morgan('dev'));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, '../../public')));
|
||||
|
||||
if (appConfig.uploads.driver === 'local') {
|
||||
app.use('/uploads', express.static(appConfig.uploads.local.path));
|
||||
}
|
||||
|
||||
app.use('/favicon', express.static(downloadedFaviconFolder));
|
||||
|
||||
app.use('/', routes);
|
||||
|
||||
|
||||
// global error handler
|
||||
app.use(function (err: unknown, req: Request, res: Response, next: NextFunction) {
|
||||
// send any type of error to hawk server.
|
||||
if (appConfig.hawk?.backendToken && err instanceof Error) {
|
||||
HawkCatcher.send(err);
|
||||
}
|
||||
// only send Http based exception to client.
|
||||
if (err instanceof HttpException) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
}
|
||||
next(err);
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and run HTTP server.
|
||||
*/
|
||||
export default function runHttpServer(): void {
|
||||
const app = createApp();
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server 'listening' event.
|
||||
*/
|
||||
function onListening(): void {
|
||||
const addr = server.address();
|
||||
|
||||
if (addr === null) {
|
||||
debug('Address not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
|
||||
debug('Listening on ' + bind);
|
||||
|
||||
drawBanner([
|
||||
`CodeX Docs server is running`,
|
||||
``,
|
||||
`Main page: http://localhost:${port}`,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*
|
||||
* @param val
|
||||
*/
|
||||
function normalizePort(val: string): number | string | false {
|
||||
const value = parseInt(val, 10);
|
||||
|
||||
if (isNaN(value)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (value >= 0) {
|
||||
// port number
|
||||
return value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server 'error' event.
|
||||
*
|
||||
* @param error
|
||||
*/
|
||||
function onError(error: NodeJS.ErrnoException): void {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -84,6 +84,16 @@ const FrontendConfig = z.object({
|
|||
uri: z.string() })])), // Menu for pages
|
||||
});
|
||||
|
||||
/**
|
||||
* Static build configuration
|
||||
*/
|
||||
const StaticBuildConfig = z.object({
|
||||
outputDir: z.string(), // Output directory for static build
|
||||
indexPageUri: z.string(), // URI for index page to render
|
||||
});
|
||||
|
||||
export type StaticBuildConfig = z.infer<typeof StaticBuildConfig>;
|
||||
|
||||
/**
|
||||
* Application configuration
|
||||
*/
|
||||
|
@ -97,6 +107,7 @@ const AppConfig = z.object({
|
|||
frontend: FrontendConfig, // Frontend configuration
|
||||
auth: AuthConfig, // Auth configuration
|
||||
database: z.union([LocalDatabaseConfig, MongoDatabaseConfig]), // Database configuration
|
||||
staticBuild: StaticBuildConfig.optional(), // Static build configuration
|
||||
});
|
||||
|
||||
export type AppConfig = z.infer<typeof AppConfig>;
|
||||
|
@ -107,7 +118,7 @@ const args = arg({ /* eslint-disable @typescript-eslint/naming-convention */
|
|||
});
|
||||
|
||||
const cwd = process.cwd();
|
||||
const paths = (args['--config'] || [ './app-config.yaml' ]).map((configPath) => {
|
||||
const paths = (args['--config'] || [ './docs-config.yaml' ]).map((configPath) => {
|
||||
if (path.isAbsolute(configPath)) {
|
||||
return configPath;
|
||||
}
|
||||
|
|
33
src/backend/utils/banner.ts
Normal file
33
src/backend/utils/banner.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Draw banner in console with given text lines
|
||||
*
|
||||
* @param lines - data to draw
|
||||
*/
|
||||
export function drawBanner(lines: string[]): void {
|
||||
/** Define banner parts */
|
||||
const PARTS = {
|
||||
TOP_LEFT: '┌',
|
||||
TOP_RIGHT: '┐',
|
||||
BOTTOM_LEFT: '└',
|
||||
BOTTOM_RIGHT: '┘',
|
||||
HORIZONTAL: '─',
|
||||
VERTICAL: '│',
|
||||
SPACE: ' ',
|
||||
};
|
||||
|
||||
/** Calculate max line length */
|
||||
const maxLength = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
||||
|
||||
/** Prepare top line */
|
||||
const top = PARTS.TOP_LEFT + PARTS.HORIZONTAL.repeat(maxLength + 2) + PARTS.TOP_RIGHT;
|
||||
|
||||
/** Compose middle lines */
|
||||
const middle = lines.map(line => PARTS.VERTICAL + ' ' + line + PARTS.SPACE.repeat(maxLength - line.length) + ' ' + PARTS.VERTICAL);
|
||||
|
||||
/** Prepare bottom line */
|
||||
const bottom = PARTS.BOTTOM_LEFT + PARTS.HORIZONTAL.repeat(maxLength + 2) + PARTS.BOTTOM_RIGHT;
|
||||
|
||||
console.log(top);
|
||||
console.log(middle.join('\n'));
|
||||
console.log(bottom);
|
||||
}
|
49
src/backend/utils/menu.ts
Normal file
49
src/backend/utils/menu.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { EntityId } from '../database/types.js';
|
||||
import Page from '../models/page.js';
|
||||
import PageOrder from '../models/pageOrder.js';
|
||||
import { isEqualIds } from '../database/index.js';
|
||||
|
||||
/**
|
||||
* Process one-level pages list to parent-children list
|
||||
*
|
||||
* @param parentPageId - parent page id
|
||||
* @param pages - list of all available pages
|
||||
* @param pagesOrder - list of pages order
|
||||
* @param level - max level recursion
|
||||
* @param currentLevel - current level of element
|
||||
*/
|
||||
export function createMenuTree(parentPageId: EntityId, pages: Page[], pagesOrder: PageOrder[], level = 1, currentLevel = 1): Page[] {
|
||||
const childrenOrder = pagesOrder.find(order => isEqualIds(order.data.page, parentPageId));
|
||||
|
||||
/**
|
||||
* branch is a page children in tree
|
||||
* if we got some children order on parents tree, then we push found pages in order sequence
|
||||
* otherwise just find all pages includes parent tree
|
||||
*/
|
||||
let ordered: any[] = [];
|
||||
|
||||
if (childrenOrder) {
|
||||
ordered = childrenOrder.order.map((pageId: EntityId) => {
|
||||
return pages.find(page => isEqualIds(page._id, pageId));
|
||||
});
|
||||
}
|
||||
|
||||
const unordered = pages.filter(page => isEqualIds(page._parent, parentPageId));
|
||||
const branch = Array.from(new Set([...ordered, ...unordered]));
|
||||
|
||||
/**
|
||||
* stop recursion when we got the passed max level
|
||||
*/
|
||||
if (currentLevel === level + 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Each parents children can have subbranches
|
||||
*/
|
||||
return branch.filter(page => page && page._id).map(page => {
|
||||
return Object.assign({
|
||||
children: createMenuTree(page._id, pages, pagesOrder, level, currentLevel + 1),
|
||||
}, page.data);
|
||||
});
|
||||
}
|
|
@ -12,13 +12,13 @@
|
|||
<script>
|
||||
</script>
|
||||
<body class="greeting-body">
|
||||
{% include "components/header.twig" %}
|
||||
{% include "../components/header.twig" %}
|
||||
<div class="greeting-content">
|
||||
{{ svg('frog') }}
|
||||
<p class="greeting-content__message">
|
||||
It’s time to create the first page!
|
||||
</p>
|
||||
{% include 'components/button.twig' with {label: 'Add page', icon: 'plus', size: 'small', url: '/page/new'} %}
|
||||
{% include '../components/button.twig' with {label: 'Add page', icon: 'plus', size: 'small', url: '/page/new'} %}
|
||||
</div>
|
||||
{% if config.yandexMetrikaId is not empty %}
|
||||
<script type="text/javascript" >
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% extends 'layout.twig' %}
|
||||
{% extends '../layout.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<article class="page" data-module="page">
|
||||
|
@ -44,7 +44,7 @@
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% include 'components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
|
||||
{% include '../components/navigator.twig' with {previousPage: previousPage, nextPage: nextPage} %}
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
||||
|
|
400
src/bin/nvm.sh
400
src/bin/nvm.sh
|
@ -1,400 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
{ # this ensures the entire script is downloaded #
|
||||
|
||||
nvm_has() {
|
||||
type "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
nvm_install_dir() {
|
||||
command printf %s "${NVM_DIR:-"$HOME/.nvm"}"
|
||||
}
|
||||
|
||||
nvm_latest_version() {
|
||||
echo "v0.33.11"
|
||||
}
|
||||
|
||||
nvm_profile_is_bash_or_zsh() {
|
||||
local TEST_PROFILE
|
||||
TEST_PROFILE="${1-}"
|
||||
case "${TEST_PROFILE-}" in
|
||||
*"/.bashrc" | *"/.bash_profile" | *"/.zshrc")
|
||||
return
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
#
|
||||
# Outputs the location to NVM depending on:
|
||||
# * The availability of $NVM_SOURCE
|
||||
# * The method used ("script" or "git" in the script, defaults to "git")
|
||||
# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec"
|
||||
#
|
||||
nvm_source() {
|
||||
local NVM_METHOD
|
||||
NVM_METHOD="$1"
|
||||
local NVM_SOURCE_URL
|
||||
NVM_SOURCE_URL="$NVM_SOURCE"
|
||||
if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/creationix/nvm/$(nvm_latest_version)/nvm-exec"
|
||||
elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/creationix/nvm/$(nvm_latest_version)/bash_completion"
|
||||
elif [ -z "$NVM_SOURCE_URL" ]; then
|
||||
if [ "_$NVM_METHOD" = "_script" ]; then
|
||||
NVM_SOURCE_URL="https://raw.githubusercontent.com/creationix/nvm/$(nvm_latest_version)/nvm.sh"
|
||||
elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then
|
||||
NVM_SOURCE_URL="https://github.com/creationix/nvm.git"
|
||||
else
|
||||
echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
echo "$NVM_SOURCE_URL"
|
||||
}
|
||||
|
||||
#
|
||||
# Node.js version to install
|
||||
#
|
||||
nvm_node_version() {
|
||||
echo "$NODE_VERSION"
|
||||
}
|
||||
|
||||
nvm_download() {
|
||||
if nvm_has "curl"; then
|
||||
curl --compressed -q "$@"
|
||||
elif nvm_has "wget"; then
|
||||
# Emulate curl with wget
|
||||
ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \
|
||||
-e 's/-L //' \
|
||||
-e 's/--compressed //' \
|
||||
-e 's/-I /--server-response /' \
|
||||
-e 's/-s /-q /' \
|
||||
-e 's/-o /-O /' \
|
||||
-e 's/-C - /-c /')
|
||||
# shellcheck disable=SC2086
|
||||
eval wget $ARGS
|
||||
fi
|
||||
}
|
||||
|
||||
install_nvm_from_git() {
|
||||
local INSTALL_DIR
|
||||
INSTALL_DIR="$(nvm_install_dir)"
|
||||
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git"
|
||||
command printf '\r=> '
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$(nvm_latest_version)" --depth=1 2> /dev/null || {
|
||||
echo >&2 "Failed to update nvm, run 'git fetch' in $INSTALL_DIR yourself."
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
# Cloning to $INSTALL_DIR
|
||||
echo "=> Downloading nvm from git to '$INSTALL_DIR'"
|
||||
command printf '\r=> '
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
if [ "$(ls -A "${INSTALL_DIR}")" ]; then
|
||||
command git init "${INSTALL_DIR}" || {
|
||||
echo >&2 'Failed to initialize nvm repo. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \
|
||||
|| command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || {
|
||||
echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!'
|
||||
exit 2
|
||||
}
|
||||
command git --git-dir="${INSTALL_DIR}/.git" fetch origin tag "$(nvm_latest_version)" --depth=1 || {
|
||||
echo >&2 'Failed to fetch origin with tags. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
else
|
||||
command git -c advice.detachedHead=false clone "$(nvm_source)" -b "$(nvm_latest_version)" --depth=1 "${INSTALL_DIR}" || {
|
||||
echo >&2 'Failed to clone nvm repo. Please report this!'
|
||||
exit 2
|
||||
}
|
||||
fi
|
||||
fi
|
||||
command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet "$(nvm_latest_version)"
|
||||
if [ ! -z "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then
|
||||
if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1
|
||||
else
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=> Compressing and cleaning up git repository"
|
||||
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
fi
|
||||
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then
|
||||
echo >&2 "Your version of git is out of date. Please update it!"
|
||||
fi
|
||||
return
|
||||
}
|
||||
|
||||
#
|
||||
# Automatically install Node.js
|
||||
#
|
||||
nvm_install_node() {
|
||||
local NODE_VERSION_LOCAL
|
||||
NODE_VERSION_LOCAL="$(nvm_node_version)"
|
||||
|
||||
if [ -z "$NODE_VERSION_LOCAL" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "=> Installing Node.js version $NODE_VERSION_LOCAL"
|
||||
nvm install "$NODE_VERSION_LOCAL"
|
||||
local CURRENT_NVM_NODE
|
||||
|
||||
CURRENT_NVM_NODE="$(nvm_version current)"
|
||||
if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then
|
||||
echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed"
|
||||
else
|
||||
echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL"
|
||||
fi
|
||||
}
|
||||
|
||||
install_nvm_as_script() {
|
||||
local INSTALL_DIR
|
||||
INSTALL_DIR="$(nvm_install_dir)"
|
||||
local NVM_SOURCE_LOCAL
|
||||
NVM_SOURCE_LOCAL="$(nvm_source script)"
|
||||
local NVM_EXEC_SOURCE
|
||||
NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)"
|
||||
local NVM_BASH_COMPLETION_SOURCE
|
||||
NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)"
|
||||
|
||||
# Downloading to $INSTALL_DIR
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
if [ -f "$INSTALL_DIR/nvm.sh" ]; then
|
||||
echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script"
|
||||
else
|
||||
echo "=> Downloading nvm as script to '$INSTALL_DIR'"
|
||||
fi
|
||||
nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || {
|
||||
echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'"
|
||||
return 1
|
||||
} &
|
||||
nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || {
|
||||
echo >&2 "Failed to download '$NVM_EXEC_SOURCE'"
|
||||
return 2
|
||||
} &
|
||||
nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || {
|
||||
echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'"
|
||||
return 2
|
||||
} &
|
||||
for job in $(jobs -p | command sort)
|
||||
do
|
||||
wait "$job" || return $?
|
||||
done
|
||||
chmod a+x "$INSTALL_DIR/nvm-exec" || {
|
||||
echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable"
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
nvm_try_profile() {
|
||||
if [ -z "${1-}" ] || [ ! -f "${1}" ]; then
|
||||
return 1
|
||||
fi
|
||||
echo "${1}"
|
||||
}
|
||||
|
||||
#
|
||||
# Detect profile file if not specified as environment variable
|
||||
# (eg: PROFILE=~/.myprofile)
|
||||
# The echo'ed path is guaranteed to be an existing file
|
||||
# Otherwise, an empty string is returned
|
||||
#
|
||||
nvm_detect_profile() {
|
||||
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then
|
||||
echo "${PROFILE}"
|
||||
return
|
||||
fi
|
||||
|
||||
local DETECTED_PROFILE
|
||||
DETECTED_PROFILE=''
|
||||
|
||||
if [ -n "${BASH_VERSION-}" ]; then
|
||||
if [ -f "$HOME/.bashrc" ]; then
|
||||
DETECTED_PROFILE="$HOME/.bashrc"
|
||||
elif [ -f "$HOME/.bash_profile" ]; then
|
||||
DETECTED_PROFILE="$HOME/.bash_profile"
|
||||
fi
|
||||
elif [ -n "${ZSH_VERSION-}" ]; then
|
||||
DETECTED_PROFILE="$HOME/.zshrc"
|
||||
fi
|
||||
|
||||
if [ -z "$DETECTED_PROFILE" ]; then
|
||||
for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zshrc"
|
||||
do
|
||||
if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ! -z "$DETECTED_PROFILE" ]; then
|
||||
echo "$DETECTED_PROFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Check whether the user has any globally-installed npm modules in their system
|
||||
# Node, and warn them if so.
|
||||
#
|
||||
nvm_check_global_modules() {
|
||||
command -v npm >/dev/null 2>&1 || return 0
|
||||
|
||||
local NPM_VERSION
|
||||
NPM_VERSION="$(npm --version)"
|
||||
NPM_VERSION="${NPM_VERSION:--1}"
|
||||
[ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0
|
||||
|
||||
local NPM_GLOBAL_MODULES
|
||||
NPM_GLOBAL_MODULES="$(
|
||||
npm list -g --depth=0 |
|
||||
command sed -e '/ npm@/d' -e '/ (empty)$/d'
|
||||
)"
|
||||
|
||||
local MODULE_COUNT
|
||||
MODULE_COUNT="$(
|
||||
command printf %s\\n "$NPM_GLOBAL_MODULES" |
|
||||
command sed -ne '1!p' | # Remove the first line
|
||||
wc -l | command tr -d ' ' # Count entries
|
||||
)"
|
||||
|
||||
if [ "${MODULE_COUNT}" != '0' ]; then
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> You currently have modules installed globally with `npm`. These will no'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> longer be linked to the active version of Node when you install a new node'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> override the binaries of modules installed with `nvm`:'
|
||||
echo
|
||||
|
||||
command printf %s\\n "$NPM_GLOBAL_MODULES"
|
||||
echo '=> If you wish to uninstall them at a later point (or re-install them under your'
|
||||
# shellcheck disable=SC2016
|
||||
echo '=> `nvm` Nodes), you can remove them from the system Node as follows:'
|
||||
echo
|
||||
echo ' $ nvm use system'
|
||||
echo ' $ npm uninstall -g a_module'
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
nvm_do_install() {
|
||||
if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then
|
||||
echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment."
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${METHOD}" ]; then
|
||||
# Autodetect install method
|
||||
if nvm_has git; then
|
||||
install_nvm_from_git
|
||||
elif nvm_has nvm_download; then
|
||||
install_nvm_as_script
|
||||
else
|
||||
echo >&2 'You need git, curl, or wget to install nvm'
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${METHOD}" = 'git' ]; then
|
||||
if ! nvm_has git; then
|
||||
echo >&2 "You need git to install nvm"
|
||||
exit 1
|
||||
fi
|
||||
install_nvm_from_git
|
||||
elif [ "${METHOD}" = 'script' ]; then
|
||||
if ! nvm_has nvm_download; then
|
||||
echo >&2 "You need curl or wget to install nvm"
|
||||
exit 1
|
||||
fi
|
||||
install_nvm_as_script
|
||||
fi
|
||||
|
||||
echo
|
||||
|
||||
local NVM_PROFILE
|
||||
NVM_PROFILE="$(nvm_detect_profile)"
|
||||
local PROFILE_INSTALL_DIR
|
||||
PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")"
|
||||
|
||||
SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n"
|
||||
|
||||
# shellcheck disable=SC2016
|
||||
COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n'
|
||||
BASH_OR_ZSH=false
|
||||
|
||||
if [ -z "${NVM_PROFILE-}" ] ; then
|
||||
local TRIED_PROFILE
|
||||
if [ -n "${PROFILE}" ]; then
|
||||
TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), "
|
||||
fi
|
||||
echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile."
|
||||
echo "=> Create one of them and run this script again"
|
||||
echo " OR"
|
||||
echo "=> Append the following lines to the correct file yourself:"
|
||||
command printf "${SOURCE_STR}"
|
||||
echo
|
||||
else
|
||||
if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then
|
||||
BASH_OR_ZSH=true
|
||||
fi
|
||||
if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then
|
||||
echo "=> Appending nvm source string to $NVM_PROFILE"
|
||||
command printf "${SOURCE_STR}" >> "$NVM_PROFILE"
|
||||
else
|
||||
echo "=> nvm source string already in ${NVM_PROFILE}"
|
||||
fi
|
||||
# shellcheck disable=SC2016
|
||||
if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then
|
||||
echo "=> Appending bash_completion source string to $NVM_PROFILE"
|
||||
command printf "$COMPLETION_STR" >> "$NVM_PROFILE"
|
||||
else
|
||||
echo "=> bash_completion source string already in ${NVM_PROFILE}"
|
||||
fi
|
||||
fi
|
||||
if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then
|
||||
echo "=> Please also append the following lines to the if you are using bash/zsh shell:"
|
||||
command printf "${COMPLETION_STR}"
|
||||
fi
|
||||
|
||||
# Source nvm
|
||||
# shellcheck source=/dev/null
|
||||
\. "$(nvm_install_dir)/nvm.sh"
|
||||
|
||||
nvm_check_global_modules
|
||||
|
||||
nvm_install_node
|
||||
|
||||
nvm_reset
|
||||
|
||||
echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:"
|
||||
command printf "${SOURCE_STR}"
|
||||
if ${BASH_OR_ZSH} ; then
|
||||
command printf "${COMPLETION_STR}"
|
||||
fi
|
||||
}
|
||||
|
||||
#
|
||||
# Unsets the various functions defined
|
||||
# during the execution of the install script
|
||||
#
|
||||
nvm_reset() {
|
||||
unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \
|
||||
nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \
|
||||
install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \
|
||||
nvm_do_install nvm_reset
|
||||
}
|
||||
|
||||
[ "_$NVM_ENV" = "_testing" ] || nvm_do_install
|
||||
|
||||
} # this ensures the entire script is downloaded #
|
|
@ -1,138 +0,0 @@
|
|||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
import app from '../backend/app.js';
|
||||
import http from 'http';
|
||||
import Debug from 'debug';
|
||||
import appConfig from "../backend/utils/appConfig.js";
|
||||
|
||||
const debug = Debug.debug('codex.editor.docs:server');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
const port = normalizePort(appConfig.port.toString() || '3000');
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
* @param val
|
||||
*/
|
||||
function normalizePort(val: string): number | string | false {
|
||||
const value = parseInt(val, 10);
|
||||
|
||||
if (isNaN(value)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (value >= 0) {
|
||||
// port number
|
||||
return value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server 'error' event.
|
||||
* @param error
|
||||
*/
|
||||
function onError(error: NodeJS.ErrnoException): void {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server 'listening' event.
|
||||
*/
|
||||
function onListening(): void {
|
||||
const addr = server.address();
|
||||
|
||||
if (addr === null) {
|
||||
debug('Address not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
|
||||
debug('Listening on ' + bind);
|
||||
|
||||
drawBanner([
|
||||
`CodeX Docs server is running`,
|
||||
``,
|
||||
`Main page: http://localhost:${port}`
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw banner in console with given text lines
|
||||
* @param {string[]} lines
|
||||
*/
|
||||
function drawBanner(lines: string[]) {
|
||||
/** Define banner parts */
|
||||
const PARTS = {
|
||||
TOP_LEFT: '┌',
|
||||
TOP_RIGHT: '┐',
|
||||
BOTTOM_LEFT: '└',
|
||||
BOTTOM_RIGHT: '┘',
|
||||
HORIZONTAL: '─',
|
||||
VERTICAL: '│',
|
||||
SPACE: ' ',
|
||||
}
|
||||
|
||||
/** Calculate max line length */
|
||||
const maxLength = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
||||
|
||||
/** Prepare top line */
|
||||
const top = PARTS.TOP_LEFT + PARTS.HORIZONTAL.repeat(maxLength + 2) + PARTS.TOP_RIGHT;
|
||||
|
||||
/** Compose middle lines */
|
||||
const middle = lines.map(line => PARTS.VERTICAL + ' ' + line + PARTS.SPACE.repeat(maxLength - line.length) + ' ' + PARTS.VERTICAL);
|
||||
|
||||
/** Prepare bottom line */
|
||||
const bottom = PARTS.BOTTOM_LEFT + PARTS.HORIZONTAL.repeat(maxLength + 2) + PARTS.BOTTOM_RIGHT;
|
||||
|
||||
console.log(top);
|
||||
console.log(middle.join('\n'));
|
||||
console.log(bottom);
|
||||
}
|
||||
|
||||
export default {
|
||||
server,
|
||||
app,
|
||||
};
|
|
@ -16,7 +16,7 @@
|
|||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist/", /* Redirect output structure to the directory. */
|
||||
"outDir": "./dist/backend", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
|
@ -74,12 +74,15 @@
|
|||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/backend/**/*"],
|
||||
"ts-node": {
|
||||
/**
|
||||
* Tell ts-node CLI to install the --loader automatically, explained below
|
||||
* https://typestrong.org/ts-node/docs/imports/
|
||||
*/
|
||||
"esm": true
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"src/test/**/*"
|
||||
]
|
||||
}
|
||||
|
|
66
yarn.lock
66
yarn.lock
|
@ -2216,6 +2216,13 @@
|
|||
dependencies:
|
||||
file-type "*"
|
||||
|
||||
"@types/fs-extra@^9.0.13":
|
||||
version "9.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
|
||||
integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/glob@*":
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
|
||||
|
@ -2397,6 +2404,18 @@
|
|||
"@types/node" "*"
|
||||
"@types/webidl-conversions" "*"
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "21.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
|
||||
integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==
|
||||
|
||||
"@types/yargs@^17.0.13":
|
||||
version "17.0.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.13.tgz#34cced675ca1b1d51fcf4d34c3c6f0fa142a5c76"
|
||||
integrity sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.38.0":
|
||||
version "5.38.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz#9f05d42fa8fb9f62304cc2f5c2805e03c01c2620"
|
||||
|
@ -3152,6 +3171,15 @@ cliui@^7.0.2:
|
|||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
cliui@^8.0.1:
|
||||
version "8.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
|
||||
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
|
||||
dependencies:
|
||||
string-width "^4.2.0"
|
||||
strip-ansi "^6.0.1"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clone-deep@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
|
||||
|
@ -4315,6 +4343,15 @@ fresh@0.5.2:
|
|||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
|
||||
fs-extra@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
|
||||
dependencies:
|
||||
graceful-fs "^4.2.0"
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
|
@ -4467,7 +4504,7 @@ got@^11.8.5:
|
|||
p-cancelable "^2.0.0"
|
||||
responselike "^2.0.0"
|
||||
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9:
|
||||
version "4.2.10"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
|
||||
|
@ -4936,6 +4973,15 @@ json5@^2.1.1, json5@^2.1.2, json5@^2.2.1:
|
|||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
|
||||
dependencies:
|
||||
universalify "^2.0.0"
|
||||
optionalDependencies:
|
||||
graceful-fs "^4.1.6"
|
||||
|
||||
jsonwebtoken@^8.5.1:
|
||||
version "8.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
||||
|
@ -7135,6 +7181,11 @@ uniq@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
|
@ -7441,6 +7492,19 @@ yargs@^17.3.1:
|
|||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
yargs@^17.6.0:
|
||||
version "17.6.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.0.tgz#e134900fc1f218bc230192bdec06a0a5f973e46c"
|
||||
integrity sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==
|
||||
dependencies:
|
||||
cliui "^8.0.1"
|
||||
escalade "^3.1.1"
|
||||
get-caller-file "^2.0.5"
|
||||
require-directory "^2.1.1"
|
||||
string-width "^4.2.3"
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^21.0.0"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue