node-riner 1.3.1 → 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 +74 -52
  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,45 +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
- * - オブジェクト: 各キーをタグ名として再帰的にレンダリング
31
- *
32
- * 注意: '@_' で始まるキー(属性)はここではタグ化しない。
32
+ * - ctx を渡すと { ... } 内のコードをそのコンテキストで評価する
33
+ * - 出力は console.log() の内容を優先して取得し、なければ評価結果を文字列化する
33
34
  */
34
- private renderValue(value: any): string {
35
- // null / undefined
35
+ private renderValue(value: any, ctx: any = {}): string {
36
36
  if (value === null || value === undefined) return ''
37
37
 
38
38
  // 文字列処理({...} の評価を含む)
39
39
  if (typeof value === 'string') {
40
- // 複数マッチにも対応
41
40
  const matches = value.match(/{.*?}/g)
42
41
  if (matches) {
43
42
  let newValue = value
44
43
  matches.forEach(m => {
45
- 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
46
57
  try {
47
- // 既存の実装を踏襲して runs.js に書き出して node で実行
48
- writeFileSync("./runs.js", code)
49
- const out = execSync("node runs.js").toString()
50
- newValue = newValue.replace(m, out)
58
+ // 実行が式でも文でも動くようにそのまま実行
59
+ res = runInNewContext(code, sandbox, { timeout: 2000 })
51
60
  } catch (e) {
52
- newValue = newValue.replace(m, '')
61
+ // 実行に失敗したら空文字に
62
+ res = undefined
53
63
  }
64
+
65
+ const out = output.length ? output.join('\n') : (res !== undefined ? String(res) : '')
66
+ newValue = newValue.replace(m, out)
54
67
  })
55
68
  return newValue
56
69
  }
@@ -59,74 +72,64 @@ export default class Ride {
59
72
 
60
73
  // 配列: 各要素を再帰的に処理して <item> でラップ
61
74
  if (Array.isArray(value)) {
62
- return value.map((item: any) => `<item>${this.renderValue(item)}</item>`).join('')
75
+ return value.map((item: any) => `<item>${this.renderValue(item, ctx)}</item>`).join('')
63
76
  }
64
77
 
65
78
  // オブジェクト: 属性(@_...) を無視して子要素だけタグ化する
66
79
  if (typeof value === 'object') {
67
80
  return Object.keys(value)
68
- .filter(key => !key.startsWith('@_')) // 属性はここで無視
81
+ .filter(key => !key.startsWith('@_'))
69
82
  .map(key => {
70
- // テキストノードは直接中身を出力
71
83
  if (key === '#text' || key === '_text') {
72
- return this.renderValue(value[key])
84
+ return this.renderValue(value[key], ctx)
73
85
  }
74
- return `<${key}>${this.renderValue(value[key])}</${key}>`
86
+ return `<${key}>${this.renderValue(value[key], ctx)}</${key}>`
75
87
  }).join('')
76
88
  }
77
89
 
78
- // その他(数値や真偽値など)はそのまま文字列化
79
90
  return String(value)
80
91
  }
81
92
 
82
93
  /**
83
94
  * タグ名とその値から、属性を正しく反映した HTML を生成する
84
- * - value が配列ならタグを複数分生成
85
- * - value がオブジェクトで属性(@_...)を持つ場合、属性を開タグに組み込む
95
+ * - ctx を渡して { ... } の中で req / server / page / data が参照できる
86
96
  */
87
- private renderContentTag(tag: string, value: any): string {
88
- // 配列の場合は各要素で同じタグを生成
97
+ private renderContentTag(tag: string, value: any, ctx: any = {}): string {
89
98
  if (Array.isArray(value)) {
90
- return value.map((item: any) => this.renderContentTag(tag, item)).join('')
99
+ return value.map((item: any) => this.renderContentTag(tag, item, ctx)).join('')
91
100
  }
92
101
 
93
- // プリミティブ(文字列等)の場合はタグでラップ
94
102
  if (value === null || value === undefined || typeof value !== 'object') {
95
- return `<${tag}>${this.renderValue(value)}</${tag}>`
103
+ return `<${tag}>${this.renderValue(value, ctx)}</${tag}>`
96
104
  }
97
105
 
98
- // オブジェクトの場合、属性を抽出
99
106
  const entries = Object.entries(value)
100
107
  const attrEntries = entries.filter(([k]) => k.startsWith('@_'))
101
108
  const childEntries = entries.filter(([k]) => !k.startsWith('@_'))
102
109
 
103
- // 属性文字列を組み立て(@_ を取り除く)
104
110
  const attrs = attrEntries.map(([k, v]) => {
105
111
  const name = k.replace(/^@_/, '')
106
112
  const safe = String(v).replace(/"/g, '&quot;')
107
113
  return ` ${name}="${safe}"`
108
114
  }).join('')
109
115
 
110
- // 子要素・テキストを組み立て
111
116
  let inner = ''
112
- // 優先してテキストノードを出す
113
117
  if (value['#text'] !== undefined) {
114
- inner += this.renderValue(value['#text'])
118
+ inner += this.renderValue(value['#text'], ctx)
115
119
  } else if (value['_text'] !== undefined) {
116
- inner += this.renderValue(value['_text'])
120
+ inner += this.renderValue(value['_text'], ctx)
117
121
  }
118
122
 
119
- // その他の子要素をレンダリング
120
123
  childEntries.forEach(([k, v]) => {
121
124
  if (k === '#text' || k === '_text') return
122
- inner += `<${k}>${this.renderValue(v)}</${k}>`
125
+ inner += `<${k}>${this.renderValue(v, ctx)}</${k}>`
123
126
  })
124
127
 
125
128
  return `<${tag}${attrs}>${inner}</${tag}>`
126
129
  }
127
130
 
128
131
  /**
129
- * XML を解析して pages HTML を格納
132
+ * XML を解析して pagesTemplates に生の Content を格納(評価はリクエスト時に行う)
130
133
  */
131
134
  returnhtml(): string {
132
135
  const options = { ignoreAttributes: false }
@@ -141,24 +144,14 @@ export default class Ride {
141
144
  const pages = root["Page"]
142
145
  if (!pages) return ""
143
146
 
144
- // Page が配列か単一かを吸収して配列化
145
147
  const pageArray = Array.isArray(pages) ? pages : [pages]
146
148
 
147
149
  pageArray.forEach((page: any) => {
148
150
  const route = page['@_route'] || '/'
149
-
150
- // Content が存在しない場合は空にする
151
151
  const content = page['Content'] || {}
152
152
 
153
- // 各タグを再帰的にレンダリングして結合
154
- const generatedhtml = Object.keys(content).map(tag => {
155
- const value = content[tag]
156
- // 属性付きタグなどを正しく扱う helper を使う
157
- return this.renderContentTag(tag, value)
158
- }).join('')
159
-
160
- // ページを格納(タイトルを付与)
161
- this.pages[route] = `<title>${this.name}</title>` + generatedhtml
153
+ // 生の content を保存しておく(ルートアクセス時に評価する)
154
+ this.pagesTemplates[route] = { content, pageMeta: page }
162
155
  })
163
156
  } catch (e) {
164
157
  console.error("Whoops, an error occurred during analysis : " + e)
@@ -177,13 +170,38 @@ export default class Ride {
177
170
 
178
171
  /**
179
172
  * 簡易サーバを起動
173
+ * - リクエスト毎に { ... } の評価を行い、req, query, params, body, server(defaultContext), page が参照可能になる
180
174
  */
181
175
  serve(port: number) {
182
176
  const app = exp();
183
177
 
184
- Object.keys(this.pages).forEach(route => {
178
+ Object.keys(this.pagesTemplates).forEach(route => {
185
179
  app.get(route, (req, res) => {
186
- 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)
187
205
  })
188
206
  })
189
207
 
@@ -194,7 +212,11 @@ export default class Ride {
194
212
  return app
195
213
  }
196
214
 
215
+ /**
216
+ * サーバ側で渡したい既定のコンテキストを設定する
217
+ * 例: ride.config({ currentUser: {...}, env: process.env.NODE_ENV })
218
+ */
197
219
  config(options: object) {
198
- // 将来の拡張ポイント
220
+ this.defaultContext = Object.assign({}, this.defaultContext, options)
199
221
  }
200
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-riner",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",