nadesiko3 3.7.21 → 3.7.23

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 (212) hide show
  1. package/README.md +20 -1
  2. package/batch/command.txt +370 -334
  3. package/batch/generate_command_list.mjs +102 -0
  4. package/batch/jsplugin2text.nako3 +1 -1
  5. package/batch/pickup_command.nako3 +12 -0
  6. package/batch/search_command.mjs +34 -0
  7. package/batch/search_command.nako3 +430 -0
  8. package/core/command/snako.mts +5 -1
  9. package/core/deno/snako.ts +5 -1
  10. package/core/package-lock.json +382 -21
  11. package/core/package.json +7 -4
  12. package/core/sample/algorithms/README.md +43 -0
  13. package/core/sample/algorithms/graph/dijkstra.nako3 +47 -0
  14. package/core/sample/algorithms/graph/kruskal.nako3 +69 -0
  15. package/core/sample/algorithms/math/euclidean_gcd.nako3 +15 -0
  16. package/core/sample/algorithms/math/sieve_of_eratosthenes.nako3 +26 -0
  17. package/core/sample/algorithms/search/binary_search.nako3 +17 -0
  18. package/core/sample/algorithms/search/breadth_first_search.nako3 +33 -0
  19. package/core/sample/algorithms/search/depth_first_search.nako3 +25 -0
  20. package/core/sample/algorithms/sort/heap_sort.nako3 +37 -0
  21. package/core/sample/algorithms/sort/insertion_sort.nako3 +18 -0
  22. package/core/sample/algorithms/sort/merge_sort.nako3 +40 -0
  23. package/core/sample/algorithms/sort/quick_sort.nako3 +36 -0
  24. package/core/sample/algorithms/string/knuth_morris_pratt.nako3 +50 -0
  25. package/core/sample/algorithms/string/run_length_encoding.nako3 +25 -0
  26. package/core/src/nako3.mts +178 -614
  27. package/core/src/nako_basic_plugins.mts +39 -0
  28. package/core/src/nako_core_version.mts +2 -2
  29. package/core/src/nako_csv.mts +0 -1
  30. package/core/src/nako_event.mts +49 -0
  31. package/core/src/nako_gen.mts +320 -209
  32. package/core/src/nako_global.mts +18 -0
  33. package/core/src/nako_indent_inline.mts +2 -2
  34. package/core/src/nako_lex_rules.mts +1 -1
  35. package/core/src/nako_parser3.mts +111 -166
  36. package/core/src/nako_parser_async.mts +165 -0
  37. package/core/src/nako_parser_base.mts +4 -55
  38. package/core/src/nako_parser_message.mts +105 -0
  39. package/core/src/nako_parser_operator.mts +93 -0
  40. package/core/src/nako_plugin_manager.mts +260 -0
  41. package/core/src/nako_require.mts +292 -0
  42. package/core/src/nako_runner.mts +208 -0
  43. package/core/src/nako_tokenizer.mts +221 -0
  44. package/core/src/plugin_csv.mts +1 -1
  45. package/core/src/plugin_system.mts +34 -3259
  46. package/core/src/plugin_system_array.mts +699 -0
  47. package/core/src/plugin_system_datetime.mts +368 -0
  48. package/core/src/plugin_system_debug.mts +403 -0
  49. package/core/src/plugin_system_dict.mts +85 -0
  50. package/core/src/plugin_system_json.mts +73 -0
  51. package/core/src/plugin_system_math.mts +383 -0
  52. package/core/src/plugin_system_regexp.mts +120 -0
  53. package/core/src/plugin_system_stdio.mts +86 -0
  54. package/core/src/plugin_system_string.mts +666 -0
  55. package/core/src/plugin_system_timer.mts +152 -0
  56. package/core/src/plugin_system_types.mts +151 -0
  57. package/core/src/plugin_system_url.mts +193 -0
  58. package/core/src/plugin_toml.mts +3 -3
  59. package/core/test/algorithm_samples_test.mjs +58 -0
  60. package/core/test/algorithms/graph/dijkstra_test.nako3 +21 -0
  61. package/core/test/algorithms/graph/kruskal_test.nako3 +21 -0
  62. package/core/test/algorithms/helper.mjs +13 -0
  63. package/core/test/algorithms/math/euclidean_gcd_test.nako3 +13 -0
  64. package/core/test/algorithms/math/sieve_of_eratosthenes_test.nako3 +10 -0
  65. package/core/test/algorithms/search/binary_search_test.nako3 +15 -0
  66. package/core/test/algorithms/search/breadth_first_search_test.nako3 +13 -0
  67. package/core/test/algorithms/search/depth_first_search_test.nako3 +13 -0
  68. package/core/test/algorithms/sort/heap_sort_test.nako3 +13 -0
  69. package/core/test/algorithms/sort/insertion_sort_test.nako3 +13 -0
  70. package/core/test/algorithms/sort/merge_sort_test.nako3 +18 -0
  71. package/core/test/algorithms/sort/quick_sort_test.nako3 +34 -0
  72. package/core/test/algorithms/string/knuth_morris_pratt_test.nako3 +16 -0
  73. package/core/test/algorithms/string/run_length_encoding_test.nako3 +13 -0
  74. package/core/test/array_test.mjs +3 -0
  75. package/core/test/fixtures/README.md +39 -0
  76. package/core/test/fixtures/make_parser_ast_golden.mjs +31 -0
  77. package/core/test/fixtures/parser_ast_golden.json +8027 -0
  78. package/core/test/fixtures/parser_corpus.mjs +120 -0
  79. package/core/test/func_call.mjs +18 -0
  80. package/core/test/func_test.mjs +28 -0
  81. package/core/test/indent_test.mjs +6 -0
  82. package/core/test/nako_basic_plugins_test.mjs +44 -0
  83. package/core/test/nako_event_test.mjs +127 -0
  84. package/core/test/nako_gen_perf_test.mjs +178 -0
  85. package/core/test/nako_parser_async_test.mjs +385 -0
  86. package/core/test/nako_parser_test.mjs +160 -0
  87. package/core/test/nako_plugin_manager_test.mjs +185 -0
  88. package/core/test/nako_require_test.mjs +153 -0
  89. package/core/test/nako_runner_test.mjs +174 -0
  90. package/core/test/nako_tokenizer_test.mjs +115 -0
  91. package/core/test/plugin_system_debug_test.mjs +138 -0
  92. package/core/test/plugin_system_split_test.mjs +179 -0
  93. package/core/test/plugin_system_test.mjs +6 -0
  94. package/core/tsconfig.json +0 -1
  95. package/demo/browsers.html +1 -1
  96. package/doc/SETUP.md +1 -1
  97. package/doc/ai-code-generation.md +136 -0
  98. package/doc/browsers.md +1 -1
  99. package/doc/command_list.json +16842 -0
  100. package/doc/search_command.md +222 -0
  101. package/doc/syntax-nako3.md +344 -0
  102. package/package.json +29 -26
  103. package/release/_hash.txt +40 -40
  104. package/release/_script-tags.txt +16 -16
  105. package/release/command.json +1 -1
  106. package/release/command.json.js +1 -1
  107. package/release/command_cnako3.json +1 -1
  108. package/release/command_list.json +1 -1
  109. package/release/edit_main.js +6 -6
  110. package/release/edit_main.js.map +3 -3
  111. package/release/editor.js +6 -6
  112. package/release/plugin_caniuse.js +1 -1
  113. package/release/plugin_caniuse.js.map +2 -2
  114. package/release/plugin_keigo.js.map +3 -3
  115. package/release/plugin_markup.js +46 -46
  116. package/release/plugin_markup.js.map +3 -3
  117. package/release/plugin_weykturtle3d.js +1 -1
  118. package/release/plugin_weykturtle3d.js.map +3 -3
  119. package/release/version.js +2 -2
  120. package/release/version_main.js +2 -2
  121. package/release/version_main.js.map +1 -1
  122. package/release/wnako3.js +219 -230
  123. package/release/wnako3.js.map +4 -4
  124. package/release/wnako3webworker.js +193 -204
  125. package/release/wnako3webworker.js.map +4 -4
  126. package/src/browsers.mjs +1 -1
  127. package/src/browsers.txt +0 -1
  128. package/src/cnako3mod.mjs +7 -2
  129. package/src/cnako3mod.mts +7 -2
  130. package/src/nako_version.mjs +2 -2
  131. package/src/nako_version.mts +2 -2
  132. package/src/plugin_browser_ajax.mjs +4 -4
  133. package/src/plugin_browser_ajax.mts +4 -4
  134. package/src/plugin_browser_audio.mjs +10 -10
  135. package/src/plugin_browser_audio.mts +10 -10
  136. package/src/plugin_browser_camera.mjs +4 -4
  137. package/src/plugin_browser_camera.mts +4 -4
  138. package/src/plugin_browser_canvas.mjs +2 -2
  139. package/src/plugin_browser_canvas.mts +2 -2
  140. package/src/plugin_browser_crypto.mjs +3 -3
  141. package/src/plugin_browser_crypto.mts +3 -3
  142. package/src/plugin_browser_dom_event.mjs +1 -1
  143. package/src/plugin_browser_dom_event.mts +1 -1
  144. package/src/plugin_browser_geolocation.mjs +1 -1
  145. package/src/plugin_browser_geolocation.mts +1 -1
  146. package/src/plugin_browser_hotkey.mjs +1 -1
  147. package/src/plugin_browser_hotkey.mts +1 -1
  148. package/src/plugin_browser_html.mjs +1 -1
  149. package/src/plugin_browser_html.mts +1 -1
  150. package/src/plugin_browser_location.mjs +1 -1
  151. package/src/plugin_browser_location.mts +1 -1
  152. package/src/plugin_browser_speech.mjs +1 -1
  153. package/src/plugin_browser_speech.mts +1 -1
  154. package/src/plugin_browser_storage.mjs +2 -2
  155. package/src/plugin_browser_storage.mts +2 -2
  156. package/src/plugin_httpserver.mjs +3 -3
  157. package/src/plugin_httpserver.mts +3 -3
  158. package/src/plugin_keigo.mjs +2 -2
  159. package/src/plugin_keigo.mts +2 -2
  160. package/src/plugin_node.mjs +48 -48
  161. package/src/plugin_node.mts +53 -53
  162. package/src/plugin_weykturtle3d.mjs +56 -56
  163. package/src/plugin_weykturtle3d.mts +56 -56
  164. package/src/wnako3.mjs +1 -1
  165. package/src/wnako3.mts +1 -1
  166. package/src/wnako3_editor.mjs +5 -5
  167. package/src/wnako3_editor.mts +5 -5
  168. package/src/wnako3mod.mjs +2 -2
  169. package/src/wnako3mod.mts +2 -2
  170. package/test/nako3edit/index_nako3_test.mjs +282 -0
  171. package/test/nako3server/index_nako3_test.mjs +239 -0
  172. package/test/node/ai_code_generation_guide_test.mjs +29 -0
  173. package/test/node/node_version_support_test.mjs +45 -0
  174. package/test/node/search_command_test.mjs +174 -0
  175. package/tools/nako3edit/AGENTS.md +23 -0
  176. package/tools/nako3edit/index.nako3 +404 -0
  177. package/tools/nako3server/AGENTS.md +23 -0
  178. package/tools/nako3server/index.nako3 +156 -23
  179. package/core/src/nako3.mjs +0 -1021
  180. package/core/src/nako_ast.mjs +0 -4
  181. package/core/src/nako_colors.mjs +0 -77
  182. package/core/src/nako_core_version.mjs +0 -8
  183. package/core/src/nako_csv.mjs +0 -193
  184. package/core/src/nako_errors.mjs +0 -166
  185. package/core/src/nako_from_dncl.mjs +0 -285
  186. package/core/src/nako_from_dncl2.mjs +0 -347
  187. package/core/src/nako_gen.mjs +0 -2500
  188. package/core/src/nako_global.mjs +0 -138
  189. package/core/src/nako_indent.mjs +0 -442
  190. package/core/src/nako_indent_chars.mjs +0 -29
  191. package/core/src/nako_indent_inline.mjs +0 -361
  192. package/core/src/nako_josi_list.mjs +0 -47
  193. package/core/src/nako_lex_rules.mjs +0 -319
  194. package/core/src/nako_lexer.mjs +0 -794
  195. package/core/src/nako_logger.mjs +0 -221
  196. package/core/src/nako_parser3.mjs +0 -3250
  197. package/core/src/nako_parser_base.mjs +0 -403
  198. package/core/src/nako_parser_const.mjs +0 -37
  199. package/core/src/nako_prepare.mjs +0 -329
  200. package/core/src/nako_reserved_words.mjs +0 -42
  201. package/core/src/nako_source_mapping.mjs +0 -207
  202. package/core/src/nako_test.mjs +0 -37
  203. package/core/src/nako_token.mjs +0 -1
  204. package/core/src/nako_tools.mjs +0 -53
  205. package/core/src/nako_types.mjs +0 -14
  206. package/core/src/plugin_api.mjs +0 -4
  207. package/core/src/plugin_csv.mjs +0 -97
  208. package/core/src/plugin_math.mjs +0 -352
  209. package/core/src/plugin_promise.mjs +0 -102
  210. package/core/src/plugin_system.mjs +0 -3810
  211. package/core/src/plugin_test.mjs +0 -52
  212. package/core/src/plugin_toml.mjs +0 -39
