node-riner 1.3.1 → 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 +155 -53
- 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
|
@@ -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,137 @@ 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 // サーバ/アプリ側で渡したい既定のコンテキスト
|
|
19
|
+
|
|
20
|
+
// riner 用の持続状態と登録済み move 関数のソース一覧
|
|
21
|
+
private rinerState: Record<string, any>
|
|
22
|
+
private rinerMoves: string[]
|
|
17
23
|
|
|
18
24
|
constructor(name: string) {
|
|
19
25
|
this.F = ""
|
|
20
26
|
this.parsedJson = {}
|
|
21
27
|
this.pages = {}
|
|
28
|
+
this.pagesTemplates = {}
|
|
22
29
|
this.name = name
|
|
23
30
|
this.defaultinfo = ""
|
|
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
|
+
})
|
|
24
70
|
}
|
|
25
71
|
|
|
26
72
|
/**
|
|
27
73
|
* 値を再帰的に HTML に変換するユーティリティ
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
30
|
-
* - オブジェクト: 各キーをタグ名として再帰的にレンダリング
|
|
31
|
-
*
|
|
32
|
-
* 注意: '@_' で始まるキー(属性)はここではタグ化しない。
|
|
74
|
+
* - ctx を渡すと { ... } 内のコードをそのコンテキストで評価する
|
|
75
|
+
* - riner.out / riner.move / riner.state を提供
|
|
33
76
|
*/
|
|
34
|
-
private renderValue(value: any): string {
|
|
35
|
-
// null / undefined
|
|
77
|
+
private renderValue(value: any, ctx: any = {}): string {
|
|
36
78
|
if (value === null || value === undefined) return ''
|
|
37
79
|
|
|
38
80
|
// 文字列処理({...} の評価を含む)
|
|
39
81
|
if (typeof value === 'string') {
|
|
40
|
-
// 複数マッチにも対応
|
|
41
82
|
const matches = value.match(/{.*?}/g)
|
|
42
83
|
if (matches) {
|
|
43
84
|
let newValue = value
|
|
44
85
|
matches.forEach(m => {
|
|
45
|
-
const code = m.slice(1, -1) //
|
|
86
|
+
const code = m.slice(1, -1) // 中身
|
|
87
|
+
|
|
88
|
+
// まず、保存済み move を実行して最新の状態にする
|
|
89
|
+
this.runPersistedMoves(ctx)
|
|
90
|
+
|
|
91
|
+
// 出力収集用
|
|
92
|
+
const output: string[] = []
|
|
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]
|
|
119
|
+
})
|
|
120
|
+
|
|
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)
|
|
131
|
+
let res: any
|
|
46
132
|
try {
|
|
47
|
-
|
|
48
|
-
writeFileSync("./runs.js", code)
|
|
49
|
-
const out = execSync("node runs.js").toString()
|
|
50
|
-
newValue = newValue.replace(m, out)
|
|
133
|
+
res = runInNewContext(code, sandbox, { timeout: 2000 })
|
|
51
134
|
} catch (e) {
|
|
52
|
-
|
|
135
|
+
res = undefined
|
|
53
136
|
}
|
|
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
|
+
|
|
144
|
+
const out = output.length ? output.join('\n') : (res !== undefined ? String(res) : '')
|
|
145
|
+
newValue = newValue.replace(m, out)
|
|
54
146
|
})
|
|
55
147
|
return newValue
|
|
56
148
|
}
|
|
@@ -59,74 +151,64 @@ export default class Ride {
|
|
|
59
151
|
|
|
60
152
|
// 配列: 各要素を再帰的に処理して <item> でラップ
|
|
61
153
|
if (Array.isArray(value)) {
|
|
62
|
-
return value.map((item: any) => `<item>${this.renderValue(item)}</item>`).join('')
|
|
154
|
+
return value.map((item: any) => `<item>${this.renderValue(item, ctx)}</item>`).join('')
|
|
63
155
|
}
|
|
64
156
|
|
|
65
157
|
// オブジェクト: 属性(@_...) を無視して子要素だけタグ化する
|
|
66
158
|
if (typeof value === 'object') {
|
|
67
159
|
return Object.keys(value)
|
|
68
|
-
.filter(key => !key.startsWith('@_'))
|
|
160
|
+
.filter(key => !key.startsWith('@_'))
|
|
69
161
|
.map(key => {
|
|
70
|
-
// テキストノードは直接中身を出力
|
|
71
162
|
if (key === '#text' || key === '_text') {
|
|
72
|
-
return this.renderValue(value[key])
|
|
163
|
+
return this.renderValue(value[key], ctx)
|
|
73
164
|
}
|
|
74
|
-
return `<${key}>${this.renderValue(value[key])}</${key}>`
|
|
165
|
+
return `<${key}>${this.renderValue(value[key], ctx)}</${key}>`
|
|
75
166
|
}).join('')
|
|
76
167
|
}
|
|
77
168
|
|
|
78
|
-
// その他(数値や真偽値など)はそのまま文字列化
|
|
79
169
|
return String(value)
|
|
80
170
|
}
|
|
81
171
|
|
|
82
172
|
/**
|
|
83
173
|
* タグ名とその値から、属性を正しく反映した HTML を生成する
|
|
84
|
-
* -
|
|
85
|
-
* - value がオブジェクトで属性(@_...)を持つ場合、属性を開タグに組み込む
|
|
174
|
+
* - ctx を渡して { ... } の中で req / server / page / data が参照できる
|
|
86
175
|
*/
|
|
87
|
-
private renderContentTag(tag: string, value: any): string {
|
|
88
|
-
// 配列の場合は各要素で同じタグを生成
|
|
176
|
+
private renderContentTag(tag: string, value: any, ctx: any = {}): string {
|
|
89
177
|
if (Array.isArray(value)) {
|
|
90
|
-
return value.map((item: any) => this.renderContentTag(tag, item)).join('')
|
|
178
|
+
return value.map((item: any) => this.renderContentTag(tag, item, ctx)).join('')
|
|
91
179
|
}
|
|
92
180
|
|
|
93
|
-
// プリミティブ(文字列等)の場合はタグでラップ
|
|
94
181
|
if (value === null || value === undefined || typeof value !== 'object') {
|
|
95
|
-
return `<${tag}>${this.renderValue(value)}</${tag}>`
|
|
182
|
+
return `<${tag}>${this.renderValue(value, ctx)}</${tag}>`
|
|
96
183
|
}
|
|
97
184
|
|
|
98
|
-
// オブジェクトの場合、属性を抽出
|
|
99
185
|
const entries = Object.entries(value)
|
|
100
186
|
const attrEntries = entries.filter(([k]) => k.startsWith('@_'))
|
|
101
187
|
const childEntries = entries.filter(([k]) => !k.startsWith('@_'))
|
|
102
188
|
|
|
103
|
-
// 属性文字列を組み立て(@_ を取り除く)
|
|
104
189
|
const attrs = attrEntries.map(([k, v]) => {
|
|
105
190
|
const name = k.replace(/^@_/, '')
|
|
106
191
|
const safe = String(v).replace(/"/g, '"')
|
|
107
192
|
return ` ${name}="${safe}"`
|
|
108
193
|
}).join('')
|
|
109
194
|
|
|
110
|
-
// 子要素・テキストを組み立て
|
|
111
195
|
let inner = ''
|
|
112
|
-
// 優先してテキストノードを出す
|
|
113
196
|
if (value['#text'] !== undefined) {
|
|
114
|
-
inner += this.renderValue(value['#text'])
|
|
197
|
+
inner += this.renderValue(value['#text'], ctx)
|
|
115
198
|
} else if (value['_text'] !== undefined) {
|
|
116
|
-
inner += this.renderValue(value['_text'])
|
|
199
|
+
inner += this.renderValue(value['_text'], ctx)
|
|
117
200
|
}
|
|
118
201
|
|
|
119
|
-
// その他の子要素をレンダリング
|
|
120
202
|
childEntries.forEach(([k, v]) => {
|
|
121
203
|
if (k === '#text' || k === '_text') return
|
|
122
|
-
inner += `<${k}>${this.renderValue(v)}</${k}>`
|
|
204
|
+
inner += `<${k}>${this.renderValue(v, ctx)}</${k}>`
|
|
123
205
|
})
|
|
124
206
|
|
|
125
207
|
return `<${tag}${attrs}>${inner}</${tag}>`
|
|
126
208
|
}
|
|
127
209
|
|
|
128
210
|
/**
|
|
129
|
-
* XML を解析して
|
|
211
|
+
* XML を解析して pagesTemplates に生の Content を格納(評価はリクエスト時に行う)
|
|
130
212
|
*/
|
|
131
213
|
returnhtml(): string {
|
|
132
214
|
const options = { ignoreAttributes: false }
|
|
@@ -141,24 +223,14 @@ export default class Ride {
|
|
|
141
223
|
const pages = root["Page"]
|
|
142
224
|
if (!pages) return ""
|
|
143
225
|
|
|
144
|
-
// Page が配列か単一かを吸収して配列化
|
|
145
226
|
const pageArray = Array.isArray(pages) ? pages : [pages]
|
|
146
227
|
|
|
147
228
|
pageArray.forEach((page: any) => {
|
|
148
229
|
const route = page['@_route'] || '/'
|
|
149
|
-
|
|
150
|
-
// Content が存在しない場合は空にする
|
|
151
230
|
const content = page['Content'] || {}
|
|
152
231
|
|
|
153
|
-
//
|
|
154
|
-
|
|
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
|
|
232
|
+
// 生の content を保存しておく(ルートアクセス時に評価する)
|
|
233
|
+
this.pagesTemplates[route] = { content, pageMeta: page }
|
|
162
234
|
})
|
|
163
235
|
} catch (e) {
|
|
164
236
|
console.error("Whoops, an error occurred during analysis : " + e)
|
|
@@ -171,19 +243,45 @@ export default class Ride {
|
|
|
171
243
|
* Ride メソッド(ファイル読み込みと HTML 生成)
|
|
172
244
|
*/
|
|
173
245
|
Ride(name: string) {
|
|
174
|
-
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"})
|
|
175
248
|
this.returnhtml()
|
|
176
249
|
}
|
|
177
250
|
|
|
178
251
|
/**
|
|
179
252
|
* 簡易サーバを起動
|
|
253
|
+
* - リクエスト毎に { ... } の評価を行い、req, query, params, body, server(defaultContext), page が参照可能になる
|
|
180
254
|
*/
|
|
181
255
|
serve(port: number) {
|
|
182
256
|
const app = exp();
|
|
183
257
|
|
|
184
|
-
Object.keys(this.
|
|
258
|
+
Object.keys(this.pagesTemplates).forEach(route => {
|
|
185
259
|
app.get(route, (req, res) => {
|
|
186
|
-
|
|
260
|
+
const tpl = this.pagesTemplates[route]
|
|
261
|
+
const content = tpl?.content || {}
|
|
262
|
+
|
|
263
|
+
// コンテキストを組み立て(デフォルト + リクエスト情報 + ページメタ)
|
|
264
|
+
const ctx = {
|
|
265
|
+
server: this.defaultContext,
|
|
266
|
+
req: {
|
|
267
|
+
params: req.params,
|
|
268
|
+
query: req.query,
|
|
269
|
+
path: req.path,
|
|
270
|
+
headers: req.headers
|
|
271
|
+
},
|
|
272
|
+
page: tpl.pageMeta || {},
|
|
273
|
+
// 任意のデータを追加できるように空の data を用意
|
|
274
|
+
data: {}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 各タグを評価して結合
|
|
278
|
+
const generatedhtml = Object.keys(content).map(tag => {
|
|
279
|
+
const value = content[tag]
|
|
280
|
+
return this.renderContentTag(tag, value, ctx)
|
|
281
|
+
}).join('')
|
|
282
|
+
|
|
283
|
+
// タイトルを付与して送信
|
|
284
|
+
res.send(`<title>${this.name}</title>` + generatedhtml)
|
|
187
285
|
})
|
|
188
286
|
})
|
|
189
287
|
|
|
@@ -194,7 +292,11 @@ export default class Ride {
|
|
|
194
292
|
return app
|
|
195
293
|
}
|
|
196
294
|
|
|
295
|
+
/**
|
|
296
|
+
* サーバ側で渡したい既定のコンテキストを設定する
|
|
297
|
+
* 例: ride.config({ currentUser: {...}, env: process.env.NODE_ENV })
|
|
298
|
+
*/
|
|
197
299
|
config(options: object) {
|
|
198
|
-
|
|
300
|
+
this.defaultContext = Object.assign({}, this.defaultContext, options)
|
|
199
301
|
}
|
|
200
302
|
}
|
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
|