nadesiko3 3.7.19 → 3.7.21
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/batch/command.txt +115 -113
- package/core/package.json +2 -2
- package/core/src/README.md +98 -0
- package/core/src/nako_ast.mts +1 -0
- package/core/src/nako_core_version.mjs +2 -2
- package/core/src/nako_core_version.mts +2 -2
- package/core/src/nako_gen.mjs +54 -0
- package/core/src/nako_gen.mts +61 -0
- package/core/src/nako_parser3.mjs +45 -7
- package/core/src/nako_parser3.mts +43 -7
- package/core/test/array_test.mjs +16 -0
- package/core/test/func_call.mjs +11 -0
- package/core/test/func_scope.mjs +112 -0
- package/package.json +21 -27
- package/release/_hash.txt +36 -36
- package/release/_script-tags.txt +16 -16
- package/release/command.json +1 -1
- package/release/command.json.js +1 -1
- package/release/command_cnako3.json +1 -1
- package/release/command_list.json +1 -1
- package/release/edit_main.js +2 -2
- package/release/edit_main.js.map +3 -3
- package/release/editor.js +2 -2
- package/release/plugin_datetime.js +1 -1
- package/release/plugin_datetime.js.map +3 -3
- package/release/plugin_markup.js +28 -28
- package/release/plugin_markup.js.map +3 -3
- package/release/version.js +2 -2
- package/release/version_main.js +2 -2
- package/release/version_main.js.map +2 -2
- package/release/wnako3.js +85 -77
- package/release/wnako3.js.map +3 -3
- package/release/wnako3webworker.js +106 -98
- package/release/wnako3webworker.js.map +3 -3
- package/src/cnako3mod.mjs +1 -2
- package/src/cnako3mod.mts +1 -2
- package/src/nako_version.mjs +2 -2
- package/src/nako_version.mts +2 -2
- package/src/plugin_httpserver.mjs +184 -13
- package/src/plugin_httpserver.mts +176 -10
- package/src/plugin_node.mjs +0 -1
- package/src/plugin_node.mts +2 -3
- package/src/wnako3mod.mjs +3 -0
- package/src/wnako3mod.mts +3 -0
- package/test/common/wnako3mod_test.mjs +48 -0
- package/test/node/package_json_test.mjs +17 -0
- package/test/node/plugin_httpserver_test.mjs +239 -0
- package/tools/nako3edit/html/edit.html +21 -0
- package/tools/nako3edit/html/nako3edit.css +10 -0
- package/tools/nako3edit/index.mjs +15 -4
- package/tools/nako3server/index.mjs +15 -4
- package/tools/nako3server/index.nako3 +4 -2
package/src/cnako3mod.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import { exec } from 'node:child_process';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import url from 'node:url';
|
|
10
10
|
import fse from 'fs-extra';
|
|
11
|
-
import fetch from 'node-fetch';
|
|
12
11
|
import { NakoCompiler, newCompilerOptions } from '../core/src/nako3.mjs';
|
|
13
12
|
import { NakoImportError } from '../core/src/nako_errors.mjs';
|
|
14
13
|
import { NakoGenOptions } from '../core/src/nako_gen.mjs';
|
|
@@ -238,7 +237,7 @@ export class CNako3 extends NakoCompiler {
|
|
|
238
237
|
// or 以下のコピーだと依存ファイルがコピーされない package.jsonを見てコピーする必要がある
|
|
239
238
|
const orgModule = path.join(__dirname, '..', 'node_modules');
|
|
240
239
|
const dirNodeModules = path.join(path.dirname(opt.output), 'node_modules');
|
|
241
|
-
const modlist = ['fs-extra', 'iconv-lite', 'opener', '
|
|
240
|
+
const modlist = ['fs-extra', 'iconv-lite', 'opener', 'shell-quote'];
|
|
242
241
|
const copied = {};
|
|
243
242
|
// 再帰的に必要なモジュールをコピーする
|
|
244
243
|
const copyModule = (mod) => {
|
package/src/cnako3mod.mts
CHANGED
|
@@ -11,7 +11,6 @@ import path from 'node:path'
|
|
|
11
11
|
import url from 'node:url'
|
|
12
12
|
import fse from 'fs-extra'
|
|
13
13
|
|
|
14
|
-
import fetch from 'node-fetch'
|
|
15
14
|
import { NakoCompiler, LoaderTool, newCompilerOptions } from '../core/src/nako3.mjs'
|
|
16
15
|
import { NakoImportError } from '../core/src/nako_errors.mjs'
|
|
17
16
|
|
|
@@ -278,7 +277,7 @@ export class CNako3 extends NakoCompiler {
|
|
|
278
277
|
// or 以下のコピーだと依存ファイルがコピーされない package.jsonを見てコピーする必要がある
|
|
279
278
|
const orgModule = path.join(__dirname, '..', 'node_modules')
|
|
280
279
|
const dirNodeModules = path.join(path.dirname(opt.output), 'node_modules')
|
|
281
|
-
const modlist = ['fs-extra', 'iconv-lite', 'opener', '
|
|
280
|
+
const modlist = ['fs-extra', 'iconv-lite', 'opener', 'shell-quote']
|
|
282
281
|
const copied: { [key: string]: boolean } = {}
|
|
283
282
|
// 再帰的に必要なモジュールをコピーする
|
|
284
283
|
const copyModule = (mod: string) => {
|
package/src/nako_version.mjs
CHANGED
package/src/nako_version.mts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import http from 'http';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
5
6
|
// 定数
|
|
6
7
|
const HTTPSERVER_LOGID = '[簡易HTTPサーバ]';
|
|
7
8
|
const ERR_NOHTTPSERVER = '最初に『簡易HTTPサーバ起動時』を実行してサーバを起動する必要があります。';
|
|
9
|
+
const MAX_BODY_SIZE_POST = 10 * 1024 * 1024; // 10MB
|
|
8
10
|
class EasyURLItem {
|
|
9
11
|
constructor(action) {
|
|
10
12
|
this.action = action;
|
|
@@ -30,19 +32,86 @@ class EasyURLDispather {
|
|
|
30
32
|
const params = this.parseURL(req.url);
|
|
31
33
|
const url = params['?URL'];
|
|
32
34
|
this.sys.__setSysVar('GETデータ', params);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
isBreak =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
const runDispatcher = (postData) => {
|
|
36
|
+
this.sys.__setSysVar('POSTデータ', postData);
|
|
37
|
+
// URLの一致を調べてアクションを実行
|
|
38
|
+
const filtered = this.items.filter(v => url.startsWith(v.url)).sort((a, b) => { return b.url.length - a.url.length; });
|
|
39
|
+
for (const it of filtered) {
|
|
40
|
+
let isBreak = false;
|
|
41
|
+
if (it.action === 'static') {
|
|
42
|
+
isBreak = this.doRequestStatic(req, res, it);
|
|
43
|
+
}
|
|
44
|
+
else if (it.action === 'callback') {
|
|
45
|
+
isBreak = this.doRequestCallback(req, res, it);
|
|
46
|
+
}
|
|
47
|
+
if (isBreak) {
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
45
50
|
}
|
|
51
|
+
};
|
|
52
|
+
if (req.method === 'POST') {
|
|
53
|
+
let bodySize = 0;
|
|
54
|
+
const chunks = [];
|
|
55
|
+
req.on('data', (chunk) => {
|
|
56
|
+
bodySize += chunk.length;
|
|
57
|
+
if (bodySize > MAX_BODY_SIZE_POST) {
|
|
58
|
+
res.statusCode = 413;
|
|
59
|
+
res.end('Request entity too large.');
|
|
60
|
+
req.destroy();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
});
|
|
65
|
+
req.on('end', async () => {
|
|
66
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
67
|
+
let postData = {};
|
|
68
|
+
let filesData = [];
|
|
69
|
+
const contentType = req.headers['content-type'] || '';
|
|
70
|
+
try {
|
|
71
|
+
if (contentType.indexOf('multipart/form-data') >= 0) {
|
|
72
|
+
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
|
|
73
|
+
if (boundaryMatch) {
|
|
74
|
+
const boundary = (boundaryMatch[1] || boundaryMatch[2] || '').trim();
|
|
75
|
+
const parsed = await parseMultipart(bodyBuffer, boundary);
|
|
76
|
+
postData = parsed.fields;
|
|
77
|
+
filesData = parsed.files;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (contentType.indexOf('application/json') >= 0) {
|
|
81
|
+
const bodyStr = bodyBuffer.toString('utf-8');
|
|
82
|
+
try {
|
|
83
|
+
postData = JSON.parse(bodyStr);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
postData = bodyStr;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (contentType.indexOf('application/x-www-form-urlencoded') >= 0) {
|
|
90
|
+
const bodyStr = bodyBuffer.toString('utf-8');
|
|
91
|
+
const searchParams = new URLSearchParams(bodyStr);
|
|
92
|
+
const obj = {};
|
|
93
|
+
for (const [key, val] of searchParams.entries()) {
|
|
94
|
+
obj[key] = val;
|
|
95
|
+
}
|
|
96
|
+
postData = obj;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
postData = bodyBuffer.toString('utf-8');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error(`${HTTPSERVER_LOGID} アップロード保存エラー: ${err.message}`);
|
|
104
|
+
res.statusCode = 500;
|
|
105
|
+
res.end('Failed to save upload file.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.sys.__setSysVar('FILESデータ', filesData);
|
|
109
|
+
runDispatcher(postData);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.sys.__setSysVar('FILESデータ', []);
|
|
114
|
+
runDispatcher({});
|
|
46
115
|
}
|
|
47
116
|
}
|
|
48
117
|
return404(res) {
|
|
@@ -51,7 +120,9 @@ class EasyURLDispather {
|
|
|
51
120
|
res.end('<html><meta charset="utf-8"><body><h1>404 見当たりません。</h1></body></html>');
|
|
52
121
|
}
|
|
53
122
|
doRequestStatic(req, res, it) {
|
|
54
|
-
|
|
123
|
+
const params = this.parseURL(req.url);
|
|
124
|
+
const rawUrl = params['?URL'] || '';
|
|
125
|
+
let url = ('' + rawUrl).replace(/\.\./g, ''); // URLの..を許可しない
|
|
55
126
|
url = url.substring(it.url.length);
|
|
56
127
|
let fpath = path.join(it.path, url);
|
|
57
128
|
console.log('FILE=', fpath);
|
|
@@ -114,11 +185,106 @@ class EasyURLDispather {
|
|
|
114
185
|
return params;
|
|
115
186
|
}
|
|
116
187
|
}
|
|
188
|
+
async function parseMultipart(body, boundary) {
|
|
189
|
+
const fields = {};
|
|
190
|
+
const files = [];
|
|
191
|
+
const boundaryBuffer = Buffer.from('--' + boundary);
|
|
192
|
+
let pos = 0;
|
|
193
|
+
const parts = [];
|
|
194
|
+
while (true) {
|
|
195
|
+
const nextIdx = body.indexOf(boundaryBuffer, pos);
|
|
196
|
+
if (nextIdx === -1) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
if (pos > 0) {
|
|
200
|
+
let endPos = nextIdx;
|
|
201
|
+
if (body[nextIdx - 2] === 13 && body[nextIdx - 1] === 10) {
|
|
202
|
+
endPos -= 2;
|
|
203
|
+
}
|
|
204
|
+
else if (body[nextIdx - 1] === 10) {
|
|
205
|
+
endPos -= 1;
|
|
206
|
+
}
|
|
207
|
+
parts.push(body.subarray(pos, endPos));
|
|
208
|
+
}
|
|
209
|
+
pos = nextIdx + boundaryBuffer.length;
|
|
210
|
+
}
|
|
211
|
+
for (const part of parts) {
|
|
212
|
+
if (part.length === 0) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
let start = 0;
|
|
216
|
+
if (part[0] === 13 && part[1] === 10) {
|
|
217
|
+
start = 2;
|
|
218
|
+
}
|
|
219
|
+
else if (part[0] === 10) {
|
|
220
|
+
start = 1;
|
|
221
|
+
}
|
|
222
|
+
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'), start);
|
|
223
|
+
let bodyStart = 0;
|
|
224
|
+
let headerStr = '';
|
|
225
|
+
if (headerEnd !== -1) {
|
|
226
|
+
headerStr = part.toString('utf-8', start, headerEnd);
|
|
227
|
+
bodyStart = headerEnd + 4;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const headerEndLf = part.indexOf(Buffer.from('\n\n'), start);
|
|
231
|
+
if (headerEndLf !== -1) {
|
|
232
|
+
headerStr = part.toString('utf-8', start, headerEndLf);
|
|
233
|
+
bodyStart = headerEndLf + 2;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (bodyStart === 0) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const partBody = part.subarray(bodyStart);
|
|
240
|
+
const headers = {};
|
|
241
|
+
const lines = headerStr.split(/\r?\n/);
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
const idx = line.indexOf(':');
|
|
244
|
+
if (idx !== -1) {
|
|
245
|
+
const key = line.substring(0, idx).trim().toLowerCase();
|
|
246
|
+
const val = line.substring(idx + 1).trim();
|
|
247
|
+
headers[key] = val;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const contentDisposition = headers['content-disposition'] || '';
|
|
251
|
+
const nameMatch = contentDisposition.match(/name="([^"]+)"/);
|
|
252
|
+
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
|
|
253
|
+
if (nameMatch) {
|
|
254
|
+
const name = nameMatch[1];
|
|
255
|
+
if (filenameMatch) {
|
|
256
|
+
const filename = filenameMatch[1];
|
|
257
|
+
const safeFilename = path.basename(filename).replace(/[\\/]/g, '_');
|
|
258
|
+
const contentType = headers['content-type'] || 'application/octet-stream';
|
|
259
|
+
const uploadDir = path.join(os.tmpdir(), 'nako3-plugin_httpserver_upload');
|
|
260
|
+
if (!fs.existsSync(uploadDir)) {
|
|
261
|
+
await fs.promises.mkdir(uploadDir, { recursive: true });
|
|
262
|
+
}
|
|
263
|
+
const uniqueName = Date.now() + '_' + Math.random().toString(36).substring(2, 8) + '_' + safeFilename;
|
|
264
|
+
const filepath = path.join(uploadDir, uniqueName);
|
|
265
|
+
await fs.promises.writeFile(filepath, partBody);
|
|
266
|
+
files.push({
|
|
267
|
+
fieldName: name,
|
|
268
|
+
name: filename,
|
|
269
|
+
path: filepath,
|
|
270
|
+
size: partBody.length,
|
|
271
|
+
type: contentType
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
fields[name] = partBody.toString('utf-8');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { files, fields };
|
|
280
|
+
}
|
|
117
281
|
// MIMEタイプ
|
|
118
282
|
const MimeTypes = {
|
|
119
283
|
'.html': 'text/html',
|
|
120
284
|
'.css': 'text/css',
|
|
121
285
|
'.js': 'text/javascript',
|
|
286
|
+
'.mjs': 'text/javascript',
|
|
287
|
+
'.nako3': 'text/nadesiko3',
|
|
122
288
|
'.png': 'image/png',
|
|
123
289
|
'.gif': 'image/gif',
|
|
124
290
|
'.svg': 'svg+xml'
|
|
@@ -168,6 +334,8 @@ const PluginHttpServer = {
|
|
|
168
334
|
},
|
|
169
335
|
// @簡易HTTPサーバ
|
|
170
336
|
'GETデータ': { type: 'const', value: '' }, // @GETでーた
|
|
337
|
+
'POSTデータ': { type: 'const', value: '' }, // @POSTでーた
|
|
338
|
+
'FILESデータ': { type: 'const', value: '' }, // @FILESでーた
|
|
171
339
|
'簡易HTTPサーバ起動時': {
|
|
172
340
|
type: 'func',
|
|
173
341
|
josi: [['を'], ['の', 'で']],
|
|
@@ -179,6 +347,9 @@ const PluginHttpServer = {
|
|
|
179
347
|
dp.server = http.createServer((req, res) => {
|
|
180
348
|
dp.doRequest(req, res);
|
|
181
349
|
});
|
|
350
|
+
dp.server.on('error', (err) => {
|
|
351
|
+
console.error(`${HTTPSERVER_LOGID} エラー: ${err.message}`);
|
|
352
|
+
});
|
|
182
353
|
// サーバ起動
|
|
183
354
|
dp.server.listen(port, () => {
|
|
184
355
|
console.log(`${HTTPSERVER_LOGID} ポート番号(${port})で監視開始`);
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import http from 'http'
|
|
4
4
|
import path from 'path'
|
|
5
|
+
import os from 'os'
|
|
5
6
|
|
|
6
7
|
// 定数
|
|
7
8
|
const HTTPSERVER_LOGID = '[簡易HTTPサーバ]'
|
|
8
9
|
const ERR_NOHTTPSERVER = '最初に『簡易HTTPサーバ起動時』を実行してサーバを起動する必要があります。'
|
|
10
|
+
const MAX_BODY_SIZE_POST = 10 * 1024 * 1024 // 10MB
|
|
11
|
+
|
|
9
12
|
// オブジェクト
|
|
10
13
|
type EasyURLActionType = 'static' | 'callback'
|
|
11
14
|
type EasyURLCallback = (req: any, res: any) => void
|
|
@@ -49,16 +52,79 @@ class EasyURLDispather {
|
|
|
49
52
|
const params = this.parseURL(req.url)
|
|
50
53
|
const url = params['?URL']
|
|
51
54
|
this.sys.__setSysVar('GETデータ', params)
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
|
|
56
|
+
const runDispatcher = (postData: any) => {
|
|
57
|
+
this.sys.__setSysVar('POSTデータ', postData)
|
|
58
|
+
// URLの一致を調べてアクションを実行
|
|
59
|
+
const filtered = this.items.filter(v => url.startsWith(v.url)).sort((a, b) => { return b.url.length - a.url.length })
|
|
60
|
+
for (const it of filtered) {
|
|
61
|
+
let isBreak = false
|
|
62
|
+
if (it.action === 'static') {
|
|
63
|
+
isBreak = this.doRequestStatic(req, res, it)
|
|
64
|
+
} else if (it.action === 'callback') {
|
|
65
|
+
isBreak = this.doRequestCallback(req, res, it)
|
|
66
|
+
}
|
|
67
|
+
if (isBreak) { break }
|
|
60
68
|
}
|
|
61
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (req.method === 'POST') {
|
|
72
|
+
let bodySize = 0
|
|
73
|
+
const chunks: Buffer[] = []
|
|
74
|
+
req.on('data', (chunk: Buffer) => {
|
|
75
|
+
bodySize += chunk.length
|
|
76
|
+
if (bodySize > MAX_BODY_SIZE_POST) {
|
|
77
|
+
res.statusCode = 413
|
|
78
|
+
res.end('Request entity too large.')
|
|
79
|
+
req.destroy()
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
chunks.push(chunk)
|
|
83
|
+
})
|
|
84
|
+
req.on('end', async() => {
|
|
85
|
+
const bodyBuffer = Buffer.concat(chunks)
|
|
86
|
+
let postData: any = {}
|
|
87
|
+
let filesData: any[] = []
|
|
88
|
+
const contentType = req.headers['content-type'] || ''
|
|
89
|
+
try {
|
|
90
|
+
if (contentType.indexOf('multipart/form-data') >= 0) {
|
|
91
|
+
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/)
|
|
92
|
+
if (boundaryMatch) {
|
|
93
|
+
const boundary = (boundaryMatch[1] || boundaryMatch[2] || '').trim()
|
|
94
|
+
const parsed = await parseMultipart(bodyBuffer, boundary)
|
|
95
|
+
postData = parsed.fields
|
|
96
|
+
filesData = parsed.files
|
|
97
|
+
}
|
|
98
|
+
} else if (contentType.indexOf('application/json') >= 0) {
|
|
99
|
+
const bodyStr = bodyBuffer.toString('utf-8')
|
|
100
|
+
try {
|
|
101
|
+
postData = JSON.parse(bodyStr)
|
|
102
|
+
} catch (e) {
|
|
103
|
+
postData = bodyStr
|
|
104
|
+
}
|
|
105
|
+
} else if (contentType.indexOf('application/x-www-form-urlencoded') >= 0) {
|
|
106
|
+
const bodyStr = bodyBuffer.toString('utf-8')
|
|
107
|
+
const searchParams = new URLSearchParams(bodyStr)
|
|
108
|
+
const obj: any = {}
|
|
109
|
+
for (const [key, val] of searchParams.entries()) {
|
|
110
|
+
obj[key] = val
|
|
111
|
+
}
|
|
112
|
+
postData = obj
|
|
113
|
+
} else {
|
|
114
|
+
postData = bodyBuffer.toString('utf-8')
|
|
115
|
+
}
|
|
116
|
+
} catch (err: any) {
|
|
117
|
+
console.error(`${HTTPSERVER_LOGID} アップロード保存エラー: ${err.message}`)
|
|
118
|
+
res.statusCode = 500
|
|
119
|
+
res.end('Failed to save upload file.')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
this.sys.__setSysVar('FILESデータ', filesData)
|
|
123
|
+
runDispatcher(postData)
|
|
124
|
+
})
|
|
125
|
+
} else {
|
|
126
|
+
this.sys.__setSysVar('FILESデータ', [])
|
|
127
|
+
runDispatcher({})
|
|
62
128
|
}
|
|
63
129
|
}
|
|
64
130
|
|
|
@@ -69,7 +135,9 @@ class EasyURLDispather {
|
|
|
69
135
|
}
|
|
70
136
|
|
|
71
137
|
doRequestStatic(req: any, res: any, it: EasyURLItem): boolean {
|
|
72
|
-
|
|
138
|
+
const params = this.parseURL(req.url)
|
|
139
|
+
const rawUrl = params['?URL'] || ''
|
|
140
|
+
let url: string = ('' + rawUrl).replace(/\.\./g, '') // URLの..を許可しない
|
|
73
141
|
url = url.substring(it.url.length)
|
|
74
142
|
let fpath = path.join(it.path, url)
|
|
75
143
|
console.log('FILE=', fpath)
|
|
@@ -134,11 +202,104 @@ class EasyURLDispather {
|
|
|
134
202
|
return params
|
|
135
203
|
}
|
|
136
204
|
}
|
|
205
|
+
async function parseMultipart(body: Buffer, boundary: string): Promise<{ files: any[], fields: any }> {
|
|
206
|
+
const fields: any = {}
|
|
207
|
+
const files: any[] = []
|
|
208
|
+
|
|
209
|
+
const boundaryBuffer = Buffer.from('--' + boundary)
|
|
210
|
+
let pos = 0
|
|
211
|
+
const parts: Buffer[] = []
|
|
212
|
+
|
|
213
|
+
while (true) {
|
|
214
|
+
const nextIdx = body.indexOf(boundaryBuffer, pos)
|
|
215
|
+
if (nextIdx === -1) { break }
|
|
216
|
+
if (pos > 0) {
|
|
217
|
+
let endPos = nextIdx
|
|
218
|
+
if (body[nextIdx - 2] === 13 && body[nextIdx - 1] === 10) {
|
|
219
|
+
endPos -= 2
|
|
220
|
+
} else if (body[nextIdx - 1] === 10) {
|
|
221
|
+
endPos -= 1
|
|
222
|
+
}
|
|
223
|
+
parts.push(body.subarray(pos, endPos))
|
|
224
|
+
}
|
|
225
|
+
pos = nextIdx + boundaryBuffer.length
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const part of parts) {
|
|
229
|
+
if (part.length === 0) { continue }
|
|
230
|
+
let start = 0
|
|
231
|
+
if (part[0] === 13 && part[1] === 10) { start = 2 }
|
|
232
|
+
else if (part[0] === 10) { start = 1 }
|
|
233
|
+
|
|
234
|
+
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'), start)
|
|
235
|
+
let bodyStart = 0
|
|
236
|
+
let headerStr = ''
|
|
237
|
+
if (headerEnd !== -1) {
|
|
238
|
+
headerStr = part.toString('utf-8', start, headerEnd)
|
|
239
|
+
bodyStart = headerEnd + 4
|
|
240
|
+
} else {
|
|
241
|
+
const headerEndLf = part.indexOf(Buffer.from('\n\n'), start)
|
|
242
|
+
if (headerEndLf !== -1) {
|
|
243
|
+
headerStr = part.toString('utf-8', start, headerEndLf)
|
|
244
|
+
bodyStart = headerEndLf + 2
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (bodyStart === 0) { continue }
|
|
249
|
+
|
|
250
|
+
const partBody = part.subarray(bodyStart)
|
|
251
|
+
const headers: any = {}
|
|
252
|
+
const lines = headerStr.split(/\r?\n/)
|
|
253
|
+
for (const line of lines) {
|
|
254
|
+
const idx = line.indexOf(':')
|
|
255
|
+
if (idx !== -1) {
|
|
256
|
+
const key = line.substring(0, idx).trim().toLowerCase()
|
|
257
|
+
const val = line.substring(idx + 1).trim()
|
|
258
|
+
headers[key] = val
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const contentDisposition = headers['content-disposition'] || ''
|
|
263
|
+
const nameMatch = contentDisposition.match(/name="([^"]+)"/)
|
|
264
|
+
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/)
|
|
265
|
+
|
|
266
|
+
if (nameMatch) {
|
|
267
|
+
const name = nameMatch[1]
|
|
268
|
+
if (filenameMatch) {
|
|
269
|
+
const filename = filenameMatch[1]
|
|
270
|
+
const safeFilename = path.basename(filename).replace(/[\\/]/g, '_')
|
|
271
|
+
const contentType = headers['content-type'] || 'application/octet-stream'
|
|
272
|
+
|
|
273
|
+
const uploadDir = path.join(os.tmpdir(), 'nako3-plugin_httpserver_upload')
|
|
274
|
+
if (!fs.existsSync(uploadDir)) {
|
|
275
|
+
await fs.promises.mkdir(uploadDir, { recursive: true })
|
|
276
|
+
}
|
|
277
|
+
const uniqueName = Date.now() + '_' + Math.random().toString(36).substring(2, 8) + '_' + safeFilename
|
|
278
|
+
const filepath = path.join(uploadDir, uniqueName)
|
|
279
|
+
await fs.promises.writeFile(filepath, partBody)
|
|
280
|
+
|
|
281
|
+
files.push({
|
|
282
|
+
fieldName: name,
|
|
283
|
+
name: filename,
|
|
284
|
+
path: filepath,
|
|
285
|
+
size: partBody.length,
|
|
286
|
+
type: contentType
|
|
287
|
+
})
|
|
288
|
+
} else {
|
|
289
|
+
fields[name] = partBody.toString('utf-8')
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { files, fields }
|
|
295
|
+
}
|
|
137
296
|
// MIMEタイプ
|
|
138
297
|
const MimeTypes: any = {
|
|
139
298
|
'.html': 'text/html',
|
|
140
299
|
'.css': 'text/css',
|
|
141
300
|
'.js': 'text/javascript',
|
|
301
|
+
'.mjs': 'text/javascript',
|
|
302
|
+
'.nako3': 'text/nadesiko3',
|
|
142
303
|
'.png': 'image/png',
|
|
143
304
|
'.gif': 'image/gif',
|
|
144
305
|
'.svg': 'svg+xml'
|
|
@@ -184,6 +345,8 @@ const PluginHttpServer = {
|
|
|
184
345
|
},
|
|
185
346
|
// @簡易HTTPサーバ
|
|
186
347
|
'GETデータ': { type: 'const', value: '' }, // @GETでーた
|
|
348
|
+
'POSTデータ': { type: 'const', value: '' }, // @POSTでーた
|
|
349
|
+
'FILESデータ': { type: 'const', value: '' }, // @FILESでーた
|
|
187
350
|
'簡易HTTPサーバ起動時': { // @ポート番号PORTを指定して簡易HTTPサーバを起動して、CALLBACKを実行する。 // @かんいHTTPさーばきどうしたとき
|
|
188
351
|
type: 'func',
|
|
189
352
|
josi: [['を'], ['の', 'で']],
|
|
@@ -195,6 +358,9 @@ const PluginHttpServer = {
|
|
|
195
358
|
dp.server = http.createServer((req: any, res: any) => {
|
|
196
359
|
dp.doRequest(req, res)
|
|
197
360
|
})
|
|
361
|
+
dp.server.on('error', (err: any) => {
|
|
362
|
+
console.error(`${HTTPSERVER_LOGID} エラー: ${err.message}`)
|
|
363
|
+
})
|
|
198
364
|
// サーバ起動
|
|
199
365
|
dp.server.listen(port, () => {
|
|
200
366
|
console.log(`${HTTPSERVER_LOGID} ポート番号(${port})で監視開始`)
|
package/src/plugin_node.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import url from 'node:url';
|
|
|
13
13
|
import opener from 'opener';
|
|
14
14
|
import iconv from 'iconv-lite';
|
|
15
15
|
import shellQuote from 'shell-quote';
|
|
16
|
-
import fetch, { FormData, Blob } from 'node-fetch';
|
|
17
16
|
import fse from 'fs-extra';
|
|
18
17
|
import { getEnv, isWindows, getCommandLineArgs } from './deno_wrapper.mjs';
|
|
19
18
|
const __filename = url.fileURLToPath(import.meta.url);
|
package/src/plugin_node.mts
CHANGED
|
@@ -14,7 +14,6 @@ import url from 'node:url'
|
|
|
14
14
|
import opener from 'opener'
|
|
15
15
|
import iconv from 'iconv-lite'
|
|
16
16
|
import shellQuote from 'shell-quote'
|
|
17
|
-
import fetch, { FormData, Blob } from 'node-fetch'
|
|
18
17
|
import fse from 'fs-extra'
|
|
19
18
|
import { NakoSystem } from '../core/src/plugin_api.mjs'
|
|
20
19
|
|
|
@@ -699,7 +698,7 @@ export default {
|
|
|
699
698
|
},
|
|
700
699
|
return_none: true
|
|
701
700
|
},
|
|
702
|
-
'ファイル上書コピー': { // @パスAをパスBへファイルコピーする(
|
|
701
|
+
'ファイル上書コピー': { // @パスAをパスBへファイルコピーする(コピー先に内容をマージしてコピー) // @ふぁいるうわがきこぴー
|
|
703
702
|
type: 'func',
|
|
704
703
|
josi: [['から', 'を'], ['に', 'へ']],
|
|
705
704
|
pure: true,
|
|
@@ -740,7 +739,7 @@ export default {
|
|
|
740
739
|
},
|
|
741
740
|
return_none: true
|
|
742
741
|
},
|
|
743
|
-
'ファイル上書移動': { // @パスAをパスBへ移動する(
|
|
742
|
+
'ファイル上書移動': { // @パスAをパスBへ移動する(移動先に内容をマージしてコピー後、元を削除) // @ふぁいるうわがきいどう
|
|
744
743
|
type: 'func',
|
|
745
744
|
josi: [['から', 'を'], ['に', 'へ']],
|
|
746
745
|
pure: true,
|
package/src/wnako3mod.mjs
CHANGED
|
@@ -236,6 +236,9 @@ function dirname(s) {
|
|
|
236
236
|
}
|
|
237
237
|
function resolveURL(base, s) {
|
|
238
238
|
const baseA = base.split('/');
|
|
239
|
+
if (baseA.length > 0 && baseA[baseA.length - 1] === '') {
|
|
240
|
+
baseA.pop();
|
|
241
|
+
}
|
|
239
242
|
const sA = s.split('/');
|
|
240
243
|
for (const p of sA) {
|
|
241
244
|
if (p === '') {
|
package/src/wnako3mod.mts
CHANGED
|
@@ -240,6 +240,9 @@ function dirname(s: string) {
|
|
|
240
240
|
|
|
241
241
|
function resolveURL(base: string, s: string) {
|
|
242
242
|
const baseA = base.split('/')
|
|
243
|
+
if (baseA.length > 0 && baseA[baseA.length - 1] === '') {
|
|
244
|
+
baseA.pop()
|
|
245
|
+
}
|
|
243
246
|
const sA = s.split('/')
|
|
244
247
|
for (const p of sA) {
|
|
245
248
|
if (p === '') { continue }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import { WebNakoCompiler } from '../../src/wnako3mod.mjs'
|
|
3
|
+
|
|
4
|
+
describe('wnako3mod_test', () => {
|
|
5
|
+
let originalWindow
|
|
6
|
+
|
|
7
|
+
before(() => {
|
|
8
|
+
// window のモック
|
|
9
|
+
originalWindow = globalThis.window
|
|
10
|
+
globalThis.window = {
|
|
11
|
+
location: {
|
|
12
|
+
href: 'http://localhost:8888/index.html'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
after(() => {
|
|
18
|
+
globalThis.window = originalWindow
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('resolvePath で baseDir が / のときの // スタートの不具合が修正されていること', () => {
|
|
22
|
+
const compiler = new WebNakoCompiler()
|
|
23
|
+
const token = { file: 'main', line: 1 }
|
|
24
|
+
|
|
25
|
+
// http://localhost:8888/index.html から hoge.nako3 を取り込む想定
|
|
26
|
+
const result = compiler.resolvePath('hoge.nako3', token, '')
|
|
27
|
+
// 結果が /hoge.nako3 になるべき(//hoge.nako3 ではなく)
|
|
28
|
+
assert.strictEqual(result.filePath, '/hoge.nako3')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('resolvePath で baseDir が /dir/ のとき', () => {
|
|
32
|
+
const compiler = new WebNakoCompiler()
|
|
33
|
+
const token = { file: 'main', line: 1 }
|
|
34
|
+
|
|
35
|
+
globalThis.window.location.href = 'http://localhost:8888/dir/index.html'
|
|
36
|
+
const result = compiler.resolvePath('hoge.nako3', token, '')
|
|
37
|
+
assert.strictEqual(result.filePath, '/dir/hoge.nako3')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('resolvePath で baseDir が /dir/subdir/ のとき', () => {
|
|
41
|
+
const compiler = new WebNakoCompiler()
|
|
42
|
+
const token = { file: 'main', line: 1 }
|
|
43
|
+
|
|
44
|
+
globalThis.window.location.href = 'http://localhost:8888/dir/subdir/index.html'
|
|
45
|
+
const result = compiler.resolvePath('hoge.nako3', token, '')
|
|
46
|
+
assert.strictEqual(result.filePath, '/dir/subdir/hoge.nako3')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { test } from 'node:test'
|
|
4
|
+
|
|
5
|
+
test('package.json に node-fetch の依存がない', () => {
|
|
6
|
+
const pkgUrl = new URL('../../package.json', import.meta.url)
|
|
7
|
+
const pkg = JSON.parse(fs.readFileSync(pkgUrl, 'utf-8'))
|
|
8
|
+
const dependencySections = [
|
|
9
|
+
'dependencies',
|
|
10
|
+
'devDependencies',
|
|
11
|
+
'optionalDependencies',
|
|
12
|
+
'peerDependencies'
|
|
13
|
+
]
|
|
14
|
+
for (const section of dependencySections) {
|
|
15
|
+
assert.equal(pkg[section]?.['node-fetch'], undefined, `${section} に node-fetch が含まれています`)
|
|
16
|
+
}
|
|
17
|
+
})
|