node-riner 1.3.0 → 1.3.2
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/index.ts +111 -40
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// ...existing code...
|
|
2
1
|
import fs, { readFileSync, writeFileSync } from 'fs'
|
|
3
2
|
import { XMLParser } from 'fast-xml-parser'
|
|
4
3
|
import exp from 'express'
|
|
5
4
|
import path from 'path'
|
|
6
5
|
import { execSync } from 'node:child_process'
|
|
6
|
+
import { runInNewContext } from 'vm'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Ride クラス
|
|
@@ -12,44 +12,58 @@ export default class Ride {
|
|
|
12
12
|
F: string
|
|
13
13
|
parsedJson: any
|
|
14
14
|
pages: Record<string, string>
|
|
15
|
+
pagesTemplates: Record<string, any> // ルート => 生の Content オブジェクト(リクエスト毎に評価)
|
|
15
16
|
name: string
|
|
16
17
|
defaultinfo: string
|
|
18
|
+
defaultContext: any // サーバ/アプリ側で渡したい既定のコンテキスト
|
|
17
19
|
|
|
18
20
|
constructor(name: string) {
|
|
19
21
|
this.F = ""
|
|
20
22
|
this.parsedJson = {}
|
|
21
23
|
this.pages = {}
|
|
24
|
+
this.pagesTemplates = {}
|
|
22
25
|
this.name = name
|
|
23
26
|
this.defaultinfo = ""
|
|
27
|
+
this.defaultContext = {}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* 値を再帰的に HTML に変換するユーティリティ
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* - オブジェクト: 各キーをタグ名として再帰的にレンダリング
|
|
32
|
+
* - ctx を渡すと { ... } 内のコードをそのコンテキストで評価する
|
|
33
|
+
* - 出力は console.log() の内容を優先して取得し、なければ評価結果を文字列化する
|
|
31
34
|
*/
|
|
32
|
-
private renderValue(value: any): string {
|
|
33
|
-
// null / undefined
|
|
35
|
+
private renderValue(value: any, ctx: any = {}): string {
|
|
34
36
|
if (value === null || value === undefined) return ''
|
|
35
37
|
|
|
36
38
|
// 文字列処理({...} の評価を含む)
|
|
37
39
|
if (typeof value === 'string') {
|
|
38
|
-
// 複数マッチにも対応
|
|
39
40
|
const matches = value.match(/{.*?}/g)
|
|
40
41
|
if (matches) {
|
|
41
42
|
let newValue = value
|
|
42
43
|
matches.forEach(m => {
|
|
43
|
-
const code = m.slice(1, -1) //
|
|
44
|
+
const code = m.slice(1, -1) // 中身
|
|
45
|
+
// sandbox を作り、console.log の出力を収集する
|
|
46
|
+
const output: string[] = []
|
|
47
|
+
const sandbox = Object.assign({}, ctx, {
|
|
48
|
+
console: {
|
|
49
|
+
log: (...args: any[]) => output.push(args.join(' ')),
|
|
50
|
+
error: (...args: any[]) => output.push(args.join(' ')),
|
|
51
|
+
warn: (...args: any[]) => output.push(args.join(' '))
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// まずは vm で実行(タイムアウト付き)
|
|
56
|
+
let res: any
|
|
44
57
|
try {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
const out = execSync("node runs.js").toString()
|
|
48
|
-
newValue = newValue.replace(m, out)
|
|
58
|
+
// 実行が式でも文でも動くようにそのまま実行
|
|
59
|
+
res = runInNewContext(code, sandbox, { timeout: 2000 })
|
|
49
60
|
} catch (e) {
|
|
50
|
-
//
|
|
51
|
-
|
|
61
|
+
// 実行に失敗したら空文字に
|
|
62
|
+
res = undefined
|
|
52
63
|
}
|
|
64
|
+
|
|
65
|
+
const out = output.length ? output.join('\n') : (res !== undefined ? String(res) : '')
|
|
66
|
+
newValue = newValue.replace(m, out)
|
|
53
67
|
})
|
|
54
68
|
return newValue
|
|
55
69
|
}
|
|
@@ -58,26 +72,64 @@ export default class Ride {
|
|
|
58
72
|
|
|
59
73
|
// 配列: 各要素を再帰的に処理して <item> でラップ
|
|
60
74
|
if (Array.isArray(value)) {
|
|
61
|
-
return value.map((item: any) => `<item>${this.renderValue(item)}</item>`).join('')
|
|
75
|
+
return value.map((item: any) => `<item>${this.renderValue(item, ctx)}</item>`).join('')
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
// オブジェクト:
|
|
78
|
+
// オブジェクト: 属性(@_...) を無視して子要素だけタグ化する
|
|
65
79
|
if (typeof value === 'object') {
|
|
66
|
-
return Object.keys(value)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
return Object.keys(value)
|
|
81
|
+
.filter(key => !key.startsWith('@_'))
|
|
82
|
+
.map(key => {
|
|
83
|
+
if (key === '#text' || key === '_text') {
|
|
84
|
+
return this.renderValue(value[key], ctx)
|
|
85
|
+
}
|
|
86
|
+
return `<${key}>${this.renderValue(value[key], ctx)}</${key}>`
|
|
87
|
+
}).join('')
|
|
73
88
|
}
|
|
74
89
|
|
|
75
|
-
// その他(数値や真偽値など)はそのまま文字列化
|
|
76
90
|
return String(value)
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
/**
|
|
80
|
-
*
|
|
94
|
+
* タグ名とその値から、属性を正しく反映した HTML を生成する
|
|
95
|
+
* - ctx を渡して { ... } の中で req / server / page / data が参照できる
|
|
96
|
+
*/
|
|
97
|
+
private renderContentTag(tag: string, value: any, ctx: any = {}): string {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
return value.map((item: any) => this.renderContentTag(tag, item, ctx)).join('')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
103
|
+
return `<${tag}>${this.renderValue(value, ctx)}</${tag}>`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const entries = Object.entries(value)
|
|
107
|
+
const attrEntries = entries.filter(([k]) => k.startsWith('@_'))
|
|
108
|
+
const childEntries = entries.filter(([k]) => !k.startsWith('@_'))
|
|
109
|
+
|
|
110
|
+
const attrs = attrEntries.map(([k, v]) => {
|
|
111
|
+
const name = k.replace(/^@_/, '')
|
|
112
|
+
const safe = String(v).replace(/"/g, '"')
|
|
113
|
+
return ` ${name}="${safe}"`
|
|
114
|
+
}).join('')
|
|
115
|
+
|
|
116
|
+
let inner = ''
|
|
117
|
+
if (value['#text'] !== undefined) {
|
|
118
|
+
inner += this.renderValue(value['#text'], ctx)
|
|
119
|
+
} else if (value['_text'] !== undefined) {
|
|
120
|
+
inner += this.renderValue(value['_text'], ctx)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
childEntries.forEach(([k, v]) => {
|
|
124
|
+
if (k === '#text' || k === '_text') return
|
|
125
|
+
inner += `<${k}>${this.renderValue(v, ctx)}</${k}>`
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return `<${tag}${attrs}>${inner}</${tag}>`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* XML を解析して pagesTemplates に生の Content を格納(評価はリクエスト時に行う)
|
|
81
133
|
*/
|
|
82
134
|
returnhtml(): string {
|
|
83
135
|
const options = { ignoreAttributes: false }
|
|
@@ -92,24 +144,14 @@ export default class Ride {
|
|
|
92
144
|
const pages = root["Page"]
|
|
93
145
|
if (!pages) return ""
|
|
94
146
|
|
|
95
|
-
// Page が配列か単一かを吸収して配列化
|
|
96
147
|
const pageArray = Array.isArray(pages) ? pages : [pages]
|
|
97
148
|
|
|
98
149
|
pageArray.forEach((page: any) => {
|
|
99
150
|
const route = page['@_route'] || '/'
|
|
100
|
-
|
|
101
|
-
// Content が存在しない場合は空にする
|
|
102
151
|
const content = page['Content'] || {}
|
|
103
152
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
const value = content[tag]
|
|
107
|
-
const inner = this.renderValue(value)
|
|
108
|
-
return `<${tag}>${inner}</${tag}>`
|
|
109
|
-
}).join('')
|
|
110
|
-
|
|
111
|
-
// ページを格納(タイトルを付与)
|
|
112
|
-
this.pages[route] = `<title>${this.name}</title>` + generatedhtml
|
|
153
|
+
// 生の content を保存しておく(ルートアクセス時に評価する)
|
|
154
|
+
this.pagesTemplates[route] = { content, pageMeta: page }
|
|
113
155
|
})
|
|
114
156
|
} catch (e) {
|
|
115
157
|
console.error("Whoops, an error occurred during analysis : " + e)
|
|
@@ -128,13 +170,38 @@ export default class Ride {
|
|
|
128
170
|
|
|
129
171
|
/**
|
|
130
172
|
* 簡易サーバを起動
|
|
173
|
+
* - リクエスト毎に { ... } の評価を行い、req, query, params, body, server(defaultContext), page が参照可能になる
|
|
131
174
|
*/
|
|
132
175
|
serve(port: number) {
|
|
133
176
|
const app = exp();
|
|
134
177
|
|
|
135
|
-
Object.keys(this.
|
|
178
|
+
Object.keys(this.pagesTemplates).forEach(route => {
|
|
136
179
|
app.get(route, (req, res) => {
|
|
137
|
-
|
|
180
|
+
const tpl = this.pagesTemplates[route]
|
|
181
|
+
const content = tpl?.content || {}
|
|
182
|
+
|
|
183
|
+
// コンテキストを組み立て(デフォルト + リクエスト情報 + ページメタ)
|
|
184
|
+
const ctx = {
|
|
185
|
+
server: this.defaultContext,
|
|
186
|
+
req: {
|
|
187
|
+
params: req.params,
|
|
188
|
+
query: req.query,
|
|
189
|
+
path: req.path,
|
|
190
|
+
headers: req.headers
|
|
191
|
+
},
|
|
192
|
+
page: tpl.pageMeta || {},
|
|
193
|
+
// 任意のデータを追加できるように空の data を用意
|
|
194
|
+
data: {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 各タグを評価して結合
|
|
198
|
+
const generatedhtml = Object.keys(content).map(tag => {
|
|
199
|
+
const value = content[tag]
|
|
200
|
+
return this.renderContentTag(tag, value, ctx)
|
|
201
|
+
}).join('')
|
|
202
|
+
|
|
203
|
+
// タイトルを付与して送信
|
|
204
|
+
res.send(`<title>${this.name}</title>` + generatedhtml)
|
|
138
205
|
})
|
|
139
206
|
})
|
|
140
207
|
|
|
@@ -145,7 +212,11 @@ export default class Ride {
|
|
|
145
212
|
return app
|
|
146
213
|
}
|
|
147
214
|
|
|
215
|
+
/**
|
|
216
|
+
* サーバ側で渡したい既定のコンテキストを設定する
|
|
217
|
+
* 例: ride.config({ currentUser: {...}, env: process.env.NODE_ENV })
|
|
218
|
+
*/
|
|
148
219
|
config(options: object) {
|
|
149
|
-
|
|
220
|
+
this.defaultContext = Object.assign({}, this.defaultContext, options)
|
|
150
221
|
}
|
|
151
222
|
}
|