node-riner 1.3.2 → 2.0.0

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 (6) hide show
  1. package/README.md +72 -15
  2. package/index.ts +92 -12
  3. package/package.json +2 -2
  4. package/runs.js +0 -1
  5. package/t.rd +0 -0
  6. package/t.ts +0 -0
package/README.md CHANGED
@@ -1,28 +1,85 @@
1
- # Ride Framework
1
+ # Riner Framework (Node Riner)
2
2
 
3
- XMLファイルをベースに動的なHTMLページを生成し、Expressサーバーとして配信する軽量なSSR(Server-Side Rendering)ユーティリティです。
3
+ Rinerは独自のファイルをhtmlに変換し、Expressサーバーで配信します。
4
4
 
5
- ## 📦 インストール
6
-
7
- プロジェクトのルートディレクトリで以下のコマンドを実行してください。
5
+ ## インストール
8
6
 
9
7
  ```bash
10
8
  npm i node-riner@latest
11
9
  ```
12
- ## Example
13
- index.rd
14
- ``` RideFrameworks
10
+
11
+ ## 特長(主な機能)
12
+
13
+ - RinerRideFile(.rd) から HTML を自動生成して Express で配信
14
+ - `{ ... }` 内の JavaScript をサンドボックス実行して動的に値を埋め込み
15
+ - 永続状態管理 riner.state(サーバ側に保存)
16
+ - 永続的に実行される move(riner.move)を登録して state を更新
17
+ - riner.out / riner.move / riner.off / riner.clear をテンプレートから利用可能
18
+ - SSE(Server-Sent Events)で state の変更をクライアントに配信
19
+ - REST API で state / moves の取得・更新が可能
20
+
21
+ ## 使い方(最小構成)
22
+
23
+ index.rd(XML)
24
+ ```xml
15
25
  <Root>
16
- <Page route="/">
17
- <h1>hello world</h1>
18
- </Page>
26
+ <Page route="/">
27
+ <Content>
28
+ <h1>Hello</h1>
29
+ <p>{riner.out(new Date().toString())}</p>
30
+ </Content>
31
+ </Page>
19
32
  </Root>
20
33
  ```
34
+
21
35
  main.ts
22
- ```
36
+ ```ts
23
37
  import Riner from 'node-riner'
24
38
 
25
- let App = new Riner("my app")
26
- App.Ride("index.rd")
27
- App.serve(3000)
39
+ const app = new Riner('my app')
40
+ app.Ride('index.rd') // XML を読み込む
41
+ app.config({ siteName: 'MyBlog' }) // サーバ側既定コンテキスト
42
+ app.serve(3000) // サーバ起動
43
+ ```
44
+
45
+ ## riner API(テンプレート内で使用)
46
+
47
+ - riner.out(...args) — 出力を収集して文字列化して埋め込む
48
+ - riner.move(fn) — 永続的な move を登録(fn は関数。例: () => counter = (counter||0)+1)
49
+ - riner.off(fnOrSrc) — 指定した move を解除(関数かソース文字列で指定)
50
+ - riner.clear() — 全ての move を削除
51
+ - riner.state — サーバ保存の state オブジェクトへ参照
52
+
53
+ テンプレート例:
54
+ ```xml
55
+ <p>{riner.out(counter)}</p>
56
+ <p>{riner.move(() => counter = (counter || 0) + 1)}</p>
57
+ ```
58
+
59
+ ## サーバ側 API / SSE
60
+
61
+ - GET /__riner/state — 現在の riner.state を返す(JSON)
62
+ - POST /__riner/state — JSON を送って state をマージ
63
+ - GET /__riner/moves — 登録中の move 一覧を返す
64
+ - POST /__riner/moves — { source: "function source" } で move 登録
65
+ - DELETE /__riner/moves — { source: "function source" } で move 削除
66
+ - SSE /__riner/events — state 更新を受け取る(リアルタイム)
67
+
68
+ ## 注意(セキュリティ)
69
+
70
+ - テンプレート内の `{ ... }` はサンドボックス(vm)で実行しますが、任意コード実行のリスクは完全には無くなりません。
71
+ - 外部入力や未信頼のテンプレートを評価しないでください。プロダクションでは追加の制限・監査が必須です。
72
+
73
+ ## 例: Test.rd(サンプル)
74
+
75
+ ```xml
76
+ <Root>
77
+ <Page route="/">
78
+ <Content>
79
+ <h1>My Blog</h1>
80
+ <p>{riner.out(new Date().toString())}</p>
81
+ <p>訪問回数: {riner.out(counter)}</p>
82
+ </Content>
83
+ </Page>
84
+ </Root>
28
85
  ```
