asljs-server 0.2.0 → 0.2.1
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 +4 -3
- package/cli.js +1 -1
- package/dist/server.js +104 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,11 @@ Or without installing:
|
|
|
26
26
|
- `asljs-server --port 8080`
|
|
27
27
|
- `asljs-server --host 0.0.0.0 --port 8080`
|
|
28
28
|
|
|
29
|
-
##
|
|
29
|
+
## File API
|
|
30
30
|
|
|
31
|
-
- `GET /api/
|
|
32
|
-
- `PUT|POST /api/
|
|
31
|
+
- `GET /api/file?path=path` returns the file contents
|
|
32
|
+
- `PUT|POST /api/file?path=path` writes the file
|
|
33
|
+
- `GET /api/files?path=path` lists all files in the directory
|
|
33
34
|
|
|
34
35
|
## Live reload
|
|
35
36
|
|
package/cli.js
CHANGED
package/dist/server.js
CHANGED
|
@@ -20,6 +20,36 @@ const safeJsonName = (name) => typeof name === 'string'
|
|
|
20
20
|
&& /^[a-zA-Z0-9._/-]+$/.test(name)
|
|
21
21
|
? name
|
|
22
22
|
: null;
|
|
23
|
+
const safeRelativeApiPath = (value, { allowEmpty = false } = {}) => {
|
|
24
|
+
if (value === null
|
|
25
|
+
|| value === undefined) {
|
|
26
|
+
return allowEmpty
|
|
27
|
+
? ''
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
if (value === '')
|
|
33
|
+
return allowEmpty
|
|
34
|
+
? ''
|
|
35
|
+
: null;
|
|
36
|
+
if (!/^[a-zA-Z0-9._/-]+$/.test(value))
|
|
37
|
+
return null;
|
|
38
|
+
// Force URL-style separators.
|
|
39
|
+
if (value.includes('\\'))
|
|
40
|
+
return null;
|
|
41
|
+
if (value.startsWith('/'))
|
|
42
|
+
return null;
|
|
43
|
+
const parts = value.split('/');
|
|
44
|
+
for (const part of parts) {
|
|
45
|
+
if (!part
|
|
46
|
+
|| part === '.'
|
|
47
|
+
|| part === '..') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
};
|
|
23
53
|
function readBody(request, limit) {
|
|
24
54
|
return new Promise((resolve, reject) => {
|
|
25
55
|
let size = 0;
|
|
@@ -76,50 +106,47 @@ export function startServer({ root = '.', port = 3000, host = 'localhost', logge
|
|
|
76
106
|
}, 50);
|
|
77
107
|
};
|
|
78
108
|
watchStaticTree(FILES_DIR, () => broadcastReload());
|
|
79
|
-
// ----------
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
if (!base)
|
|
83
|
-
return null;
|
|
84
|
-
const file = base.endsWith('.json')
|
|
85
|
-
? base
|
|
86
|
-
: (base + '.json');
|
|
87
|
-
const full = path.join(FILES_DIR, file);
|
|
109
|
+
// ---------- File API helpers
|
|
110
|
+
const fileApiPath = (relativePath) => {
|
|
111
|
+
const full = path.join(FILES_DIR, relativePath);
|
|
88
112
|
return isPathInside(full, FILES_DIR)
|
|
89
113
|
? full
|
|
90
114
|
: null;
|
|
91
115
|
};
|
|
92
|
-
async function
|
|
116
|
+
async function handleFileApi(request, response, url) {
|
|
93
117
|
await fsp.mkdir(FILES_DIR, { recursive: true });
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
const relativePath = safeRelativeApiPath(url.searchParams.get('path'));
|
|
119
|
+
if (!relativePath) {
|
|
120
|
+
return send.badRequest(response, 'Invalid "path". Use a relative path with URL-style separators.');
|
|
121
|
+
}
|
|
122
|
+
const filePath = fileApiPath(relativePath);
|
|
123
|
+
if (!filePath) {
|
|
124
|
+
return send.forbidden(response);
|
|
98
125
|
}
|
|
99
126
|
if (request.method === 'GET') {
|
|
100
|
-
|
|
101
|
-
return send.json(response, 200, await fsp.readFile(file, 'utf8'));
|
|
102
|
-
}
|
|
103
|
-
catch (e) {
|
|
104
|
-
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT')
|
|
105
|
-
return send.notFound(response, 'JSON file not found');
|
|
106
|
-
throw e;
|
|
107
|
-
}
|
|
127
|
+
return send.file(response, filePath);
|
|
108
128
|
}
|
|
109
129
|
if (request.method === 'PUT'
|
|
110
130
|
|| request.method === 'POST') {
|
|
111
131
|
try {
|
|
112
132
|
// 1MB limit
|
|
113
133
|
const body = await readBody(request, 1000000);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
135
|
+
// Preserve the old behavior for JSON files.
|
|
136
|
+
if (path.extname(filePath).toLowerCase() === '.json') {
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(body);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return send.badRequest(response, 'Invalid JSON');
|
|
143
|
+
}
|
|
144
|
+
await fsp.writeFile(filePath, JSON.stringify(parsed, null, 2) + '\n', 'utf8');
|
|
117
145
|
}
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
else {
|
|
147
|
+
await fsp.writeFile(filePath, body, 'utf8');
|
|
120
148
|
}
|
|
121
|
-
|
|
122
|
-
return send.api(response, { file: path.basename(file) });
|
|
149
|
+
return send.api(response, { path: relativePath });
|
|
123
150
|
}
|
|
124
151
|
catch (error) {
|
|
125
152
|
return send.apiError(response, error);
|
|
@@ -127,15 +154,60 @@ export function startServer({ root = '.', port = 3000, host = 'localhost', logge
|
|
|
127
154
|
}
|
|
128
155
|
return send.methodNotAllowed(response, 'GET, PUT, POST');
|
|
129
156
|
}
|
|
157
|
+
async function handleFilesApi(request, response, url) {
|
|
158
|
+
if (request.method !== 'GET') {
|
|
159
|
+
return send.methodNotAllowed(response, 'GET');
|
|
160
|
+
}
|
|
161
|
+
await fsp.mkdir(FILES_DIR, { recursive: true });
|
|
162
|
+
const relativePath = safeRelativeApiPath(url.searchParams.get('path'), { allowEmpty: true });
|
|
163
|
+
if (relativePath === null) {
|
|
164
|
+
return send.badRequest(response, 'Invalid "path". Use a relative directory path.');
|
|
165
|
+
}
|
|
166
|
+
const directoryPath = fileApiPath(relativePath);
|
|
167
|
+
if (!directoryPath) {
|
|
168
|
+
return send.forbidden(response);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const stat = await fsp.stat(directoryPath);
|
|
172
|
+
if (!stat.isDirectory()) {
|
|
173
|
+
return send.badRequest(response, '"path" must point to a directory');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error
|
|
178
|
+
&& typeof error === 'object'
|
|
179
|
+
&& 'code' in error
|
|
180
|
+
&& error.code === 'ENOENT') {
|
|
181
|
+
return send.notFound(response, 'Directory not found');
|
|
182
|
+
}
|
|
183
|
+
return send.error(response, error);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const entries = await fsp.readdir(directoryPath, { withFileTypes: true });
|
|
187
|
+
const files = entries
|
|
188
|
+
.filter(e => e.isFile())
|
|
189
|
+
.map(e => e.name)
|
|
190
|
+
.sort((a, b) => a.localeCompare(b));
|
|
191
|
+
return send.api(response, { path: relativePath,
|
|
192
|
+
files });
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
return send.apiError(response, error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
130
198
|
async function serveStatic(request, response, url) {
|
|
131
199
|
const pathname = decodeURIComponent(url.pathname);
|
|
132
200
|
// SSE endpoint
|
|
133
201
|
if (pathname === '/__events') {
|
|
134
202
|
return serveSSE(request, response);
|
|
135
203
|
}
|
|
136
|
-
//
|
|
137
|
-
if (pathname === '/api/
|
|
138
|
-
return
|
|
204
|
+
// File API: /api/file?path=relative/path.ext
|
|
205
|
+
if (pathname === '/api/file') {
|
|
206
|
+
return handleFileApi(request, response, url);
|
|
207
|
+
}
|
|
208
|
+
// Directory listing API: /api/files?path=relative/dir
|
|
209
|
+
if (pathname === '/api/files') {
|
|
210
|
+
return handleFilesApi(request, response, url);
|
|
139
211
|
}
|
|
140
212
|
// Resolve file path
|
|
141
213
|
let filePath = path.normalize(path.join(FILES_DIR, pathname));
|
package/package.json
CHANGED