Skip to content

Commit

Permalink
feat(view): add template render plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
cxtom committed May 21, 2021
1 parent acdd3a5 commit cdf4682
Show file tree
Hide file tree
Showing 12 changed files with 2,623 additions and 377 deletions.
4 changes: 3 additions & 1 deletion example/hoth-quickstart/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type {FastifyInstance} from 'fastify';
import type {AppConfig} from '@hoth/app-autoload';

export default async function main(fastify, opts) {
export default async function main(fastify: FastifyInstance, opts: AppConfig) {
console.log('app entry plugin options', opts);
return;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export default class Calculator {
private service = getFastifyInstanceByAppName('quickstart');

add(a: number, b: number) {
return a + b + this.service.$appConfig.get('test');
return a + b + this.service!.$appConfig.get('test');
}
}
4 changes: 2 additions & 2 deletions example/hoth-quickstart/src/plugin/foo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* @file default plugin
* @author
*/
import type {FastifyInstance} from 'fastify';

export default function (fastify, opts, next) {
export default async function (fastify: FastifyInstance, opts: typeof autoConfig) {
console.log('foo plugin options', opts);
next();
}

export const autoConfig = {
Expand Down
51 changes: 51 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const path = require('path');
const {getPackagesSync} = require('@lerna/project');
const {configureLinter} = require('lerna-jest/lib/linter');
const {configureProject} = require('lerna-jest/lib/project');
const {configureSuite} = require('lerna-jest/lib/suite');

function nonEmpty(item) {
return Boolean(item);
}

function guessProjectConfig(rootDir) {
const integration = configureSuite(rootDir, 'integration', {
moduleFileExtensions: ['js', 'ts'],
transform: {
'^.+\\.(ts)$': 'ts-jest',
},
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
});
const linter = configureLinter(rootDir);
return configureProject(
rootDir,
[integration, linter].filter(nonEmpty)
);
}

function guessRootConfig(directory) {
const packages = getPackagesSync(directory);
const project = configureProject(
directory,
packages.reduce(
(aggr, pkg) => aggr.concat(guessProjectConfig(pkg.location).projects),
[]
),
{
collectCoverage: true,
collectCoverageFrom: [
'**/*.ts',
'!**/*.d.ts',
'!**/__fixtures__/**',
'!**/coverage/**',
'!**/templates/**',
'!**/example/**',
'!**/node_modules/**',
],
}
)
process.env.NODE_PATH = path.join(directory, 'packages');
return project;
}

module.exports = guessRootConfig(__dirname);
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"postversionup:major": "npm run commit-version",
"commit-version": "git add . && git commit -m \"chore(release): bump version\"",
"release": "lerna publish from-package",
"commitlint": "commitlint --edit"
"commitlint": "commitlint --edit",
"test": "jest"
},
"lint-staged": {
"**/*.ts": [
Expand All @@ -56,18 +57,24 @@
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@ecomfe/eslint-config": "^7.0.0",
"@lerna/project": "^4.0.0",
"@searchfe/tsconfig": "^1.1.0",
"@types/inquirer": "^7.3.1",
"@types/jest": "^26.0.23",
"@types/node": "^14.14.22",
"@types/pino": "^6.3.8",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"eslint": "^7.19.0",
"fastify": "^3.11.0",
"jest": "^26.6.3",
"lerna": "^3.22.1",
"lerna-jest": "^0.5.4",
"nodemon": "^2.0.7",
"pre-commit": "^1.2.2",
"source-map-support": "^0.5.19",
"swig": "1.4.0",
"ts-jest": "^26.5.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
Expand Down
37 changes: 37 additions & 0 deletions packages/view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# @hoth/molecule

### Demo

controller.ts

```
import {FastifyLoggerInstance} from 'fastify';
import {Controller as IController} from '@baidu/molecule';
export class Controller implements IController {
root: string;
logger: FastifyLoggerInstance;
constructor(options: Option) {
this.logger = options.logger;
this.root = options.root;
}
render(data: Data) {
return `appname is ${data.appname}, route name is ${data.name}, title is ${data.title}`;
}
}
```

node server

```
import {molecule} from '@hoth/molecule';
let ret = await molecule(ctrlPath, data, {
root: '/dist',
appName: 'appname',
name: 'route name',
logger: fastify.log,
});
```
38 changes: 38 additions & 0 deletions packages/view/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@hoth/view",
"version": "1.1.0",
"description": "template engine for hoth framework",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build tsconfig.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/searchfe/hoth.git"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"view",
"swig"
],
"author": "cxtom (cxtom2008@gmail.com)",
"license": "MIT",
"bugs": {
"url": "https://github.com/searchfe/hoth/issues"
},
"homepage": "https://github.com/searchfe/hoth#readme",
"dependencies": {
"fastify-plugin": "^3.0.0",
"lru-cache": "^6.0.0",
"tslib": "^2.1.0"
},
"files": [
"dist"
],
"devDependencies": {
"@types/lru-cache": "^5.1.0",
"@types/swig": "^0.0.29"
}
}
30 changes: 30 additions & 0 deletions packages/view/src/__tests__/swig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Fastify, {FastifyReply, FastifyRequest} from 'fastify';

describe('reply.render with swig engine', () => {
const fastify = Fastify();
const Swig = require('swig');

it('simple output', async () => {
const data = {title: 'fastify', text: 'text'};

fastify.register(require('../index'), {
engine: {
swig: Swig,
},
rootPath: __dirname,
});

fastify.get('/', (req: FastifyRequest, reply: FastifyReply) => {
reply.render('templates/index.swig', data)
});

const response = await fastify.inject({
method: 'GET',
path: '/'
});

expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toBe('text/html; charset=utf-8');
expect(response.body).toBe('<h1>fastify</h1>\n<p>text</p>');
});
});
2 changes: 2 additions & 0 deletions packages/view/src/__tests__/templates/index.swig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1>{{= title }}</h1>
<p>{{= text }}</p>
168 changes: 168 additions & 0 deletions packages/view/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type {FastifyInstance, FastifyReply} from 'fastify';
import type {Swig, SwigOptions} from 'swig';
import {join, resolve} from 'path';
import fp from 'fastify-plugin';
import LRU from 'lru-cache';


const supportedEngines = ['swig', 'nunjucks'] as const;
type supportedEnginesType = typeof supportedEngines[number];;
type EngineList = Record<supportedEnginesType, any>;

interface NunjunksOptions {
onConfigure: (env: string) => void;
}

interface swigTagsOptions {
parse: (...args: any[]) => boolean;
compile: (...args: any[]) => string;
ends: boolean;
blockLevel: boolean;
}

interface wrapSwigOptions extends SwigOptions {
filters: Record<string, (...args: any[]) => string>;
tags: Record<string, swigTagsOptions>;
}

export interface HothViewOptions {
engine: EngineList;
options?: NunjunksOptions | wrapSwigOptions;
maxCacheAge?: number;
maxCache?: number;
propertyName?: string;
rootPath?: string;
viewExt?: string;
}

declare module 'fastify' {
interface FastifyReply {
render(page: string, data?: object): FastifyReply;
// locals?: object;
}
}

async function plugin(fastify: FastifyInstance, opts: HothViewOptions) {
if (!opts.engine) {
throw new Error('Missing engine');
}

const type = Object.keys(opts.engine)[0] as supportedEnginesType;
if (supportedEngines.indexOf(type) === -1) {
throw new Error(`'${type}' not yet supported`);
}

const engine = opts.engine[type];
const options = opts.options || ({} as HothViewOptions['options'])!;
const propertyName = opts.propertyName || 'render';
const templatesDir = opts.rootPath || resolve('./');
const viewExt = opts.viewExt || '';
const maxCacheAge = opts.maxCacheAge || 1000 * 60 * 60;
const maxCache = opts.maxCache || 20 * 1024 * 1024;
const defaultCtx = {};

const renderCaches = new LRU({
max: maxCache,
length(n: string) {
return n.length;
},
maxAge: maxCacheAge,
});

const renders = {
swig: viewSwig,
nunjucks: viewNunjucks,
};

let swig: Swig;
if (type === 'swig') {
swig = new engine.Swig(options);
let setCount = 0;
// @ts-ignore
swig.renderCache = {
get: renderCaches.get.bind(renderCaches),
set(key: string, value: any) {
// 设置过多缓存时,考虑清理老缓存请求次数
if (setCount > maxCache) {
renderCaches.prune();
setCount = 0;
}
setCount++;
return renderCaches.set(key, value);
},
clean() {
renderCaches.reset();
},
};
}

const renderer = renders[type];

fastify.decorateReply(propertyName, function (this: FastifyReply, page: string, data: object) {
renderer.apply(this, [page, data]);
return this;
});

function getPage(page: string) {
if (viewExt) {
return `${page}.${viewExt}`;
}
return page
}

function viewSwig(this: FastifyReply, page: string, data: object) {
if (!page) {
this.send(new Error('Missing page'))
return
}

const finalOpts = options as wrapSwigOptions;

// 加载用户扩展
if (finalOpts.tags) {
Object.keys(finalOpts.tags).forEach(function (name) {
const t = finalOpts.tags[name];
swig.setTag(name, t.parse, t.compile, t.ends, t.blockLevel || false);
});
}

if (finalOpts.filters) {
Object.keys(finalOpts.filters).forEach(function (name) {
const t = finalOpts.filters[name];
swig.setFilter(name, t);
});
}

data = Object.assign({}, defaultCtx, data);
swig.renderFile(join(templatesDir, getPage(page)), data, (error: Error, html: string) => {
if (error) {
return this.send(error);
}
this.header('Content-Type', 'text/html; charset=utf-8');
this.send(html);
});
}

function viewNunjucks(this: FastifyReply, page: string, data: object) {
if (!page) {
this.send(new Error('Missing page'))
return
}
const env = engine.configure(templatesDir, options);
const finalOpts = options as NunjunksOptions;
if (typeof finalOpts.onConfigure === 'function') {
finalOpts.onConfigure(env)
}
data = Object.assign({}, defaultCtx, data);
page = getPage(page);
env.render(join(templatesDir, page), data, (err: Error, html: string) => {
if (err) {
return this.send(err);
}
this.header('Content-Type', 'text/html; charset=utf-8');
this.send(html);
});
}
}

export default fp(plugin);
Loading

0 comments on commit cdf4682

Please sign in to comment.