@nestia/editor 14.0.1 → 14.0.3
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/dist/assets/{index-5lpEeH1X.js → index-DIkEkcTz.js} +132 -132
- package/dist/index.html +1 -1
- package/lib/NestiaEditorApplication.d.ts +10 -0
- package/lib/NestiaEditorApplication.js +20 -4
- package/lib/NestiaEditorApplication.js.map +1 -1
- package/lib/NestiaEditorApplication.mjs +17 -1
- package/lib/NestiaEditorIframe.d.ts +31 -0
- package/lib/NestiaEditorIframe.js +22 -4
- package/lib/NestiaEditorIframe.js.map +1 -1
- package/lib/NestiaEditorIframe.mjs +49 -10
- package/lib/NestiaEditorModule.d.ts +46 -0
- package/lib/NestiaEditorModule.js +96 -24
- package/lib/NestiaEditorModule.js.map +1 -1
- package/lib/NestiaEditorModule.mjs +27 -13
- package/lib/NestiaEditorUploader.d.ts +19 -0
- package/lib/NestiaEditorUploader.js +10 -0
- package/lib/NestiaEditorUploader.js.map +1 -1
- package/lib/NestiaEditorUploader.mjs +11 -1
- package/lib/internal/NestiaEditorArchiver.d.ts +35 -3
- package/lib/internal/NestiaEditorArchiver.js +60 -8
- package/lib/internal/NestiaEditorArchiver.js.map +1 -1
- package/lib/internal/NestiaEditorArchiver.mjs +25 -5
- package/lib/internal/NestiaEditorComposer.d.ts +68 -0
- package/lib/internal/NestiaEditorComposer.js +27 -0
- package/lib/internal/NestiaEditorComposer.js.map +1 -1
- package/lib/internal/NestiaEditorFileUploader.d.ts +21 -0
- package/lib/internal/NestiaEditorFileUploader.js +12 -0
- package/lib/internal/NestiaEditorFileUploader.js.map +1 -1
- package/lib/internal/NestiaEditorFileUploader.mjs +12 -0
- package/package.json +9 -8
- package/src/NestiaEditorApplication.tsx +19 -3
- package/src/NestiaEditorIframe.tsx +61 -6
- package/src/NestiaEditorModule.ts +111 -35
- package/src/NestiaEditorUploader.tsx +19 -0
- package/src/internal/NestiaEditorArchiver.ts +65 -10
- package/src/internal/NestiaEditorComposer.ts +78 -0
- package/src/internal/NestiaEditorFileUploader.tsx +21 -0
|
@@ -3,6 +3,18 @@ import { load } from "js-yaml";
|
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
import FileUpload from "react-mui-fileuploader";
|
|
5
5
|
//#region src/internal/NestiaEditorFileUploader.tsx
|
|
6
|
+
/**
|
|
7
|
+
* A file picker that reads an OpenAPI document from a JSON or YAML file.
|
|
8
|
+
*
|
|
9
|
+
* The last chosen file is read and parsed by its extension, and the result is passed to `onChange` with the parsed document, or with an error message when the content does not parse. Clearing the selection reports `null` for both.
|
|
10
|
+
*
|
|
11
|
+
* A file-reading failure rejects the uploader callback before parsing; it is not reported through `onChange`. Parsing reports content without validating its OpenAPI shape; the composer performs that validation.
|
|
12
|
+
*
|
|
13
|
+
* @evidence contracts/common.md#principled-implementation The file is decoded as UTF-8 text and parsed with `JSON.parse` for a `json` extension and with `js-yaml` otherwise; a parse failure is reported with the message for the format, and only the last chosen file is kept, matching the single-file picker.
|
|
14
|
+
* @evidence contracts/common.md#clear-and-simple-design One component wraps the third-party uploader and owns only reading and parsing; the form around it belongs to `NestiaEditorUploader`.
|
|
15
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The parser is chosen by the file extension alone; no document name or content is special-cased.
|
|
16
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states which file is read, how it is parsed, and what `onChange` receives in each case.
|
|
17
|
+
*/
|
|
6
18
|
function NestiaEditorFileUploader(props) {
|
|
7
19
|
const [elements, setElements] = React.useState([]);
|
|
8
20
|
const onChange = async (array) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestia/editor",
|
|
3
|
-
"version": "14.0.
|
|
3
|
+
"version": "14.0.3",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"prettier": "^3.8.1",
|
|
45
45
|
"react-mui-fileuploader": "^0.5.2",
|
|
46
46
|
"typia": "15.0.0",
|
|
47
|
-
"@nestia/migrate": "^14.0.
|
|
47
|
+
"@nestia/migrate": "^14.0.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "^9.13.0",
|
|
51
|
-
"@nestjs/common": "^
|
|
52
|
-
"@nestjs/core": "^
|
|
53
|
-
"@nestjs/platform-express": "^
|
|
54
|
-
"@nestjs/platform-fastify": "^
|
|
51
|
+
"@nestjs/common": "^12.1.2",
|
|
52
|
+
"@nestjs/core": "^12.1.2",
|
|
53
|
+
"@nestjs/platform-express": "^12.1.2",
|
|
54
|
+
"@nestjs/platform-fastify": "^12.1.2",
|
|
55
55
|
"@ttsc/unplugin": "^0.28.1",
|
|
56
56
|
"@types/js-yaml": "^4.0.9",
|
|
57
57
|
"@types/node": "^25.3.3",
|
|
@@ -82,10 +82,11 @@
|
|
|
82
82
|
"scripts": {
|
|
83
83
|
"build": "npm run build:static && npm run build:lib",
|
|
84
84
|
"build:static": "rimraf dist && ttsc && vite build",
|
|
85
|
-
"build:lib": "rimraf lib && ttsc --project tsconfig.lib.json &&
|
|
85
|
+
"build:lib": "rimraf lib && ttsc --project tsconfig.lib.json && rolldown -c",
|
|
86
86
|
"dev": "vite",
|
|
87
87
|
"lint": "eslint .",
|
|
88
|
-
"preview": "vite preview"
|
|
88
|
+
"preview": "vite preview",
|
|
89
|
+
"evidence": "evidence"
|
|
89
90
|
},
|
|
90
91
|
"types": "lib/index.d.ts",
|
|
91
92
|
"module": "lib/index.mjs"
|
|
@@ -2,8 +2,19 @@ import { Typography } from "@mui/material";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
|
|
4
4
|
import { NestiaEditorIframe } from "./NestiaEditorIframe";
|
|
5
|
+
import { NESTIA_EDITOR_DEFAULT_PACKAGE } from "./internal/NestiaEditorDefaultPackage";
|
|
5
6
|
import { NestiaEditorUploader } from "./NestiaEditorUploader";
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The static editor page: converts an OpenAPI document into a downloadable project.
|
|
10
|
+
*
|
|
11
|
+
* The page reads its settings from the query string (`url`, `mode`, `package`, `keyword`, `simulate`, `e2e`) and from `window` globals the served `index.html` sets. It renders the iframe flow when a document location is given, or when `swagger.json` or `swagger.yaml` is served beside the page, and the uploader otherwise. The `uploader` query key forces the uploader.
|
|
12
|
+
*
|
|
13
|
+
* @evidence contracts/common.md#principled-implementation An explicit query URL is forwarded to the iframe, which loads it and reports a fetch error. Without one, swagger.json and then swagger.yaml are probed for HTTP 200; absent or failed lookup falls back to the uploader. Parsed generation options, including keyword, are passed to the iframe rather than replaced by its direct-component defaults.
|
|
14
|
+
* @evidence contracts/common.md#clear-and-simple-design One component chooses between two existing components; the query and probing logic stays in the private `getAsset` and `findSwagger`, and option parsing lives in one place.
|
|
15
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The package name default comes from the shared constant, and no document name, host, or fixture is special-cased; booleans accept only `true` and `1`, as the query contract states.
|
|
16
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what the page does, where each setting comes from, the lookup order, and the `uploader` override.
|
|
17
|
+
*/
|
|
7
18
|
export function NestiaEditorApplication() {
|
|
8
19
|
const [ready, setReady] = React.useState(false);
|
|
9
20
|
const [asset, setAsset] = React.useState<IAsset | null>(null);
|
|
@@ -24,6 +35,7 @@ export function NestiaEditorApplication() {
|
|
|
24
35
|
<NestiaEditorIframe
|
|
25
36
|
swagger={asset.url}
|
|
26
37
|
package={asset.package}
|
|
38
|
+
keyword={asset.keyword}
|
|
27
39
|
simulate={asset.simulate}
|
|
28
40
|
e2e={asset.e2e}
|
|
29
41
|
mode={asset.mode}
|
|
@@ -65,7 +77,7 @@ async function getAsset(): Promise<IAsset | null> {
|
|
|
65
77
|
const e2e: boolean | string | null = query.get("e2e") ?? (window as any).e2e;
|
|
66
78
|
return {
|
|
67
79
|
mode: mode === "nest" ? "nest" : "sdk",
|
|
68
|
-
package: packageName ??
|
|
80
|
+
package: packageName ?? NESTIA_EDITOR_DEFAULT_PACKAGE,
|
|
69
81
|
url,
|
|
70
82
|
keyword:
|
|
71
83
|
keyword !== null
|
|
@@ -80,8 +92,12 @@ async function getAsset(): Promise<IAsset | null> {
|
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
async function findSwagger(file: string): Promise<string | null> {
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
try {
|
|
96
|
+
const response: Response = await fetch(file);
|
|
97
|
+
return response.status === 200 ? file : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
interface IAsset {
|
|
@@ -16,7 +16,23 @@ import { IValidation } from "typia";
|
|
|
16
16
|
|
|
17
17
|
import { NestiaEditorArchiver } from "./internal/NestiaEditorArchiver";
|
|
18
18
|
import { NestiaEditorComposer } from "./internal/NestiaEditorComposer";
|
|
19
|
+
import { NESTIA_EDITOR_DEFAULT_PACKAGE } from "./internal/NestiaEditorDefaultPackage";
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Composes a project from an OpenAPI document and offers it as a zip download.
|
|
23
|
+
*
|
|
24
|
+
* The component loads the document, from a URL or from the object given,
|
|
25
|
+
* generates the project in the browser, and shows the three stages as a
|
|
26
|
+
* stepper. A fetch failure or a composition failure is reported in place of the
|
|
27
|
+
* stage's progress, and the operations the composer could not convert are
|
|
28
|
+
* listed beside the download, so an incomplete project never passes for a
|
|
29
|
+
* complete one.
|
|
30
|
+
*
|
|
31
|
+
* @evidence contracts/common.md#principled-implementation The three stages run once, in order, from an effect: load the document, compose it with NestiaEditorComposer, and expose the files for download. Document-loading and composer failure results set their stage error states. Operations the composer skipped are listed with the download. Unexpected errors outside these operations are logged by the outer catch without setting an error state; only completed stages advance the stepper.
|
|
32
|
+
* @evidence contracts/common.md#clear-and-simple-design One component owns the stepper state; document loading and operation counting stay in the private `getDocument` and `aggregateOperation`, and archiving and composition are delegated to their own namespaces.
|
|
33
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The option defaults (keyword, simulate, e2e enabled) are the documented product defaults, and the archive name comes from the shared default package constant, not from a fixture name.
|
|
34
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the stages and how failures appear.
|
|
35
|
+
*/
|
|
20
36
|
export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
21
37
|
const [step, setStep] = React.useState(0);
|
|
22
38
|
const [fetchError, setFetchError] = React.useState<string | null>(null);
|
|
@@ -25,8 +41,11 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
25
41
|
>({});
|
|
26
42
|
const [composerError, setComposerError] = React.useState<any | null>(null);
|
|
27
43
|
const [files, setFiles] = React.useState<Record<string, string> | null>(null);
|
|
44
|
+
const [skipped, setSkipped] = React.useState<NestiaEditorComposer.ISkipped[]>(
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
28
47
|
const archive: string = NestiaEditorArchiver.name(
|
|
29
|
-
props.package ??
|
|
48
|
+
props.package ?? NESTIA_EDITOR_DEFAULT_PACKAGE,
|
|
30
49
|
);
|
|
31
50
|
|
|
32
51
|
React.useEffect(() => {
|
|
@@ -56,7 +75,7 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
56
75
|
keyword: props.keyword ?? true,
|
|
57
76
|
simulate: props.simulate ?? true,
|
|
58
77
|
e2e: props.e2e ?? true,
|
|
59
|
-
package: props.package ??
|
|
78
|
+
package: props.package ?? NESTIA_EDITOR_DEFAULT_PACKAGE,
|
|
60
79
|
});
|
|
61
80
|
} catch (exp) {
|
|
62
81
|
return {
|
|
@@ -73,6 +92,7 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
73
92
|
|
|
74
93
|
// READY TO DOWNLOAD
|
|
75
94
|
setStep(2);
|
|
95
|
+
setSkipped(result.data.skipped);
|
|
76
96
|
setFiles(result.data.files);
|
|
77
97
|
})().catch((exp) => {
|
|
78
98
|
console.error("unknown error", exp);
|
|
@@ -146,7 +166,7 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
146
166
|
.toLocaleString()}
|
|
147
167
|
</li>
|
|
148
168
|
{Object.entries(operations).map(([method, count]) => (
|
|
149
|
-
<li>
|
|
169
|
+
<li key={method}>
|
|
150
170
|
{method}: #{count.toLocaleString()}
|
|
151
171
|
</li>
|
|
152
172
|
))}
|
|
@@ -189,6 +209,24 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
189
209
|
>
|
|
190
210
|
Download {archive}
|
|
191
211
|
</Button>
|
|
212
|
+
{skipped.length !== 0 ? (
|
|
213
|
+
<>
|
|
214
|
+
<br />
|
|
215
|
+
<br />
|
|
216
|
+
<Alert severity="warning">
|
|
217
|
+
<AlertTitle>Skipped Operations</AlertTitle>
|
|
218
|
+
The project leaves out operations that could not be
|
|
219
|
+
converted:
|
|
220
|
+
<ul>
|
|
221
|
+
{skipped.map((s) => (
|
|
222
|
+
<li key={`${s.method} ${s.path}`}>
|
|
223
|
+
{s.method} {s.path}: {s.messages.join(" ")}
|
|
224
|
+
</li>
|
|
225
|
+
))}
|
|
226
|
+
</ul>
|
|
227
|
+
</Alert>
|
|
228
|
+
</>
|
|
229
|
+
) : null}
|
|
192
230
|
</>
|
|
193
231
|
) : null}
|
|
194
232
|
</StepContent>
|
|
@@ -198,23 +236,40 @@ export function NestiaEditorIframe(props: NestiaEditorIframe.IProps) {
|
|
|
198
236
|
</div>
|
|
199
237
|
);
|
|
200
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Properties of {@link NestiaEditorIframe}.
|
|
241
|
+
*
|
|
242
|
+
* `swagger` is an OpenAPI document or the URL to fetch it from. The options
|
|
243
|
+
* mirror the generator options; `mode` is internal.
|
|
244
|
+
*
|
|
245
|
+
* @evidence contracts/common.md#principled-implementation The namespace holds the property type of the component with the same name, so the component and its inputs are one public identity.
|
|
246
|
+
* @evidence contracts/common.md#clear-and-simple-design It contains one interface and nothing else.
|
|
247
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It adds no behavior; the option defaults live in the component that reads them.
|
|
248
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states what `swagger` accepts and which members are internal.
|
|
249
|
+
*/
|
|
201
250
|
export namespace NestiaEditorIframe {
|
|
202
251
|
export interface IProps {
|
|
252
|
+
/** Source document or URL loaded once when this component mounts. */
|
|
203
253
|
swagger:
|
|
204
254
|
| string
|
|
205
255
|
| SwaggerV2.IDocument
|
|
206
256
|
| OpenApiV3.IDocument
|
|
207
257
|
| OpenApiV3_1.IDocument;
|
|
258
|
+
|
|
259
|
+
/** Generated package identity; defaults to @ORGANIZATION/PROJECT. */
|
|
208
260
|
package?: string;
|
|
261
|
+
|
|
262
|
+
/** Enable keyword parameter objects; enabled when omitted. */
|
|
209
263
|
keyword?: boolean;
|
|
264
|
+
|
|
265
|
+
/** Include SDK simulators; enabled when omitted. */
|
|
210
266
|
simulate?: boolean;
|
|
267
|
+
|
|
268
|
+
/** Include generated E2E functions; enabled when omitted. */
|
|
211
269
|
e2e?: boolean;
|
|
212
270
|
|
|
213
271
|
/** @internal */
|
|
214
272
|
mode?: "nest" | "sdk";
|
|
215
|
-
|
|
216
|
-
/** @internal */
|
|
217
|
-
files?: Record<string, string>;
|
|
218
273
|
}
|
|
219
274
|
}
|
|
220
275
|
|
|
@@ -5,8 +5,57 @@ import type {
|
|
|
5
5
|
SwaggerV2,
|
|
6
6
|
} from "@typia/interface";
|
|
7
7
|
import * as fs from "fs";
|
|
8
|
+
import path from "path";
|
|
8
9
|
|
|
10
|
+
import { NESTIA_EDITOR_DEFAULT_PACKAGE } from "./internal/NestiaEditorDefaultPackage";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Serves the editor from a NestJS application.
|
|
14
|
+
*
|
|
15
|
+
* Call {@link setup} before `listen()` to expose the editor page, its bundle,
|
|
16
|
+
* and the OpenAPI document under a path of the application.
|
|
17
|
+
*
|
|
18
|
+
* @evidence contracts/common.md#principled-implementation The module registers static routes on the application's own HTTP adapter, so the editor needs no second server.
|
|
19
|
+
* @evidence contracts/common.md#clear-and-simple-design One namespace with one entry point; asset reading and location resolution stay in module-private helpers.
|
|
20
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The routes use the adapter's public `get` method, and the one read of NestJS internals is the global prefix, described at `setup`.
|
|
21
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the entry point and that it must run before `listen()`.
|
|
22
|
+
*/
|
|
9
23
|
export namespace NestiaEditorModule {
|
|
24
|
+
/**
|
|
25
|
+
* Registers the editor routes on a NestJS application.
|
|
26
|
+
*
|
|
27
|
+
* The routes are `<prefix>/index.html`, the bundle under `<prefix>/assets/`,
|
|
28
|
+
* `<prefix>/swagger.json`, and redirects from `<prefix>` and `<prefix>/` to
|
|
29
|
+
* the page, where the prefix joins the application's global prefix and
|
|
30
|
+
* `path`. A document given as an object is served as is. A document given by
|
|
31
|
+
* location is fetched on the first request, after `listen()`, because a path
|
|
32
|
+
* is resolved against the address the application listens on, and the fetched
|
|
33
|
+
* document is cached; a failed fetch answers 502 and is not cached.
|
|
34
|
+
*
|
|
35
|
+
* Setup reads each page-referenced JavaScript asset once. Static setup work
|
|
36
|
+
* scales with page and asset bytes; later requests reuse the registered
|
|
37
|
+
* strings. A location-based document is fetched and serialized on demand.
|
|
38
|
+
*
|
|
39
|
+
* Each Swagger route shares one successful serialized document and one
|
|
40
|
+
* in-flight fetch. A failed fetch clears the pending promise so later
|
|
41
|
+
* requests can try again. The location and document remain fixed for that
|
|
42
|
+
* registration.
|
|
43
|
+
*
|
|
44
|
+
* The application adapter owns the registered closures and their strings for
|
|
45
|
+
* the route lifetime. Setup creates no process-global history. This API
|
|
46
|
+
* supplies no fetch cancellation or timeout, so an unresolved request remains
|
|
47
|
+
* pending until its transport settles.
|
|
48
|
+
*
|
|
49
|
+
* Built assets use Node filesystem APIs and native path joining from the
|
|
50
|
+
* installed module directory. Page script URLs are validated independently
|
|
51
|
+
* from filesystem paths. Swagger locations use URL resolution against
|
|
52
|
+
* application.getUrl.
|
|
53
|
+
*
|
|
54
|
+
* @evidence contracts/common.md#principled-implementation The prefix is built by joining the global prefix and the path and dropping empty segments, so slashes never double; the static files are read once at setup, and the swagger route is lazy for a location because the listening address, which relative locations need, does not exist before `listen()`.
|
|
55
|
+
* @evidence contracts/common.md#clear-and-simple-design One function performs the registration; reading the built page and bundle, reading the global prefix, and resolving a location are separate module-private helpers.
|
|
56
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts NestJS exposes no public getter for the global prefix, so `getGlobalPrefix` reads the internal `config.globalPrefix` and falls back to an empty prefix when it is absent; this is the single foreign-internal read, kept as a stated limitation rather than replaced by a shadow copy of the prefix.
|
|
57
|
+
* @evidence contracts/common.md#meaningful-documentation The comment lists the routes and prefix rule, explains setup versus request-time work and cached-document ownership, and states the absent timeout/cancellation limitation.
|
|
58
|
+
*/
|
|
10
59
|
export const setup = async (props: {
|
|
11
60
|
path: string;
|
|
12
61
|
application: INestApplication;
|
|
@@ -20,21 +69,21 @@ export namespace NestiaEditorModule {
|
|
|
20
69
|
simulate?: boolean;
|
|
21
70
|
e2e?: boolean;
|
|
22
71
|
}): Promise<void> => {
|
|
23
|
-
const prefix: string =
|
|
24
|
-
"/"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.join("/");
|
|
72
|
+
const prefix: string = [getGlobalPrefix(props.application), props.path]
|
|
73
|
+
.join("/")
|
|
74
|
+
.split("/")
|
|
75
|
+
.filter((str) => str.length !== 0)
|
|
76
|
+
.map((str) => "/" + str)
|
|
77
|
+
.join("");
|
|
30
78
|
const adaptor: INestHttpAdaptor = props.application.getHttpAdapter();
|
|
79
|
+
const index: string = await getIndex(props);
|
|
31
80
|
const staticFiles: IStaticFile[] = [
|
|
32
81
|
{
|
|
33
82
|
path: "/index.html",
|
|
34
83
|
type: "text/html",
|
|
35
|
-
content:
|
|
84
|
+
content: index,
|
|
36
85
|
},
|
|
37
|
-
await
|
|
86
|
+
...(await getJavaScripts(index)),
|
|
38
87
|
];
|
|
39
88
|
for (const f of staticFiles) {
|
|
40
89
|
adaptor.get(prefix + f.path, (_: any, res: any) => {
|
|
@@ -51,15 +100,25 @@ export namespace NestiaEditorModule {
|
|
|
51
100
|
typeof props.swagger === "string"
|
|
52
101
|
? null
|
|
53
102
|
: JSON.stringify(props.swagger, null, 2);
|
|
103
|
+
let pending: Promise<string> | null = null;
|
|
54
104
|
adaptor.get(prefix + "/swagger.json", async (_: any, res: any) => {
|
|
55
105
|
try {
|
|
56
|
-
document
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
106
|
+
if (document === null) {
|
|
107
|
+
pending ??= (async () =>
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
await getSwagger(
|
|
110
|
+
await resolveLocation(
|
|
111
|
+
props.application,
|
|
112
|
+
props.swagger as string,
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
))().finally(() => {
|
|
118
|
+
pending = null;
|
|
119
|
+
});
|
|
120
|
+
document = await pending;
|
|
121
|
+
}
|
|
63
122
|
} catch (error) {
|
|
64
123
|
res.status(502);
|
|
65
124
|
res.type("text/plain");
|
|
@@ -68,7 +127,7 @@ export namespace NestiaEditorModule {
|
|
|
68
127
|
res.type("application/json");
|
|
69
128
|
return res.send(document);
|
|
70
129
|
});
|
|
71
|
-
for (const p of ["", "/"])
|
|
130
|
+
for (const p of prefix.length === 0 ? ["/"] : ["", "/"])
|
|
72
131
|
adaptor.get(prefix + p, (_: any, res: any) => {
|
|
73
132
|
return res.redirect(prefix + "/index.html");
|
|
74
133
|
});
|
|
@@ -113,30 +172,47 @@ const getIndex = async (props: {
|
|
|
113
172
|
`${__dirname}/../dist/index.html`,
|
|
114
173
|
"utf8",
|
|
115
174
|
);
|
|
175
|
+
// Replacement functions, because a replacement string would read its dollar
|
|
176
|
+
// patterns, which a package name may legitimately contain.
|
|
116
177
|
return content
|
|
178
|
+
.replace(JSON.stringify(NESTIA_EDITOR_DEFAULT_PACKAGE), () =>
|
|
179
|
+
JSON.stringify(props.package ?? NESTIA_EDITOR_DEFAULT_PACKAGE),
|
|
180
|
+
)
|
|
117
181
|
.replace(
|
|
118
|
-
|
|
119
|
-
|
|
182
|
+
"window.simulate = false",
|
|
183
|
+
() => `window.simulate = ${!!props.simulate}`,
|
|
120
184
|
)
|
|
121
|
-
.replace("window.
|
|
122
|
-
.replace("window.e2e = false", `window.e2e = ${!!props.e2e}`);
|
|
185
|
+
.replace("window.e2e = false", () => `window.e2e = ${!!props.e2e}`);
|
|
123
186
|
};
|
|
124
187
|
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
188
|
+
const getJavaScripts = async (index: string): Promise<IStaticFile[]> => {
|
|
189
|
+
const scripts: Set<string> = new Set();
|
|
190
|
+
for (const match of index.matchAll(
|
|
191
|
+
/<script\b[^>]*\bsrc\s*=\s*(["'])(.*?)\1[^>]*>/gi,
|
|
192
|
+
)) {
|
|
193
|
+
const url = new URL(match[2]!, "https://nestia.invalid/");
|
|
194
|
+
if (
|
|
195
|
+
url.origin !== "https://nestia.invalid" ||
|
|
196
|
+
url.pathname.startsWith("/assets/") === false ||
|
|
197
|
+
url.pathname.endsWith(".js") === false ||
|
|
198
|
+
url.search.length !== 0 ||
|
|
199
|
+
url.hash.length !== 0
|
|
200
|
+
)
|
|
201
|
+
throw new Error(`Unsupported editor script asset: ${match[2]}`);
|
|
202
|
+
scripts.add(url.pathname);
|
|
203
|
+
}
|
|
204
|
+
if (scripts.size === 0)
|
|
205
|
+
throw new Error("The editor page references no JavaScript asset.");
|
|
206
|
+
return Promise.all(
|
|
207
|
+
[...scripts].map(async (asset) => ({
|
|
208
|
+
path: asset,
|
|
209
|
+
type: "application/javascript",
|
|
210
|
+
content: await fs.promises.readFile(
|
|
211
|
+
path.join(__dirname, "../dist", asset.slice(1)),
|
|
212
|
+
"utf8",
|
|
213
|
+
),
|
|
214
|
+
})),
|
|
128
215
|
);
|
|
129
|
-
const path: string | undefined = directory[0];
|
|
130
|
-
if (path === undefined)
|
|
131
|
-
throw new Error("Unreachable code, no JS file exists.");
|
|
132
|
-
return {
|
|
133
|
-
path: `/assets/${path}`,
|
|
134
|
-
type: "application/javascript",
|
|
135
|
-
content: await fs.promises.readFile(
|
|
136
|
-
`${__dirname}/../dist/assets/${path}`,
|
|
137
|
-
"utf8",
|
|
138
|
-
),
|
|
139
|
-
};
|
|
140
216
|
};
|
|
141
217
|
|
|
142
218
|
/** An absolute URL as is; a path of the application at its own address. */
|
|
@@ -16,6 +16,16 @@ import { NestiaEditorComposer } from "./internal/NestiaEditorComposer";
|
|
|
16
16
|
import { NESTIA_EDITOR_DEFAULT_PACKAGE } from "./internal/NestiaEditorDefaultPackage";
|
|
17
17
|
import { NestiaEditorFileUploader } from "./internal/NestiaEditorFileUploader";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The uploader form: reads a Swagger file, and downloads the generated project.
|
|
21
|
+
*
|
|
22
|
+
* The user picks a document, a package name, the mode (SDK or NestJS project), and the options. The project is generated in the browser and downloaded as a zip file. Operations the converter could not handle are reported, so an incomplete project is never presented as complete.
|
|
23
|
+
*
|
|
24
|
+
* @evidence contracts/common.md#principled-implementation The form keeps its inputs in component state, composes on request with `NestiaEditorComposer`, and hands the files to `NestiaEditorArchiver`. Composition and archive failures, file-parser errors reported by the picker, and skipped operations reach the error handler. File-reading rejection belongs to the picker callback and is not caught by this form.
|
|
25
|
+
* @evidence contracts/common.md#clear-and-simple-design One component owns the form state and delegates parsing the file, composing, and archiving to internal modules.
|
|
26
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The defaults are the same product defaults as the iframe flow and the package name comes from the shared constant; an error goes to `onError` when given and to `alert` otherwise, with no fixture-specific path.
|
|
27
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the inputs, the output, and how skipped operations are reported.
|
|
28
|
+
*/
|
|
19
29
|
export function NestiaEditorUploader(props: NestiaEditorUploader.IProps) {
|
|
20
30
|
// PARAMETERS
|
|
21
31
|
const [mode, setMode] = React.useState<"nest" | "sdk">("sdk");
|
|
@@ -149,8 +159,17 @@ export function NestiaEditorUploader(props: NestiaEditorUploader.IProps) {
|
|
|
149
159
|
</>
|
|
150
160
|
);
|
|
151
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Properties of {@link NestiaEditorUploader}.
|
|
164
|
+
*
|
|
165
|
+
* @evidence contracts/common.md#principled-implementation The namespace holds the property type of the component with the same name, so the component and its inputs are one public identity.
|
|
166
|
+
* @evidence contracts/common.md#clear-and-simple-design It contains one interface with one optional callback.
|
|
167
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It adds no behavior; when the callback is absent the component reports errors with `alert`.
|
|
168
|
+
* @evidence contracts/common.md#meaningful-documentation The comment names the component the properties belong to.
|
|
169
|
+
*/
|
|
152
170
|
export namespace NestiaEditorUploader {
|
|
153
171
|
export interface IProps {
|
|
172
|
+
/** Receives conversion errors and skipped-operation warnings; absent uses alert. */
|
|
154
173
|
onError?: (error: string) => void;
|
|
155
174
|
}
|
|
156
175
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Zip, ZipDeflate, strToU8 } from "fflate";
|
|
2
|
+
|
|
3
|
+
import { NESTIA_EDITOR_DEFAULT_PACKAGE } from "./NestiaEditorDefaultPackage";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Archives a composed project into a zip file and hands it to the browser.
|
|
@@ -8,21 +10,74 @@ import { strToU8, zipSync } from "fflate";
|
|
|
8
10
|
* WebContainer cannot execute. The editor therefore delivers the composed
|
|
9
11
|
* project as a downloadable zip archive instead of an embedded StackBlitz
|
|
10
12
|
* workspace.
|
|
13
|
+
*
|
|
14
|
+
* @evidence contracts/common.md#principled-implementation Packing is a synchronous zip of UTF-8 encoded entries through `fflate`, naming derives from the package name with a fixed character whitelist, and the download creates an object URL, clicks an anchor, and revokes the URL later.
|
|
15
|
+
* @evidence contracts/common.md#clear-and-simple-design Three operations, each one browser or zip concern, in one namespace with no shared state.
|
|
16
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The archive is built with the library's public functions and the download uses the standard anchor mechanism; nothing is patched.
|
|
17
|
+
* @evidence contracts/common.md#meaningful-documentation The namespace prose explains why the editor delivers a zip archive rather than an embedded workspace.
|
|
11
18
|
*/
|
|
12
19
|
export namespace NestiaEditorArchiver {
|
|
13
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Pack the composed project files into a zip archive.
|
|
22
|
+
*
|
|
23
|
+
* @evidence contracts/common.md#principled-implementation Each own file key names a synchronous ZipDeflate stream added to Zip. Filenames are values rather than dictionary properties, preserving prototype-related names without changing an object's prototype. UTF-8 encoding preserves text; ending the archive emits its directory, including for an empty project.
|
|
24
|
+
* @evidence contracts/common.md#clear-and-simple-design One loop supplies files to the public synchronous compression streams; the callback collects chunks which are copied once into the returned archive.
|
|
25
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It archives exactly the given files; no entry is added, filtered, or renamed.
|
|
26
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the input and output.
|
|
27
|
+
*/
|
|
14
28
|
export const pack = (files: Record<string, string>): Uint8Array => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
const chunks: Uint8Array[] = [];
|
|
30
|
+
const archive = new Zip((error, chunk) => {
|
|
31
|
+
if (error) throw error;
|
|
32
|
+
chunks.push(chunk);
|
|
33
|
+
});
|
|
34
|
+
for (const [key, value] of Object.entries(files)) {
|
|
35
|
+
const entry = new ZipDeflate(key);
|
|
36
|
+
archive.add(entry);
|
|
37
|
+
entry.push(strToU8(value), true);
|
|
38
|
+
}
|
|
39
|
+
archive.end();
|
|
40
|
+
const output = new Uint8Array(
|
|
41
|
+
chunks.reduce((length, chunk) => length + chunk.length, 0),
|
|
42
|
+
);
|
|
43
|
+
let offset = 0;
|
|
44
|
+
for (const chunk of chunks) {
|
|
45
|
+
output.set(chunk, offset);
|
|
46
|
+
offset += chunk.length;
|
|
47
|
+
}
|
|
48
|
+
return output;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compose a safe archive file name from a package name.
|
|
53
|
+
*
|
|
54
|
+
* A leading `@` is dropped and every run of characters outside letters,
|
|
55
|
+
* digits, `.`, `_`, and `-` becomes one `-`. A name with nothing left but
|
|
56
|
+
* dots, such as an empty package name, has no usable base for the archive, so
|
|
57
|
+
* the default package name is used instead. Other names retain their dots,
|
|
58
|
+
* including a leading dot.
|
|
59
|
+
*
|
|
60
|
+
* @evidence contracts/common.md#principled-implementation Dropping the `@` and replacing runs of characters outside the whitelist with one `-` yields a file name without path separators. Empty or dots-only bases use the default package name; otherwise dots are retained, so this is not a general rejection of hidden filenames or a validator of npm package names.
|
|
61
|
+
* @evidence contracts/common.md#clear-and-simple-design One private helper applies the character rule to the package name and to the default, so the rule exists once.
|
|
62
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts The rule applies to every input; the only special case is the empty-name guard, which follows from the safe-name requirement.
|
|
63
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states the character rule and the dots-only fallback.
|
|
64
|
+
*/
|
|
65
|
+
export const name = (packageName: string): string => {
|
|
66
|
+
const base: string = compose(packageName);
|
|
67
|
+
return `${/^\.*$/.test(base) ? compose(NESTIA_EDITOR_DEFAULT_PACKAGE) : base}.zip`;
|
|
19
68
|
};
|
|
20
69
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`${packageName.replace(/^@/, "").replace(/[^A-Za-z0-9._-]+/g, "-")}.zip`;
|
|
70
|
+
const compose = (packageName: string): string =>
|
|
71
|
+
packageName.replace(/^@/, "").replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
24
72
|
|
|
25
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Trigger a browser download of the packed archive.
|
|
75
|
+
*
|
|
76
|
+
* @evidence contracts/common.md#principled-implementation A Blob of the packed bytes is exposed through an object URL and an anchor with the `download` attribute is clicked, which is the browser's standard way to save generated content; the URL is revoked after 30 seconds so its memory is released.
|
|
77
|
+
* @evidence contracts/common.md#clear-and-simple-design One function, kept apart from `pack` so packing stays testable outside a browser.
|
|
78
|
+
* @evidence contracts/common.md#prohibited-implementation-shortcuts It uses the standard DOM and URL APIs; the delayed revocation is not a retry or a compensation.
|
|
79
|
+
* @evidence contracts/common.md#meaningful-documentation The comment states that it triggers a browser download of the packed archive.
|
|
80
|
+
*/
|
|
26
81
|
export const download = (props: {
|
|
27
82
|
name: string;
|
|
28
83
|
files: Record<string, string>;
|