-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(view): add template render plugin
- Loading branch information
Showing
12 changed files
with
2,623 additions
and
377 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
<h1>{{= title }}</h1> | ||
<p>{{= text }}</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.