@@ -0,0 +1,282 @@
1
+ /* eslint-disable no-undef */
2
+ // tools/nako3edit/index.nako3 (なでしこ3版エディタサーバー)のテスト
3
+ // 一時HOMEでサーバーを起動し、実際のHTTP APIとファイル操作を確認する。
4
+ import assert from 'assert'
5
+ import path from 'path'
6
+ import fs from 'fs'
7
+ import http from 'http'
8
+ import net from 'net'
9
+ import os from 'os'
10
+ import { spawn } from 'child_process'
11
+ import url from 'url'
12
+
13
+ const __filename = url.fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+ const rootDir = path.resolve(__dirname, '../../')
16
+ const cnako3Path = path.join(rootDir, 'src/cnako3.mjs')
17
+ const editorDir = path.join(rootDir, 'tools/nako3edit')
18
+ const editorNako3 = path.join(editorDir, 'index.nako3')
19
+
20
+ /** 空いているポート番号を取得する */
21
+ function getFreePort () {
22
+ return new Promise((resolve, reject) => {
23
+ const srv = net.createServer()
24
+ srv.on('error', reject)
25
+ srv.listen(0, '127.0.0.1', () => {
26
+ const port = srv.address().port
27
+ srv.close(() => { resolve(port) })
28
+ })
29
+ })
30
+ }
31
+
32
+ /** 一時HOMEを指定してcnako3版nako3editを起動する */
33
+ function spawnServer (homeDir, env = {}, args = []) {
34
+ return spawn(process.execPath, [cnako3Path, editorNako3, ...args], {
35
+ cwd: editorDir,
36
+ env: {
37
+ ...process.env,
38
+ HOME: homeDir,
39
+ USERPROFILE: homeDir,
40
+ NAKO3EDIT_HOST: '127.0.0.1',
41
+ NAKO3EDIT_OPEN: '0',
42
+ ...env
43
+ }
44
+ })
45
+ }
46
+
47
+ /** 子プロセスの標準出力に文字列が現れるまで待つ */
48
+ function waitForOutput (child, mark, timeoutMs = 60000) {
49
+ return new Promise((resolve, reject) => {
50
+ let buf = ''
51
+ const timer = setTimeout(() => {
52
+ cleanup()
53
+ reject(new Error(`タイムアウト: 「${mark}」が出力されませんでした。\n出力=${buf}`))
54
+ }, timeoutMs)
55
+ const onData = (chunk) => {
56
+ buf += chunk.toString()
57
+ if (buf.includes(mark)) {
58
+ cleanup()
59
+ resolve(buf)
60
+ }
61
+ }
62
+ const onExit = () => {
63
+ cleanup()
64
+ reject(new Error(`サーバーが起動前に終了しました。\n出力=${buf}`))
65
+ }
66
+ const cleanup = () => {
67
+ clearTimeout(timer)
68
+ child.stdout.off('data', onData)
69
+ child.stderr.off('data', onData)
70
+ child.off('exit', onExit)
71
+ }
72
+ child.stdout.on('data', onData)
73
+ child.stderr.on('data', onData)
74
+ child.on('exit', onExit)
75
+ })
76
+ }
77
+
78
+ /** 子プロセスを確実に終了する */
79
+ function killServer (child) {
80
+ return new Promise((resolve) => {
81
+ if (!child || child.exitCode !== null || child.signalCode !== null) { resolve(); return }
82
+ child.once('exit', () => { resolve() })
83
+ child.kill('SIGKILL')
84
+ })
85
+ }
86
+
87
+ /** HTTP GETを実行する。リダイレクトは追わない。 */
88
+ function httpGet (port, reqPath) {
89
+ return new Promise((resolve, reject) => {
90
+ const req = http.request({ host: '127.0.0.1', port, path: reqPath, method: 'GET' }, (res) => {
91
+ const chunks = []
92
+ res.on('data', (chunk) => { chunks.push(chunk) })
93
+ res.on('end', () => {
94
+ resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })
95
+ })
96
+ })
97
+ req.setTimeout(10000, () => { req.destroy(new Error(`HTTPタイムアウト: ${reqPath}`)) })
98
+ req.on('error', reject)
99
+ req.end()
100
+ })
101
+ }
102
+
103
+ /** クエリをURLエンコードしてAPIパスを作る */
104
+ function apiPath (pathname, params) {
105
+ return `${pathname}?${new URLSearchParams(params).toString()}`
106
+ }
107
+
108
+ describe('nako3edit/index.nako3', () => {
109
+ let child = null
110
+ let port = 0
111
+ let tempHome = ''
112
+ let userDir = ''
113
+ let appkey = ''
114
+
115
+ before(async () => {
116
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'nako3edit-test-'))
117
+ userDir = path.join(tempHome, 'nadesiko3_user')
118
+ port = await getFreePort()
119
+ child = spawnServer(tempHome, { NAKO3EDIT_PORT: String(port) })
120
+ await waitForOutput(child, '### 超簡易Webサーバが起動しました')
121
+
122
+ const root = await httpGet(port, '/')
123
+ assert.strictEqual(root.status, 302)
124
+ const location = root.headers.location
125
+ assert.ok(location.startsWith('/html/files.html?appkey='))
126
+ appkey = new URL(`http://localhost${location}`).searchParams.get('appkey')
127
+ assert.ok(appkey.startsWith('k'))
128
+ })
129
+
130
+ after(async () => {
131
+ await killServer(child)
132
+ if (tempHome && fs.existsSync(tempHome)) {
133
+ fs.rmSync(tempHome, { recursive: true, force: true })
134
+ }
135
+ })
136
+
137
+ it('ユーザーフォルダを作成して空のファイル一覧を返すこと', async () => {
138
+ const res = await httpGet(port, '/files')
139
+ assert.strictEqual(res.status, 200)
140
+ assert.strictEqual(res.headers['content-type'], 'application/json')
141
+ assert.deepStrictEqual(JSON.parse(res.body.toString('utf-8')), [])
142
+ assert.ok(fs.statSync(userDir).isDirectory())
143
+ })
144
+
145
+ it('存在しないファイルには新規ファイルの初期値を返すこと', async () => {
146
+ const res = await httpGet(port, apiPath('/load', { file: 'missing.nako3' }))
147
+ assert.strictEqual(res.status, 200)
148
+ assert.strictEqual(res.headers['content-type'], 'text/plain')
149
+ assert.strictEqual(res.body.toString('utf-8'), '# 新規ファイル\n「こんにちは」と表示。')
150
+ })
151
+
152
+ it('アプリキーが違う場合は保存しないこと', async () => {
153
+ const res = await httpGet(port, apiPath('/save', {
154
+ appkey: 'wrong-key',
155
+ file: 'invalid.nako3',
156
+ body: '保存されない'
157
+ }))
158
+ assert.strictEqual(res.body.toString('utf-8'), '[ERROR] キーが違います')
159
+ assert.ok(!fs.existsSync(path.join(userDir, 'invalid.nako3')))
160
+ })
161
+
162
+ it('ファイル名をサニタイズして保存・読込できること', async () => {
163
+ const body = '「保存できました」と表示。'
164
+ const unsafeName = '../sample/test?.nako3'
165
+ const savedName = '.._sample_test_.nako3'
166
+ const save = await httpGet(port, apiPath('/save', { appkey, file: unsafeName, body }))
167
+ assert.strictEqual(save.body.toString('utf-8'), 'ok')
168
+ assert.strictEqual(fs.readFileSync(path.join(userDir, savedName), 'utf-8'), body)
169
+
170
+ const load = await httpGet(port, apiPath('/load', { file: unsafeName }))
171
+ assert.strictEqual(load.body.toString('utf-8'), body)
172
+ })
173
+
174
+ it('保存したファイルを一覧で返すこと', async () => {
175
+ const res = await httpGet(port, '/files')
176
+ const files = JSON.parse(res.body.toString('utf-8'))
177
+ assert.ok(files.includes('.._sample_test_.nako3'))
178
+ })
179
+
180
+ it('/runで保存したプログラムをcnako3実行できること', async () => {
181
+ const res = await httpGet(port, apiPath('/run', {
182
+ appkey,
183
+ file: 'run-test.nako3',
184
+ body: '「RUN_OK」と表示。'
185
+ }))
186
+ assert.strictEqual(res.status, 200)
187
+ assert.match(res.body.toString('utf-8'), /RUN_OK/)
188
+ assert.ok(fs.existsSync(path.join(userDir, 'run-test.nako3')))
189
+ })
190
+
191
+ it('/run_directで保存済みプログラムをcnako3実行できること', async () => {
192
+ const res = await httpGet(port, apiPath('/run_direct', { appkey, file: 'run-test.nako3' }))
193
+ assert.strictEqual(res.status, 200)
194
+ assert.match(res.body.toString('utf-8'), /RUN_OK/)
195
+ })
196
+
197
+ it('未使用の新規ファイル名をJSON文字列で返すこと', async () => {
198
+ fs.writeFileSync(path.join(userDir, 'file1.nako3'), '')
199
+ const res = await httpGet(port, '/get_new_filename')
200
+ assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8')
201
+ assert.strictEqual(JSON.parse(res.body.toString('utf-8')), 'file2.nako3')
202
+ })
203
+
204
+ it('ファイルを削除し、存在しない場合はエラーを返すこと', async () => {
205
+ const target = path.join(userDir, 'delete-test.nako3')
206
+ fs.writeFileSync(target, '削除対象')
207
+ const deleted = await httpGet(port, apiPath('/deletefile', { appkey, file: 'delete-test.nako3' }))
208
+ assert.strictEqual(JSON.parse(deleted.body.toString('utf-8')), 'ok')
209
+ assert.ok(!fs.existsSync(target))
210
+
211
+ const missing = await httpGet(port, apiPath('/deletefile', { appkey, file: 'delete-test.nako3' }))
212
+ assert.strictEqual(JSON.parse(missing.body.toString('utf-8')), '[ERROR] ファイルが見つかりません。')
213
+ })
214
+
215
+ it('プラグインAPIでキーと必須パラメータを検証すること', async () => {
216
+ const wrongKey = await httpGet(port, apiPath('/get_plugins', { appkey: 'wrong-key' }))
217
+ assert.strictEqual(JSON.parse(wrongKey.body.toString('utf-8')), '[ERROR] キーが違います')
218
+
219
+ const missingName = await httpGet(port, apiPath('/add_plugins', { appkey }))
220
+ assert.strictEqual(JSON.parse(missingName.body.toString('utf-8')), '[ERROR] プラグイン名が指定されていません。')
221
+ })
222
+
223
+ it('HTMLとreleaseエイリアスの静的ファイルを返すこと', async () => {
224
+ const html = await httpGet(port, '/html/files.html')
225
+ assert.strictEqual(html.status, 200)
226
+ assert.strictEqual(html.headers['content-type'], 'text/html')
227
+ assert.ok(html.body.equals(fs.readFileSync(path.join(editorDir, 'html/files.html'))))
228
+
229
+ const release = await httpGet(port, '/release/wnako3.js')
230
+ assert.strictEqual(release.status, 200)
231
+ assert.strictEqual(release.headers['content-type'], 'text/javascript')
232
+ assert.ok(release.body.equals(fs.readFileSync(path.join(rootDir, 'release/wnako3.js'))))
233
+ })
234
+
235
+ it('存在しないファイルは404を返し、上位パス参照を除去すること', async () => {
236
+ const missing = await httpGet(port, '/no-such-file.txt')
237
+ assert.strictEqual(missing.status, 404)
238
+ assert.match(missing.body.toString('utf-8'), /404/)
239
+
240
+ const sanitized = await httpGet(port, '/ht..ml/files.html')
241
+ assert.strictEqual(sanitized.status, 200)
242
+ })
243
+ })
244
+
245
+ describe('nako3edit/index.nako3 - ポート番号の指定', () => {
246
+ let tempHome = ''
247
+
248
+ before(() => {
249
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'nako3edit-port-test-'))
250
+ })
251
+
252
+ after(() => {
253
+ if (tempHome && fs.existsSync(tempHome)) {
254
+ fs.rmSync(tempHome, { recursive: true, force: true })
255
+ }
256
+ })
257
+
258
+ it('コマンドライン引数でポート番号を指定できること', async () => {
259
+ const port = await getFreePort()
260
+ const child = spawnServer(tempHome, { NAKO3EDIT_PORT: '' }, [String(port)])
261
+ try {
262
+ const out = await waitForOutput(child, '[URL] http://')
263
+ assert.ok(out.includes(`:${port}`), `出力にポート番号${port}が含まれること 出力=${out}`)
264
+ const res = await httpGet(port, '/')
265
+ assert.strictEqual(res.status, 302)
266
+ } finally {
267
+ await killServer(child)
268
+ }
269
+ })
270
+
271
+ for (const invalidPort of ['abc', '99999']) {
272
+ it(`無効なポート番号(${invalidPort})を指定するとエラーを表示して終了すること`, async () => {
273
+ const child = spawnServer(tempHome, { NAKO3EDIT_PORT: invalidPort })
274
+ let out = ''
275
+ child.stdout.on('data', (chunk) => { out += chunk.toString() })
276
+ child.stderr.on('data', (chunk) => { out += chunk.toString() })
277
+ await new Promise((resolve) => { child.on('exit', resolve) })
278
+ assert.ok(out.includes(`[ERROR] 無効なポート番号です: ${invalidPort}`), `出力=${out}`)
279
+ assert.ok(out.includes('環境変数 NAKO3EDIT_PORT'), `出力=${out}`)
280
+ })
281
+ }
282
+ })
@@ -0,0 +1,239 @@
1
+ /* eslint-disable no-undef */
2
+ // tools/nako3server/index.nako3 (なでしこ3版の超簡易Webサーバ)のテスト
3
+ // 実際に cnako3 でサーバを起動して、HTTPリクエストを投げて動作を確認する。
4
+ import assert from 'assert'
5
+ import path from 'path'
6
+ import fs from 'fs'
7
+ import http from 'http'
8
+ import net from 'net'
9
+ import { spawn } from 'child_process'
10
+
11
+ // __dirname のために
12
+ import url from 'url'
13
+ const __filename = url.fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+
16
+ const rootDir = path.resolve(__dirname, '../../')
17
+ const cnako3Path = path.join(rootDir, 'src/cnako3.mjs')
18
+ const serverDir = path.join(rootDir, 'tools/nako3server')
19
+ const serverNako3 = path.join(serverDir, 'index.nako3')
20
+ // 外部ライブラリ(extlib)の有無を判定するファイル。無いとサーバがダウンロードを試みる。
21
+ const extlibMark = path.join(rootDir, 'demo/extlib/pure-min.css')
22
+
23
+ /** 空いているポート番号を取得する */
24
+ function getFreePort () {
25
+ return new Promise((resolve, reject) => {
26
+ const srv = net.createServer()
27
+ srv.on('error', reject)
28
+ srv.listen(0, '127.0.0.1', () => {
29
+ const port = srv.address().port
30
+ srv.close(() => { resolve(port) })
31
+ })
32
+ })
33
+ }
34
+
35
+ /** cnako3 で index.nako3 を起動する。環境変数ENVを追加で渡す。 */
36
+ function spawnServer (env) {
37
+ return spawn(process.execPath, [cnako3Path, serverNako3], {
38
+ cwd: serverDir,
39
+ env: { ...process.env, NAKO3SERVER_OPEN: '0', ...env }
40
+ })
41
+ }
42
+
43
+ /** 子プロセスの標準出力にMARKが現れるまで待つ。現れなければタイムアウトでエラー。 */
44
+ function waitForOutput (child, mark, timeoutMs) {
45
+ return new Promise((resolve, reject) => {
46
+ let buf = ''
47
+ const timer = setTimeout(() => {
48
+ cleanup()
49
+ reject(new Error(`タイムアウト: 「${mark}」が出力されませんでした。\n出力=${buf}`))
50
+ }, timeoutMs)
51
+ const onData = (chunk) => {
52
+ buf += chunk.toString()
53
+ if (buf.indexOf(mark) >= 0) {
54
+ cleanup()
55
+ resolve(buf)
56
+ }
57
+ }
58
+ const onExit = () => {
59
+ cleanup()
60
+ reject(new Error(`サーバが起動前に終了しました。\n出力=${buf}`))
61
+ }
62
+ const cleanup = () => {
63
+ clearTimeout(timer)
64
+ child.stdout.off('data', onData)
65
+ child.stderr.off('data', onData)
66
+ child.off('exit', onExit)
67
+ }
68
+ child.stdout.on('data', onData)
69
+ child.stderr.on('data', onData)
70
+ child.on('exit', onExit)
71
+ })
72
+ }
73
+
74
+ /** 子プロセスを終了させる */
75
+ function killServer (child) {
76
+ return new Promise((resolve) => {
77
+ if (!child || child.exitCode !== null || child.signalCode !== null) { resolve(); return }
78
+ child.on('exit', () => { resolve() })
79
+ child.kill('SIGKILL')
80
+ })
81
+ }
82
+
83
+ /** HTTP GETを実行して、ステータス・ヘッダ・本文(Buffer)を返す。リダイレクトは追わない。 */
84
+ function httpGet (port, reqPath) {
85
+ return new Promise((resolve, reject) => {
86
+ const req = http.request({ host: '127.0.0.1', port, path: reqPath, method: 'GET' }, (res) => {
87
+ const chunks = []
88
+ res.on('data', (c) => { chunks.push(c) })
89
+ res.on('end', () => {
90
+ resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) })
91
+ })
92
+ })
93
+ req.on('error', reject)
94
+ req.end()
95
+ })
96
+ }
97
+
98
+ describe('nako3server/index.nako3', () => {
99
+ let child = null
100
+ let port = 0
101
+ // extlibが無い環境ではサーバがダウンロードを試みるため、ダミーを置いてテストを独立させる
102
+ let createdExtlib = false
103
+
104
+ before(async () => {
105
+ if (!fs.existsSync(extlibMark)) {
106
+ fs.mkdirSync(path.dirname(extlibMark), { recursive: true })
107
+ fs.writeFileSync(extlibMark, '/* dummy for test */')
108
+ createdExtlib = true
109
+ }
110
+ port = await getFreePort()
111
+ child = spawnServer({ PORT: String(port) })
112
+ await waitForOutput(child, '### 超簡易Webサーバが起動しました', 60000)
113
+ })
114
+
115
+ after(async () => {
116
+ await killServer(child)
117
+ if (createdExtlib && fs.existsSync(extlibMark)) {
118
+ fs.unlinkSync(extlibMark)
119
+ }
120
+ })
121
+
122
+ it('ルート(/)にアクセスすると /demo/ へリダイレクトすること', async () => {
123
+ const res = await httpGet(port, '/')
124
+ assert.strictEqual(res.status, 302)
125
+ assert.strictEqual(res.headers.location, '/demo/')
126
+ })
127
+
128
+ it('フォルダを指定するとindex.htmlが返ること', async () => {
129
+ const res = await httpGet(port, '/demo/')
130
+ assert.strictEqual(res.status, 200)
131
+ assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
132
+ const expected = fs.readFileSync(path.join(rootDir, 'demo/index.html'))
133
+ assert.strictEqual(res.body.length, expected.length)
134
+ })
135
+
136
+ it('エイリアス(/css)がdemoフォルダへ割り当てられること', async () => {
137
+ const res = await httpGet(port, '/css/basic.css')
138
+ assert.strictEqual(res.status, 200)
139
+ assert.strictEqual(res.headers['content-type'], 'text/css')
140
+ const expected = fs.readFileSync(path.join(rootDir, 'demo/css/basic.css'))
141
+ assert.strictEqual(res.body.length, expected.length)
142
+ })
143
+
144
+ it('バイナリファイル(png)が壊れずに返ること', async () => {
145
+ const res = await httpGet(port, '/image/nako.png')
146
+ assert.strictEqual(res.status, 200)
147
+ assert.strictEqual(res.headers['content-type'], 'image/png')
148
+ const expected = fs.readFileSync(path.join(rootDir, 'demo/image/nako.png'))
149
+ assert.ok(res.body.equals(expected), 'PNGの内容が元ファイルと一致すること')
150
+ })
151
+
152
+ it('クエリパラメータ付きのURLでもファイルが返ること', async () => {
153
+ const res = await httpGet(port, '/demo/index.html?abc=123')
154
+ assert.strictEqual(res.status, 200)
155
+ assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
156
+ })
157
+
158
+ it('存在しないファイルは404を返すこと', async () => {
159
+ const res = await httpGet(port, '/no_such_file_12345.txt')
160
+ assert.strictEqual(res.status, 404)
161
+ assert.ok(res.body.toString('utf-8').indexOf('404') >= 0)
162
+ })
163
+
164
+ it('上位フォルダへの参照(..)でルート外のファイルが読めないこと', async () => {
165
+ // ルートフォルダの外へ確実に抜け出せるだけの「../」を並べて /etc/passwd を狙う。
166
+ // サニタイズが働いていなければ 200 で中身が漏れてしまう。
167
+ const escape = '/demo/' + '../'.repeat(20) + 'etc/passwd'
168
+ const res = await httpGet(port, escape)
169
+ assert.strictEqual(res.status, 404)
170
+ assert.ok(res.body.toString('utf-8').indexOf('root:') < 0, '/etc/passwd の内容が漏れていないこと')
171
+ })
172
+
173
+ it('URIから「..」が確実に取り除かれること', async () => {
174
+ // 「/de..mo/index.html」は「..」が除去されると「/demo/index.html」になる。
175
+ // これが200で返ることは、サニタイズが実際に動いている証拠になる。
176
+ const res = await httpGet(port, '/de..mo/index.html')
177
+ assert.strictEqual(res.status, 200)
178
+ assert.strictEqual(res.headers['content-type'], 'text/html; charset=utf-8')
179
+ })
180
+
181
+ it('未知の拡張子はtext/plainで返ること', async () => {
182
+ const res = await httpGet(port, '/package.json')
183
+ assert.strictEqual(res.status, 200)
184
+ assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8')
185
+ })
186
+ })
187
+
188
+ describe('nako3server/index.nako3 - ポート番号の指定', () => {
189
+ let createdExtlib = false
190
+
191
+ before(() => {
192
+ if (!fs.existsSync(extlibMark)) {
193
+ fs.mkdirSync(path.dirname(extlibMark), { recursive: true })
194
+ fs.writeFileSync(extlibMark, '/* dummy for test */')
195
+ createdExtlib = true
196
+ }
197
+ })
198
+
199
+ after(() => {
200
+ if (createdExtlib && fs.existsSync(extlibMark)) {
201
+ fs.unlinkSync(extlibMark)
202
+ }
203
+ })
204
+
205
+ it('コマンドライン引数でポート番号を指定できること', async () => {
206
+ const port = await getFreePort()
207
+ const child = spawn(process.execPath, [cnako3Path, serverNako3, String(port)], {
208
+ cwd: serverDir,
209
+ env: { ...process.env, NAKO3SERVER_OPEN: '0', PORT: '' }
210
+ })
211
+ try {
212
+ // 「[URL] ...」の行までを待つ(起動メッセージの次の行に出力される)
213
+ const out = await waitForOutput(child, '[URL] http://', 60000)
214
+ assert.ok(out.indexOf(`:${port}`) >= 0, `出力にポート番号${port}が含まれること 出力=${out}`)
215
+ const res = await httpGet(port, '/')
216
+ assert.strictEqual(res.status, 302)
217
+ } finally {
218
+ await killServer(child)
219
+ }
220
+ })
221
+
222
+ it('数値でないポート番号を指定するとエラーを表示して終了すること', async () => {
223
+ const child = spawnServer({ PORT: 'abc' })
224
+ let out = ''
225
+ child.stdout.on('data', (c) => { out += c.toString() })
226
+ child.stderr.on('data', (c) => { out += c.toString() })
227
+ await new Promise((resolve) => { child.on('exit', resolve) })
228
+ assert.ok(out.indexOf('[ERROR] 無効なポート番号です: abc') >= 0, `エラーメッセージが表示されること 出力=${out}`)
229
+ })
230
+
231
+ it('範囲外のポート番号を指定するとエラーを表示して終了すること', async () => {
232
+ const child = spawnServer({ PORT: '99999' })
233
+ let out = ''
234
+ child.stdout.on('data', (c) => { out += c.toString() })
235
+ child.stderr.on('data', (c) => { out += c.toString() })
236
+ await new Promise((resolve) => { child.on('exit', resolve) })
237
+ assert.ok(out.indexOf('[ERROR] 無効なポート番号です: 99999') >= 0, `エラーメッセージが表示されること 出力=${out}`)
238
+ })
239
+ })
@@ -0,0 +1,29 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ const testRoot = path.dirname(fileURLToPath(import.meta.url))
9
+ const projectRoot = path.resolve(testRoot, '../..')
10
+ const guidePath = path.join(projectRoot, 'doc/ai-code-generation.md')
11
+ const cnako3Path = path.join(projectRoot, 'src/cnako3.mjs')
12
+
13
+ describe('AIコード生成チュートリアル', () => {
14
+ it('掲載しているサンプルをcnako3で実行できる', () => {
15
+ const guide = fs.readFileSync(guidePath, 'utf8')
16
+ const match = guide.match(
17
+ /<!-- ai-code-generation-example:start -->\s*```nako3\n([\s\S]*?)\n```\s*<!-- ai-code-generation-example:end -->/
18
+ )
19
+
20
+ assert.ok(match, 'チュートリアルの実行サンプルが見つかりません')
21
+
22
+ const result = spawnSync(process.execPath, [cnako3Path, '-e', match[1]], {
23
+ encoding: 'utf8'
24
+ })
25
+
26
+ assert.strictEqual(result.status, 0, result.stderr)
27
+ assert.strictEqual(result.stdout.trim(), '合計は55です')
28
+ })
29
+ })
@@ -0,0 +1,45 @@
1
+ // Node.js v20 はEOLのためサポート対象外であることを確認するテスト (#2389)
2
+ import fs from 'node:fs'
3
+ import assert from 'node:assert'
4
+ import { test } from 'node:test'
5
+
6
+ const readText = (relPath) => fs.readFileSync(new URL(relPath, import.meta.url), 'utf-8')
7
+ const readJson = (relPath) => JSON.parse(readText(relPath))
8
+
9
+ test('package.json の engines が Node.js v22 以上を要求している', () => {
10
+ const pkg = readJson('../../package.json')
11
+ assert.equal(pkg.engines?.node, '>=22.0.0')
12
+ })
13
+
14
+ test('core/package.json の engines が Node.js v22 以上を要求している', () => {
15
+ const pkg = readJson('../../core/package.json')
16
+ assert.equal(pkg.engines?.node, '>=22.0.0')
17
+ })
18
+
19
+ test('browserslistの設定でNode.js v22未満を除外している', () => {
20
+ const pkg = readJson('../../package.json')
21
+ assert.ok(Array.isArray(pkg.browserslist), 'browserslistの設定がありません')
22
+ assert.ok(pkg.browserslist.includes('not node < 22'), 'browserslistに「not node < 22」がありません')
23
+ })
24
+
25
+ test('対応ブラウザ表にNode.js v20が含まれていない', () => {
26
+ const browsers = readText('../../src/browsers.mjs')
27
+ const matched = /"node":\[(.*?)\]/.exec(browsers)
28
+ assert.ok(matched, 'src/browsers.mjs にNode.jsの記載がありません')
29
+ const versions = matched[1].split(',').map((v) => parseInt(v.replace(/"/g, ''), 10))
30
+ assert.ok(versions.length > 0, 'Node.jsの対応バージョンが空です')
31
+ for (const ver of versions) {
32
+ assert.ok(ver >= 22, `EOLのNode.js v${ver} が対応表に含まれています`)
33
+ }
34
+ })
35
+
36
+ test('GitHub ActionsのCI対象にNode.js v20が含まれていない', () => {
37
+ const workflow = readText('../../.github/workflows/nodejs.yml')
38
+ const matched = /node-version:\s*\[(.*?)\]/.exec(workflow)
39
+ assert.ok(matched, 'node-versionの指定が見つかりません')
40
+ const versions = matched[1].split(',').map((v) => parseInt(v.trim(), 10))
41
+ assert.ok(versions.length > 0, 'node-versionの指定が空です')
42
+ for (const ver of versions) {
43
+ assert.ok(ver >= 22, `EOLのNode.js v${ver} がCI対象に含まれています`)
44
+ }
45
+ })