@tsed/platform-views 8.0.1 → 8.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -14
- package/src/decorators/view.ts +6 -0
- package/src/domain/PlatformViewsSettings.ts +101 -0
- package/src/index.ts +4 -0
- package/src/services/PlatformViews.spec.ts +88 -0
- package/src/services/PlatformViews.ts +151 -0
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "@tsed/platform-views",
|
|
3
3
|
"description": "Views module for Ts.ED Framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.3",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
9
9
|
"typings": "./lib/types/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"@tsed/source": "./src/index.ts",
|
|
12
13
|
"types": "./lib/types/index.d.ts",
|
|
13
14
|
"import": "./lib/esm/index.js",
|
|
14
15
|
"default": "./lib/esm/index.js"
|
|
@@ -21,29 +22,29 @@
|
|
|
21
22
|
"test:ci": "vitest run --coverage.thresholds.autoUpdate=true"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@tsed/engines": "8.0.
|
|
25
|
+
"@tsed/engines": "8.0.3",
|
|
25
26
|
"ejs": "^3.1.10",
|
|
26
27
|
"tslib": "2.7.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@tsed/barrels": "8.0.
|
|
30
|
-
"@tsed/core": "8.0.
|
|
31
|
-
"@tsed/di": "8.0.
|
|
32
|
-
"@tsed/engines": "8.0.
|
|
33
|
-
"@tsed/exceptions": "8.0.
|
|
34
|
-
"@tsed/schema": "8.0.
|
|
35
|
-
"@tsed/typescript": "8.0.
|
|
30
|
+
"@tsed/barrels": "8.0.3",
|
|
31
|
+
"@tsed/core": "8.0.3",
|
|
32
|
+
"@tsed/di": "8.0.3",
|
|
33
|
+
"@tsed/engines": "8.0.3",
|
|
34
|
+
"@tsed/exceptions": "8.0.3",
|
|
35
|
+
"@tsed/schema": "8.0.3",
|
|
36
|
+
"@tsed/typescript": "8.0.3",
|
|
36
37
|
"@types/ejs": "3.1.5",
|
|
37
38
|
"eslint": "9.12.0",
|
|
38
39
|
"typescript": "5.4.5",
|
|
39
40
|
"vitest": "2.1.2"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@tsed/core": "8.0.
|
|
43
|
-
"@tsed/di": "8.0.
|
|
44
|
-
"@tsed/engines": "8.0.
|
|
45
|
-
"@tsed/exceptions": "8.0.
|
|
46
|
-
"@tsed/schema": "8.0.
|
|
43
|
+
"@tsed/core": "8.0.3",
|
|
44
|
+
"@tsed/di": "8.0.3",
|
|
45
|
+
"@tsed/engines": "8.0.3",
|
|
46
|
+
"@tsed/exceptions": "8.0.3",
|
|
47
|
+
"@tsed/schema": "8.0.3"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"@tsed/core": {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {BaseContext} from "@tsed/di";
|
|
2
|
+
import {Writable} from "stream";
|
|
3
|
+
|
|
4
|
+
export const PLATFORM_VIEWS_EXTENSIONS = {
|
|
5
|
+
atpl: "atpl",
|
|
6
|
+
bracket: "bracket",
|
|
7
|
+
dot: "dot",
|
|
8
|
+
dust: "dust",
|
|
9
|
+
ect: "ect",
|
|
10
|
+
ejs: "ejs",
|
|
11
|
+
haml: "haml",
|
|
12
|
+
"haml-coffee": "haml-coffee",
|
|
13
|
+
hamlet: "hamlet",
|
|
14
|
+
hbs: "handlebars",
|
|
15
|
+
handlebars: "handlebars",
|
|
16
|
+
hogan: "hogan",
|
|
17
|
+
htmling: "htmling",
|
|
18
|
+
jazz: "jazz",
|
|
19
|
+
jqtpl: "jqtpl",
|
|
20
|
+
just: "just",
|
|
21
|
+
kernel: "kernel",
|
|
22
|
+
liquid: "liquid",
|
|
23
|
+
liquor: "liquor",
|
|
24
|
+
lodash: "lodash",
|
|
25
|
+
mote: "mote",
|
|
26
|
+
mustache: "mustache",
|
|
27
|
+
nunjucks: "nunjucks",
|
|
28
|
+
plates: "plates",
|
|
29
|
+
pug: "pug",
|
|
30
|
+
qejs: "qejs",
|
|
31
|
+
ractive: "ractive",
|
|
32
|
+
razor: "razor",
|
|
33
|
+
jsx: "react",
|
|
34
|
+
slm: "slm",
|
|
35
|
+
squirelly: "squirelly",
|
|
36
|
+
swig: "swig",
|
|
37
|
+
teacup: "teacup",
|
|
38
|
+
templayed: "templayed",
|
|
39
|
+
twig: "twig",
|
|
40
|
+
underscore: "underscore",
|
|
41
|
+
vash: "vash",
|
|
42
|
+
velocityjs: "velocityjs",
|
|
43
|
+
walrus: "walrus",
|
|
44
|
+
whiskers: "whiskers"
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type PlatformViewsExtensionsTypes = Record<string, string>;
|
|
48
|
+
|
|
49
|
+
export interface PlatformViewsEngineOptions extends Record<string, unknown> {
|
|
50
|
+
requires?: any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface PlatformRenderOptions extends Record<string, unknown> {
|
|
54
|
+
$ctx: BaseContext;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PlatformViewWritableStream {
|
|
58
|
+
pipe(stream: Writable): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PlatformViewEngine {
|
|
62
|
+
options: PlatformViewsEngineOptions;
|
|
63
|
+
|
|
64
|
+
render(path: string, options: PlatformRenderOptions): Promise<string | PlatformViewWritableStream>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PlatformViewsSettings {
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Views directory.
|
|
71
|
+
*/
|
|
72
|
+
root?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Enable cache. Ts.ED enable cache in PRODUCTION profile by default.
|
|
75
|
+
*/
|
|
76
|
+
cache?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Provide extensions mapping to match the expected engines.
|
|
79
|
+
*/
|
|
80
|
+
extensions?: Partial<PlatformViewsExtensionsTypes>;
|
|
81
|
+
/**
|
|
82
|
+
* Default view engine extension.
|
|
83
|
+
* Allow omitting extension when using View decorator or render method.
|
|
84
|
+
*/
|
|
85
|
+
viewEngine?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Options mapping for each engine.
|
|
88
|
+
*/
|
|
89
|
+
options?: Record<string, PlatformViewsEngineOptions>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
declare global {
|
|
93
|
+
namespace TsED {
|
|
94
|
+
interface Configuration {
|
|
95
|
+
/**
|
|
96
|
+
* Object to configure Views engines with Consolidate. See more on [View engine](/docs/template-engine.md).
|
|
97
|
+
*/
|
|
98
|
+
views: PlatformViewsSettings;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {Injectable} from "@tsed/di";
|
|
2
|
+
import {requires} from "@tsed/engines";
|
|
3
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
4
|
+
|
|
5
|
+
import {PlatformViews} from "./PlatformViews.js";
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
class AlterOptions {
|
|
9
|
+
$alterRenderOptions(options: any) {
|
|
10
|
+
options.alter = "alter";
|
|
11
|
+
return Promise.resolve(options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("PlatformViews", () => {
|
|
16
|
+
beforeEach(() =>
|
|
17
|
+
PlatformTest.create({
|
|
18
|
+
views: {
|
|
19
|
+
extensions: {
|
|
20
|
+
ejs: "ejs"
|
|
21
|
+
},
|
|
22
|
+
viewEngine: "ejs",
|
|
23
|
+
options: {
|
|
24
|
+
ejs: {
|
|
25
|
+
global: "global",
|
|
26
|
+
requires: "requires"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
requires.delete("ejs");
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => PlatformTest.reset());
|
|
36
|
+
|
|
37
|
+
describe("render()", () => {
|
|
38
|
+
it("should render a template with given extension", async () => {
|
|
39
|
+
const platformViews = PlatformTest.get<PlatformViews>(PlatformViews);
|
|
40
|
+
const engine = platformViews.getEngine("ejs")!;
|
|
41
|
+
|
|
42
|
+
vi.spyOn(engine, "render").mockResolvedValue("HTML");
|
|
43
|
+
|
|
44
|
+
const result = await platformViews.render("views.ejs");
|
|
45
|
+
|
|
46
|
+
expect(result).toEqual("HTML");
|
|
47
|
+
expect(engine.render).toHaveBeenCalledWith("views.ejs", {
|
|
48
|
+
cache: false,
|
|
49
|
+
alter: "alter",
|
|
50
|
+
global: "global",
|
|
51
|
+
requires: "requires"
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("should render a template without extension", async () => {
|
|
55
|
+
const platformViews = PlatformTest.get<PlatformViews>(PlatformViews);
|
|
56
|
+
const engine = platformViews.getEngine("ejs")!;
|
|
57
|
+
|
|
58
|
+
vi.spyOn(engine, "render").mockResolvedValue("HTML");
|
|
59
|
+
|
|
60
|
+
const result = await platformViews.render("views", {test: "test"});
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual("HTML");
|
|
63
|
+
expect(engine.render).toHaveBeenCalledWith("views.ejs", {
|
|
64
|
+
cache: false,
|
|
65
|
+
global: "global",
|
|
66
|
+
test: "test",
|
|
67
|
+
alter: "alter",
|
|
68
|
+
requires: "requires"
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it("should render a template without extension and catch error", async () => {
|
|
72
|
+
const platformViews = PlatformTest.get<PlatformViews>(PlatformViews);
|
|
73
|
+
const engine = platformViews.getEngine("ejs")!;
|
|
74
|
+
|
|
75
|
+
vi.spyOn(engine, "render").mockResolvedValue("HTML");
|
|
76
|
+
|
|
77
|
+
let error: any;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await platformViews.render("views.toto", {test: "test"});
|
|
81
|
+
} catch (er) {
|
|
82
|
+
error = er;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
expect(error.message).toEqual('Engine not found to render the following "views.toto"');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import "../domain/PlatformViewsSettings.js";
|
|
2
|
+
|
|
3
|
+
import {Env, getValue} from "@tsed/core";
|
|
4
|
+
import {constant, injectable, ProviderType} from "@tsed/di";
|
|
5
|
+
import {engines, getEngine, requires} from "@tsed/engines";
|
|
6
|
+
import {$asyncAlter} from "@tsed/hooks";
|
|
7
|
+
import Fs from "fs";
|
|
8
|
+
import {extname, join, resolve} from "path";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
PLATFORM_VIEWS_EXTENSIONS,
|
|
12
|
+
PlatformViewEngine,
|
|
13
|
+
PlatformViewsEngineOptions,
|
|
14
|
+
PlatformViewsExtensionsTypes,
|
|
15
|
+
PlatformViewWritableStream
|
|
16
|
+
} from "../domain/PlatformViewsSettings.js";
|
|
17
|
+
|
|
18
|
+
async function patchEJS(ejs: any) {
|
|
19
|
+
if (!ejs) {
|
|
20
|
+
const mod = await import("ejs");
|
|
21
|
+
ejs = mod.default;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
compile(str: string, {client, ...options}: any) {
|
|
26
|
+
return ejs.compile(str, options);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @platform
|
|
33
|
+
*/
|
|
34
|
+
export class PlatformViews {
|
|
35
|
+
readonly root = constant("views.root", `${process.cwd()}/views`);
|
|
36
|
+
readonly cache = constant<boolean>("views.cache");
|
|
37
|
+
readonly disabled = constant<string | boolean>("views.disabled", false);
|
|
38
|
+
readonly viewEngine = constant<string>("views.viewEngine", "ejs");
|
|
39
|
+
protected env = constant<Env>("env");
|
|
40
|
+
protected extensionsOptions = constant<PlatformViewsExtensionsTypes>("views.extensions", {});
|
|
41
|
+
protected engineOptions = constant<Record<string, PlatformViewsEngineOptions>>("views.options", {});
|
|
42
|
+
|
|
43
|
+
#extensions: Map<string, string>;
|
|
44
|
+
#engines = new Map<string, PlatformViewEngine>();
|
|
45
|
+
#cachePaths = new Map<string, {path: string; extension: string}>();
|
|
46
|
+
|
|
47
|
+
async $onInit() {
|
|
48
|
+
if (!this.disabled) {
|
|
49
|
+
this.#extensions = new Map(
|
|
50
|
+
Object.entries({
|
|
51
|
+
...PLATFORM_VIEWS_EXTENSIONS,
|
|
52
|
+
...this.extensionsOptions
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await this.loadEngines();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async loadEngines() {
|
|
61
|
+
requires.set("ejs", await patchEJS(requires.get("ejs")));
|
|
62
|
+
|
|
63
|
+
this.#extensions.forEach((engineType) => {
|
|
64
|
+
if (engines.has(engineType)) {
|
|
65
|
+
const options = this.getEngineOptions(engineType);
|
|
66
|
+
|
|
67
|
+
if (options.requires) {
|
|
68
|
+
requires.set(engineType, options.requires);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.registerEngine(engineType, {
|
|
72
|
+
options,
|
|
73
|
+
render: getEngine(engineType)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getEngines() {
|
|
80
|
+
return [...this.#extensions.entries()].map(([extension, engineType]) => {
|
|
81
|
+
const engine = this.getEngine(this.getExtension(engineType))!;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
extension,
|
|
85
|
+
engine
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
registerEngine(engineType: string, engine: PlatformViewEngine) {
|
|
91
|
+
this.#engines.set(engineType, engine);
|
|
92
|
+
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getEngine(type: string) {
|
|
97
|
+
return this.#engines.get(this.#extensions.get(type) || type);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getEngineOptions(engineType: string): PlatformViewsEngineOptions {
|
|
101
|
+
return getValue(this.engineOptions, engineType, {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async render(viewPath: string, options: any = {}): Promise<string | PlatformViewWritableStream> {
|
|
105
|
+
const {$ctx} = options;
|
|
106
|
+
|
|
107
|
+
options = await $asyncAlter("$alterRenderOptions", options, $ctx);
|
|
108
|
+
|
|
109
|
+
const {path, extension} = this.#cachePaths.get(viewPath) || this.#cachePaths.set(viewPath, this.resolve(viewPath)).get(viewPath)!;
|
|
110
|
+
const engine = this.getEngine(extension);
|
|
111
|
+
|
|
112
|
+
if (!engine) {
|
|
113
|
+
throw new Error(`Engine not found to render the following "${viewPath}"`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const finalOpts = Object.assign({cache: this.cache || this.env === Env.PROD}, engine.options, options, {$ctx});
|
|
117
|
+
|
|
118
|
+
return engine.render(path, finalOpts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected getExtension(viewPath: string) {
|
|
122
|
+
return (extname(viewPath) || this.viewEngine).replace(/\./, "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
protected resolve(viewPath: string) {
|
|
126
|
+
const extension = this.getExtension(viewPath);
|
|
127
|
+
|
|
128
|
+
viewPath = viewPath.replace(extname(viewPath), "") + "." + extension;
|
|
129
|
+
|
|
130
|
+
const path =
|
|
131
|
+
[
|
|
132
|
+
viewPath,
|
|
133
|
+
resolve(join(this.root, viewPath)),
|
|
134
|
+
resolve(join(process.cwd(), "views", viewPath)),
|
|
135
|
+
resolve(join(process.cwd(), "public", viewPath))
|
|
136
|
+
].find((file) => Fs.existsSync(file)) || viewPath;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
path,
|
|
140
|
+
extension
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
injectable(PlatformViews)
|
|
146
|
+
.type(ProviderType.MODULE)
|
|
147
|
+
.configuration({
|
|
148
|
+
views: {
|
|
149
|
+
exists: true
|
|
150
|
+
}
|
|
151
|
+
} as never);
|