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.
- package/README.md +72 -15
- package/index.ts +92 -12
- package/package.json +2 -2
- package/runs.js +0 -1
- package/t.rd +0 -0
- package/t.ts +0 -0
package/README.md
CHANGED
|
@@ -1,28 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Riner Framework (Node Riner)
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
* -
|
|
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
|
-
|
|
87
|
+
|
|
88
|
+
// まず、保存済み move を実行して最新の状態にする
|
|
89
|
+
this.runPersistedMoves(ctx)
|
|
90
|
+
|
|
91
|
+
// 出力収集用
|
|
46
92
|
const output: string[] = []
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
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
package/runs.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// debug
|
package/t.rd
DELETED
|
File without changes
|
package/t.ts
DELETED
|
File without changes
|