package/index.ts CHANGED
@@ -17,6 +17,10 @@ export default class Ride {
17
17
  defaultinfo: string
18
18
  defaultContext: any // サーバ/アプリ側で渡したい既定のコンテキスト
19
19
 
20
+ // riner 用の持続状態と登録済み move 関数のソース一覧
21
+ private rinerState: Record<string, any>
22
+ private rinerMoves: string[]
23
+
20
24
  constructor(name: string) {
21
25
  this.F = ""
22
26
  this.parsedJson = {}
@@ -25,12 +29,50 @@ export default class Ride {
25
29
  this.name = name
26
30
  this.defaultinfo = ""
27
31
  this.defaultContext = {}
32
+ this.rinerState = {}
33
+ this.rinerMoves = []
34
+ }
35
+
36
+ /**
37
+ * 保存済み move 関数を sandbox 上で実行し、rinerState を同期する
38
+ * - ctx はそのまま sandbox に渡す(req 等を参照できるように)
39
+ * - move 関数内では riner.state または state のように直接変数を参照して変更可能
40
+ */
41
+ private runPersistedMoves(ctx: any = {}) {
42
+ if (!this.rinerMoves.length) return
43
+
44
+ // ベース sandbox:ctx をコピー
45
+ const sandboxBase: Record<string, any> = Object.assign({}, ctx)
46
+
47
+ // riner.state とトップレベル変数として現在の rinerState を注入
48
+ sandboxBase.riner = { state: this.rinerState }
49
+ Object.keys(this.rinerState).forEach(k => {
50
+ sandboxBase[k] = this.rinerState[k]
51
+ })
52
+
53
+ // 実行中にエラーが出ても次の move に進める
54
+ for (const src of this.rinerMoves) {
55
+ try {
56
+ // src は関数ソース文字列 (例: "() => cout += 1")
57
+ // 即時実行して state を更新させる
58
+ runInNewContext(`(${src})()`, sandboxBase, { timeout: 2000 })
59
+ } catch (e) {
60
+ // 失敗は無視(必要ならログを出す)
61
+ // console.error('move execution error', e)
62
+ }
63
+ }
64
+
65
+ // sandboxBase のトップレベル変数を rinerState に同期(riner/state は除外)
66
+ Object.keys(sandboxBase).forEach(k => {
67
+ if (k === 'riner' || k === 'console' || k === 'req' || k === 'server' || k === 'page' || k === 'data') return
68
+ this.rinerState[k] = sandboxBase[k]
69
+ })
28
70
  }
29
71
 
30
72
  /**
31
73
  * 値を再帰的に HTML に変換するユーティリティ
32
74
  * - ctx を渡すと { ... } 内のコードをそのコンテキストで評価する
33
- * - 出力は console.log() の内容を優先して取得し、なければ評価結果を文字列化する
75
+ * - riner.out / riner.move / riner.state を提供
34
76
  */
35
77
  private renderValue(value: any, ctx: any = {}): string {
36
78
  if (value === null || value === undefined) return ''
@@ -42,26 +84,63 @@ export default class Ride {
42
84
  let newValue = value
43
85
  matches.forEach(m => {
44
86
  const code = m.slice(1, -1) // 中身
45
- // sandbox を作り、console.log の出力を収集する
87
+
88
+ // まず、保存済み move を実行して最新の状態にする
89
+ this.runPersistedMoves(ctx)
90
+
91
+ // 出力収集用
46
92
  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
- }
93
+
94
+ // sandbox を組み立て:ctx をコピー、riner、そしてトップレベルに rinerState を展開
95
+ const sandbox: Record<string, any> = Object.assign({}, ctx)
96
+
97
+ // riner オブジェクト(host 側のメソッドはクロスコンテキストで呼ばれる)
98
+ sandbox.riner = {
99
+ // 出力を収集するために riner.out を提供
100
+ out: (...args: any[]) => output.push(args.map(a => String(a)).join(' ')),
101
+ // riner.move は sandbox 内で関数を渡すと、その関数ソースを保存して以降持続的に実行される
102
+ move: (fn: Function) => {
103
+ try {
104
+ const src = fn.toString()
105
+ // 重複を避けるため同一ソースは再登録しない
106
+ if (!this.rinerMoves.includes(src)) this.rinerMoves.push(src)
107
+ return true
108
+ } catch (e) {
109
+ return false
110
+ }
111
+ },
112
+ // 直接 state にアクセスできるように参照を渡す
113
+ state: this.rinerState
114
+ }
115
+
116
+ // トップレベルでも直接 state の各キーを参照できるように注入
117
+ Object.keys(this.rinerState).forEach(k => {
118
+ sandbox[k] = this.rinerState[k]
53
119
  })
54
120
 
55
- // まずは vm で実行(タイムアウト付き)
121
+ // console を簡易的にラップして出力を収集
122
+ sandbox.console = {
123
+ log: (...args: any[]) => output.push(args.map(a => String(a)).join(' ')),
124
+ error: (...args: any[]) => output.push(args.map(a => String(a)).join(' ')),
125
+ warn: (...args: any[]) => output.push(args.map(a => String(a)).join(' '))
126
+ }
127
+
128
+ // 実行
129
+ console.log(sandbox)
130
+ console.log(code)
56
131
  let res: any
57
132
  try {
58
- // 実行が式でも文でも動くようにそのまま実行
59
133
  res = runInNewContext(code, sandbox, { timeout: 2000 })
60
134
  } catch (e) {
61
- // 実行に失敗したら空文字に
62
135
  res = undefined
63
136
  }
64
137
 
138
+ // sandbox 上で変更されたトップレベル変数を rinerState に反映
139
+ Object.keys(sandbox).forEach(k => {
140
+ if (k === 'riner' || k === 'console' || k === 'req' || k === 'server' || k === 'page' || k === 'data') return
141
+ this.rinerState[k] = sandbox[k]
142
+ })
143
+
65
144
  const out = output.length ? output.join('\n') : (res !== undefined ? String(res) : '')
66
145
  newValue = newValue.replace(m, out)
67
146
  })
@@ -164,7 +243,8 @@ export default class Ride {
164
243
  * Ride メソッド(ファイル読み込みと HTML 生成)
165
244
  */
166
245
  Ride(name: string) {
167
- this.F = readFileSync(path.join(__dirname, "../../", name), { encoding: "utf-8"})
246
+ // this.F = readFileSync(path.join(__dirname, "../../", name), { encoding: "utf-8"})
247
+ this.F = readFileSync(path.join(process.cwd(), name), { encoding: "utf-8"})
168
248
  this.returnhtml()
169
249
  }
170
250
 
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "node-riner",
3
- "version": "1.3.2",
3
+ "version": "2.0.0",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
7
- "dev": "tsx script.ts"
7
+ "dev": "tsx t.ts"
8
8
  },
9
9
  "keywords": [],
10
10
  "author": "",
package/runs.js DELETED
@@ -1 +0,0 @@
1
- // debug
package/t.rd DELETED
File without changes
package/t.ts DELETED
File without changes