@entrinsik/vite-plugin-informer 2.6.0-beta.2 → 2.6.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/bin/publish.js +11 -0
- package/package.json +1 -1
- package/src/assemble.js +4 -1
- package/src/openapi-local.js +283 -0
- package/src/publish.js +8 -4
- package/src/server-routes.js +18 -4
package/bin/publish.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFile, access, readdir } from 'node:fs/promises';
|
|
|
4
4
|
import { resolve, join } from 'node:path';
|
|
5
5
|
import { collectAppFiles } from '../src/assemble.js';
|
|
6
6
|
import { extractNotes } from '../src/changelog.js';
|
|
7
|
+
import { buildLocalOpenApi } from '../src/openapi-local.js';
|
|
7
8
|
import { publish } from '../src/publish.js';
|
|
8
9
|
import { loadEnv, parseModeArg } from '../src/env.js';
|
|
9
10
|
|
|
@@ -63,6 +64,14 @@ try {
|
|
|
63
64
|
process.exit(1);
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// Freeze the API contract with this version: build the OpenAPI doc from the
|
|
68
|
+
// local handlers (see openapi-local.js) and ship it in the archive as root
|
|
69
|
+
// openapi.json — the License Manager extracts it (like informer.yaml), stores
|
|
70
|
+
// it per version, and diffs the public surface against the previous version.
|
|
71
|
+
const { spec: apiSpec, warnings: apiWarnings } = await buildLocalOpenApi({ projectRoot, name, slug, version });
|
|
72
|
+
for (const w of apiWarnings) console.warn(` warning: ${w}`);
|
|
73
|
+
if (apiSpec) files.push({ rel: 'openapi.json', content: JSON.stringify(apiSpec, null, 2) });
|
|
74
|
+
|
|
66
75
|
const icon = await resolveIcon(projectRoot, inf.icon);
|
|
67
76
|
const screenshots = await resolveScreenshots(projectRoot, inf.screenshots);
|
|
68
77
|
const channel = version.includes('-') ? 'beta' : 'stable';
|
|
@@ -93,6 +102,8 @@ try {
|
|
|
93
102
|
}
|
|
94
103
|
});
|
|
95
104
|
console.log(`Published ${slug} v${version} (${result.channel || channel}).`);
|
|
105
|
+
// Server-side advisories (e.g. "public API changed without a major bump").
|
|
106
|
+
for (const w of result.warnings || []) console.warn(` warning: ${w}`);
|
|
96
107
|
} catch (err) {
|
|
97
108
|
console.error(err.message);
|
|
98
109
|
process.exit(1);
|
package/package.json
CHANGED
package/src/assemble.js
CHANGED
|
@@ -14,7 +14,10 @@ import { join, relative, posix } from 'node:path';
|
|
|
14
14
|
* (index.html, assets/…), while the source trees keep their directory prefix
|
|
15
15
|
* (server/…, tools/…) — matching how an app's library is structured in Informer.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
// API.md is the author-written integration guide: deployed to the library root
|
|
18
|
+
// (the live openapi.json route folds it into info.description) and tarred into
|
|
19
|
+
// the published archive for the marketplace listing's Integrate surface.
|
|
20
|
+
const ROOT_CONFIG_FILES = ['informer.yaml', 'data-access.yaml', 'API.md'];
|
|
18
21
|
const SOURCE_DIRS = ['migrations', 'tools', 'mcp', 'server', 'webhooks', 'lib', 'shared'];
|
|
19
22
|
|
|
20
23
|
// Entries never worth shipping in an app's server-side library: OS/editor
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { filePathToRoute, walkJsFiles } from './server-routes.js';
|
|
5
|
+
|
|
6
|
+
// Build the app's OpenAPI 3.0.3 document from the LOCAL project at publish
|
|
7
|
+
// time — the frozen, per-version contract the marketplace stores alongside the
|
|
8
|
+
// changelog. CI often has no access to the Informer host an app was developed
|
|
9
|
+
// against, so this cannot fetch the live /apps/{id}/openapi.json; instead the
|
|
10
|
+
// handlers are import()ed (this runs in the author's own project, where
|
|
11
|
+
// executing their code is exactly what vite does anyway) and their real
|
|
12
|
+
// exports — methods, config, schema, description — are read directly.
|
|
13
|
+
//
|
|
14
|
+
// The document-shaping half below is a deliberate port of
|
|
15
|
+
// informer-server/modules/app/lib/openapi-emitter.js (the deploy-time emitter):
|
|
16
|
+
// the frozen doc must match what a live instance serves for the same source.
|
|
17
|
+
// Keep the two in sync when either changes. This package publishes to npm on
|
|
18
|
+
// its own, so importing across the monorepo is not an option.
|
|
19
|
+
|
|
20
|
+
const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
21
|
+
const HTTP_METHOD_KEYS = new Set(VALID_METHODS);
|
|
22
|
+
|
|
23
|
+
// Fallback when a handler cannot be import()ed (a missing optional dep, a
|
|
24
|
+
// top-level env access that only resolves in dev): the file still contributes
|
|
25
|
+
// skeleton routes (method + path) so the frozen doc doesn't silently lose them.
|
|
26
|
+
const METHOD_EXPORT_RE = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\b|export\s+(?:const|let|var)\s+(GET|POST|PUT|PATCH|DELETE)\b/g;
|
|
27
|
+
|
|
28
|
+
function routePathToOpenApi(path) {
|
|
29
|
+
return path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function pathParamNames(path) {
|
|
33
|
+
const names = new Set();
|
|
34
|
+
const re = /:([A-Za-z0-9_]+)/g;
|
|
35
|
+
let m;
|
|
36
|
+
while ((m = re.exec(path)) !== null) names.add(m[1]);
|
|
37
|
+
return [...names];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function operationId(method, path) {
|
|
41
|
+
const slug = path.split('/').filter(Boolean)
|
|
42
|
+
.map(seg => (seg.startsWith(':') ? `by_${seg.slice(1)}` : seg))
|
|
43
|
+
.join('_') || 'root';
|
|
44
|
+
return `${method.toLowerCase()}_${slug}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// OpenAPI 3.0.3 Schema Object keywords, by required value type (port of the
|
|
48
|
+
// server emitter's sanitizeSchema — see file header).
|
|
49
|
+
const SCHEMA_STRING_KEYS = new Set(['type', 'format', 'title', 'description', 'pattern']);
|
|
50
|
+
const SCHEMA_NUMBER_KEYS = new Set(['multipleOf', 'maximum', 'minimum', 'maxLength', 'minLength', 'maxItems', 'minItems', 'maxProperties', 'minProperties']);
|
|
51
|
+
const SCHEMA_BOOLEAN_KEYS = new Set(['exclusiveMaximum', 'exclusiveMinimum', 'uniqueItems', 'nullable', 'readOnly', 'writeOnly', 'deprecated']);
|
|
52
|
+
const SCHEMA_ANY_KEYS = new Set(['default', 'example']);
|
|
53
|
+
const SCHEMA_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'array', 'object']);
|
|
54
|
+
const MAX_SCHEMA_DEPTH = 32;
|
|
55
|
+
|
|
56
|
+
function sanitizeSchema(schema, depth = 0) {
|
|
57
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return null;
|
|
58
|
+
if (depth >= MAX_SCHEMA_DEPTH) return null;
|
|
59
|
+
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
62
|
+
if (SCHEMA_STRING_KEYS.has(key)) {
|
|
63
|
+
if (typeof value !== 'string') continue;
|
|
64
|
+
if (key === 'type' && !SCHEMA_TYPES.has(value)) continue;
|
|
65
|
+
out[key] = value;
|
|
66
|
+
} else if (SCHEMA_NUMBER_KEYS.has(key)) {
|
|
67
|
+
if (typeof value === 'number') out[key] = value;
|
|
68
|
+
} else if (SCHEMA_BOOLEAN_KEYS.has(key)) {
|
|
69
|
+
if (typeof value === 'boolean') out[key] = value;
|
|
70
|
+
} else if (SCHEMA_ANY_KEYS.has(key)) {
|
|
71
|
+
out[key] = value;
|
|
72
|
+
} else if (key === 'enum') {
|
|
73
|
+
if (Array.isArray(value) && value.length) out.enum = value;
|
|
74
|
+
} else if (key === 'required') {
|
|
75
|
+
const names = Array.isArray(value) ? value.filter(v => typeof v === 'string') : [];
|
|
76
|
+
if (names.length) out.required = names;
|
|
77
|
+
} else if (key === 'properties') {
|
|
78
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
|
79
|
+
const props = {};
|
|
80
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
81
|
+
const clean = sanitizeSchema(sub, depth + 1);
|
|
82
|
+
props[name] = clean || {};
|
|
83
|
+
}
|
|
84
|
+
out.properties = props;
|
|
85
|
+
} else if (key === 'items' || key === 'not') {
|
|
86
|
+
const clean = sanitizeSchema(value, depth + 1);
|
|
87
|
+
if (clean) out[key] = clean;
|
|
88
|
+
} else if (key === 'allOf' || key === 'anyOf' || key === 'oneOf') {
|
|
89
|
+
const clean = (Array.isArray(value) ? value : []).map(v => sanitizeSchema(v, depth + 1)).filter(Boolean);
|
|
90
|
+
if (clean.length) out[key] = clean;
|
|
91
|
+
} else if (key === 'additionalProperties') {
|
|
92
|
+
if (typeof value === 'boolean') out.additionalProperties = value;
|
|
93
|
+
else {
|
|
94
|
+
const clean = sanitizeSchema(value, depth + 1);
|
|
95
|
+
if (clean) out.additionalProperties = clean;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// anything else (including $ref) is dropped
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (out.type === 'array' && !out.items) out.items = {};
|
|
102
|
+
|
|
103
|
+
return Object.keys(out).length ? out : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A method-keyed `export const schema` returns its per-method slice; a flat
|
|
107
|
+
// schema applies to every method (mirror of the server scanner's sliceSchema).
|
|
108
|
+
function sliceSchema(schema, method) {
|
|
109
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
110
|
+
const methodKeyed = Object.keys(schema).some(k => HTTP_METHOD_KEYS.has(k));
|
|
111
|
+
if (!methodKeyed) return schema;
|
|
112
|
+
return schema[method] || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildOperation(row) {
|
|
116
|
+
const cfg = row.config || {};
|
|
117
|
+
const s = (row.schema && typeof row.schema === 'object') ? row.schema : {};
|
|
118
|
+
const successDesc = cfg.responseDescription ? String(cfg.responseDescription) : 'Success';
|
|
119
|
+
|
|
120
|
+
const op = {
|
|
121
|
+
operationId: operationId(row.method, row.path),
|
|
122
|
+
responses: {}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (cfg.summary) op.summary = String(cfg.summary);
|
|
126
|
+
const desc = cfg.description || row.description;
|
|
127
|
+
if (desc) op.description = String(desc);
|
|
128
|
+
if (cfg.deprecated) op.deprecated = true;
|
|
129
|
+
if (Array.isArray(cfg.tags) && cfg.tags.length) op.tags = cfg.tags.map(String);
|
|
130
|
+
|
|
131
|
+
const params = pathParamNames(row.path).map(name => ({
|
|
132
|
+
name,
|
|
133
|
+
in: 'path',
|
|
134
|
+
required: true,
|
|
135
|
+
schema: { type: 'string' }
|
|
136
|
+
}));
|
|
137
|
+
if (s.query && s.query.properties && typeof s.query.properties === 'object') {
|
|
138
|
+
const required = new Set(Array.isArray(s.query.required) ? s.query.required : []);
|
|
139
|
+
for (const [name, propSchema] of Object.entries(s.query.properties)) {
|
|
140
|
+
params.push({
|
|
141
|
+
name,
|
|
142
|
+
in: 'query',
|
|
143
|
+
required: required.has(name),
|
|
144
|
+
schema: sanitizeSchema(propSchema) || {}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (params.length) op.parameters = params;
|
|
149
|
+
|
|
150
|
+
const body = sanitizeSchema(s.body);
|
|
151
|
+
if (body) {
|
|
152
|
+
op.requestBody = { content: { 'application/json': { schema: body } } };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const response = sanitizeSchema(s.response);
|
|
156
|
+
if (response) {
|
|
157
|
+
op.responses['200'] = {
|
|
158
|
+
description: successDesc,
|
|
159
|
+
content: { 'application/json': { schema: response } }
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
op.responses['200'] = { description: successDesc };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (Array.isArray(cfg.roles) && cfg.roles.length) {
|
|
166
|
+
op['x-informer-roles'] = cfg.roles.map(String);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Curated public surface — see the server emitter for semantics.
|
|
170
|
+
if (cfg.api === 'public') {
|
|
171
|
+
op['x-informer-public'] = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return op;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Import every server/ handler and read its real exports into emitter rows.
|
|
179
|
+
* Returns { rows, warnings }: a file that fails to import degrades to skeleton
|
|
180
|
+
* routes (regex-detected methods) with a warning, never a lost route.
|
|
181
|
+
*/
|
|
182
|
+
async function collectLocalRoutes(projectRoot) {
|
|
183
|
+
const files = await walkJsFiles(join(projectRoot, 'server'), 'server');
|
|
184
|
+
const rows = [];
|
|
185
|
+
const warnings = [];
|
|
186
|
+
|
|
187
|
+
for (const { relPath, absPath } of files) {
|
|
188
|
+
const routePath = filePathToRoute(relPath);
|
|
189
|
+
let mod = null;
|
|
190
|
+
try {
|
|
191
|
+
mod = await import(pathToFileURL(absPath).href);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
warnings.push(`${relPath}: could not be imported (${err.message}) — frozen doc keeps its routes at skeleton level`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (mod) {
|
|
197
|
+
for (const method of VALID_METHODS) {
|
|
198
|
+
if (typeof mod[method] !== 'function') continue;
|
|
199
|
+
rows.push({
|
|
200
|
+
method,
|
|
201
|
+
path: routePath,
|
|
202
|
+
handlerPath: relPath,
|
|
203
|
+
config: (mod.config && typeof mod.config === 'object') ? mod.config : {},
|
|
204
|
+
description: typeof mod.description === 'string' ? mod.description : null,
|
|
205
|
+
schema: sliceSchema((mod.schema && typeof mod.schema === 'object') ? mod.schema : null, method)
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const source = await readFile(absPath, 'utf8');
|
|
210
|
+
const methods = new Set();
|
|
211
|
+
let m;
|
|
212
|
+
while ((m = METHOD_EXPORT_RE.exec(source)) !== null) methods.add(m[1] || m[2]);
|
|
213
|
+
for (const method of methods) {
|
|
214
|
+
rows.push({ method, path: routePath, handlerPath: relPath, config: {}, description: null, schema: null });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Deterministic doc: same ordering the live endpoint uses.
|
|
220
|
+
rows.sort((a, b) => (a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path)));
|
|
221
|
+
return { rows, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Build the frozen OpenAPI document for a publish.
|
|
226
|
+
*
|
|
227
|
+
* The installing tenant's naturalId ({owner}:{slug}) is unknowable at publish
|
|
228
|
+
* time, so the dispatch base is emitted as an OpenAPI server variable with the
|
|
229
|
+
* slug as its default.
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} opts
|
|
232
|
+
* @param {string} opts.projectRoot
|
|
233
|
+
* @param {string} opts.name - listing name (info.title)
|
|
234
|
+
* @param {string} opts.slug - marketplace slug
|
|
235
|
+
* @param {string} opts.version - the version being published (info.version)
|
|
236
|
+
* @returns {Promise<{ spec: Object|null, warnings: string[] }>} spec is null
|
|
237
|
+
* when the app has no server/ routes at all (nothing to freeze).
|
|
238
|
+
*/
|
|
239
|
+
export async function buildLocalOpenApi({ projectRoot, name, slug, version }) {
|
|
240
|
+
const { rows, warnings } = await collectLocalRoutes(projectRoot);
|
|
241
|
+
if (!rows.length) return { spec: null, warnings };
|
|
242
|
+
|
|
243
|
+
const paths = {};
|
|
244
|
+
const usedIds = new Set();
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
const oaPath = routePathToOpenApi(row.path);
|
|
247
|
+
paths[oaPath] = paths[oaPath] || {};
|
|
248
|
+
const op = buildOperation(row);
|
|
249
|
+
while (usedIds.has(op.operationId)) {
|
|
250
|
+
const m = op.operationId.match(/^(.*?)(?:_(\d+))?$/);
|
|
251
|
+
op.operationId = `${m[1]}_${(parseInt(m[2], 10) || 1) + 1}`;
|
|
252
|
+
}
|
|
253
|
+
usedIds.add(op.operationId);
|
|
254
|
+
paths[oaPath][row.method.toLowerCase()] = op;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let description;
|
|
258
|
+
try {
|
|
259
|
+
description = await readFile(join(projectRoot, 'API.md'), 'utf8');
|
|
260
|
+
} catch {
|
|
261
|
+
// no API.md — fall through to the generated blurb
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const spec = {
|
|
265
|
+
openapi: '3.0.3',
|
|
266
|
+
info: {
|
|
267
|
+
title: name || slug,
|
|
268
|
+
version: String(version),
|
|
269
|
+
description: description || (
|
|
270
|
+
`Auto-generated from the server/ routes of "${name || slug}". ` +
|
|
271
|
+
'Invoke a route through a cross-app dependency: context.<slot>.request({ method, url }).'
|
|
272
|
+
)
|
|
273
|
+
},
|
|
274
|
+
servers: [{
|
|
275
|
+
url: '/api/apps/{app}/view/_',
|
|
276
|
+
description: 'App server-route dispatch base ({app} is the installed naturalId, owner:name)',
|
|
277
|
+
variables: { app: { default: slug } }
|
|
278
|
+
}],
|
|
279
|
+
paths
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return { spec, warnings };
|
|
283
|
+
}
|
package/src/publish.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtemp, mkdir, copyFile, rm, readFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdtemp, mkdir, copyFile, rm, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { execFile } from 'node:child_process';
|
|
@@ -24,17 +24,21 @@ export { PublishError };
|
|
|
24
24
|
* the server's node-tar expects; COPYFILE_DISABLE suppresses macOS AppleDouble
|
|
25
25
|
* (`._*`) junk from leaking into the archive.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
27
|
+
* Entries carry either `abs` (copied from disk) or `content` (generated at
|
|
28
|
+
* publish time, e.g. the frozen openapi.json).
|
|
29
|
+
*
|
|
30
|
+
* @param {Array<{abs?: string, rel: string, content?: string}>} files
|
|
28
31
|
* @returns {Promise<Buffer>}
|
|
29
32
|
*/
|
|
30
33
|
async function buildArchive(files) {
|
|
31
34
|
const stage = await mkdtemp(join(tmpdir(), 'informer-publish-'));
|
|
32
35
|
const archivePath = `${stage}.tgz`;
|
|
33
36
|
try {
|
|
34
|
-
for (const { abs, rel } of files) {
|
|
37
|
+
for (const { abs, rel, content } of files) {
|
|
35
38
|
const dest = join(stage, rel);
|
|
36
39
|
await mkdir(dirname(dest), { recursive: true });
|
|
37
|
-
await
|
|
40
|
+
if (content !== undefined) await writeFile(dest, content);
|
|
41
|
+
else await copyFile(abs, dest);
|
|
38
42
|
}
|
|
39
43
|
await execFileP('tar', ['-czf', archivePath, '-C', stage, '.'], {
|
|
40
44
|
env: { ...process.env, COPYFILE_DISABLE: '1' }
|
package/src/server-routes.js
CHANGED
|
@@ -9,9 +9,21 @@ const VALID_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
|
9
9
|
// hanging the handler.
|
|
10
10
|
const FETCH_TIMEOUT_MS = 30000;
|
|
11
11
|
|
|
12
|
-
// Strict base64 — see view-api.js for the rationale. Mirror kept identical
|
|
13
|
-
//
|
|
14
|
-
|
|
12
|
+
// Strict base64 — see view-api.js for the rationale. Mirror kept identical to
|
|
13
|
+
// keep dev and prod behavior aligned, down to the scan: the quantified
|
|
14
|
+
// /^[A-Za-z0-9+/]+={0,2}$/ throws "RangeError: Maximum call stack size
|
|
15
|
+
// exceeded" instead of answering once a body passes ~4M characters, so dev
|
|
16
|
+
// would 500 on exactly the large images and PDFs prod serves fine.
|
|
17
|
+
const NON_BASE64 = /[^A-Za-z0-9+/]/; // unquantified: never backtracks
|
|
18
|
+
|
|
19
|
+
function isBase64(s) {
|
|
20
|
+
if (typeof s !== 'string') return false;
|
|
21
|
+
let end = s.length;
|
|
22
|
+
while (end > 0 && s.charCodeAt(end - 1) === 0x3d /* '=' */) end--;
|
|
23
|
+
if (s.length - end > 2) return false;
|
|
24
|
+
if (end === 0) return true; // '', '=', '==' → zero bytes
|
|
25
|
+
return !NON_BASE64.test(end === s.length ? s : s.slice(0, end));
|
|
26
|
+
}
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* Convert a file path under server/ to a route path.
|
|
@@ -120,6 +132,8 @@ async function scanRoutes(serverDir) {
|
|
|
120
132
|
}));
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
export { filePathToRoute, walkJsFiles };
|
|
136
|
+
|
|
123
137
|
async function walkJsFiles(dir, basePath) {
|
|
124
138
|
const results = [];
|
|
125
139
|
let items;
|
|
@@ -398,7 +412,7 @@ export function createMiddleware(viteServer, { serverOrigin, authHeader, devWork
|
|
|
398
412
|
if (responseBody === null) {
|
|
399
413
|
res.end();
|
|
400
414
|
} else if (encoding === 'base64' && typeof responseBody === 'string') {
|
|
401
|
-
if (!
|
|
415
|
+
if (!isBase64(responseBody)) {
|
|
402
416
|
throw new Error('Handler returned malformed base64 body');
|
|
403
417
|
}
|
|
404
418
|
res.end(Buffer.from(responseBody, 'base64'));
|