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.
Files changed (2) hide show
  1. package/index.ts +111 -40
  2. 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
- * - 配列: 各要素を <item> でラップして連結
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
- // 既存の実装を踏襲して runs.js に書き出して node で実行
46
- writeFileSync("./runs.js", code)
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
- newValue = newValue.replace(m, '')
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).map(key => {
67
- // fast-xml-parser の場合、テキストノードはキーが '#text' や '_text' の可能性があるので考慮
68
- if (key === '#text' || key === '_text') {
69
- return this.renderValue(value[key])
70
- }
71
- return `<${key}>${this.renderValue(value[key])}</${key}>`
72
- }).join('')
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
- * XML を解析して pages に HTML を格納
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, '&quot;')
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
- const generatedhtml = Object.keys(content).map(tag => {
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.pages).forEach(route => {
178
+ Object.keys(this.pagesTemplates).forEach(route => {
136
179
  app.get(route, (req, res) => {
137
- res.send(this.pages[route])
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-riner",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",