@nestia/editor 14.0.0 → 14.0.2

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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{index-C1g1lYbd.js → index-rFcfd8RQ.js} +138 -137
  3. package/dist/index.html +1 -1
  4. package/lib/NestiaEditorApplication.d.ts +10 -0
  5. package/lib/NestiaEditorApplication.js +20 -4
  6. package/lib/NestiaEditorApplication.js.map +1 -1
  7. package/lib/NestiaEditorApplication.mjs +17 -1
  8. package/lib/NestiaEditorIframe.d.ts +31 -0
  9. package/lib/NestiaEditorIframe.js +22 -4
  10. package/lib/NestiaEditorIframe.js.map +1 -1
  11. package/lib/NestiaEditorIframe.mjs +49 -10
  12. package/lib/NestiaEditorModule.d.ts +46 -0
  13. package/lib/NestiaEditorModule.js +120 -30
  14. package/lib/NestiaEditorModule.js.map +1 -1
  15. package/lib/NestiaEditorModule.mjs +43 -23
  16. package/lib/NestiaEditorUploader.d.ts +19 -0
  17. package/lib/NestiaEditorUploader.js +15 -0
  18. package/lib/NestiaEditorUploader.js.map +1 -1
  19. package/lib/NestiaEditorUploader.mjs +18 -6
  20. package/lib/internal/NestiaEditorArchiver.d.ts +35 -3
  21. package/lib/internal/NestiaEditorArchiver.js +60 -8
  22. package/lib/internal/NestiaEditorArchiver.js.map +1 -1
  23. package/lib/internal/NestiaEditorArchiver.mjs +25 -5
  24. package/lib/internal/NestiaEditorComposer.d.ts +80 -0
  25. package/lib/internal/NestiaEditorComposer.js +33 -0
  26. package/lib/internal/NestiaEditorComposer.js.map +1 -1
  27. package/lib/internal/NestiaEditorComposer.mjs +8 -2
  28. package/lib/internal/NestiaEditorFileUploader.d.ts +21 -0
  29. package/lib/internal/NestiaEditorFileUploader.js +12 -0
  30. package/lib/internal/NestiaEditorFileUploader.js.map +1 -1
  31. package/lib/internal/NestiaEditorFileUploader.mjs +12 -0
  32. package/package.json +6 -5
  33. package/src/NestiaEditorApplication.tsx +19 -3
  34. package/src/NestiaEditorIframe.tsx +61 -6
  35. package/src/NestiaEditorModule.ts +139 -39
  36. package/src/NestiaEditorUploader.tsx +28 -0
  37. package/src/internal/NestiaEditorArchiver.ts +65 -10
  38. package/src/internal/NestiaEditorComposer.ts +96 -0
  39. package/src/internal/NestiaEditorFileUploader.tsx +21 -0
@@ -21,7 +21,8 @@ let NestiaEditorComposer;
21
21
  data: {
22
22
  files: props.files,
23
23
  openFile: config.openFile,
24
- startScript: config.startScript
24
+ startScript: config.startScript,
25
+ skipped: []
25
26
  }
26
27
  };
27
28
  const result = await migrate.NestiaMigrateApplication.validate(props.document);
@@ -41,7 +42,12 @@ let NestiaEditorComposer;
41
42
  data: {
42
43
  files,
43
44
  openFile: config.openFile,
44
- startScript: config.startScript
45
+ startScript: config.startScript,
46
+ skipped: app.getData().errors.map((error) => ({
47
+ method: error.method.toUpperCase(),
48
+ path: error.path,
49
+ messages: error.messages
50
+ }))
45
51
  }
46
52
  };
47
53
  };
@@ -1,7 +1,28 @@
1
1
  import { OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@typia/interface";
2
+ /**
3
+ * A file picker that reads an OpenAPI document from a JSON or YAML file.
4
+ *
5
+ * 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.
6
+ *
7
+ * 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.
8
+ *
9
+ * @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.
10
+ * @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`.
11
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The parser is chosen by the file extension alone; no document name or content is special-cased.
12
+ * @evidence contracts/common.md#meaningful-documentation The comment states which file is read, how it is parsed, and what `onChange` receives in each case.
13
+ */
2
14
  export declare function NestiaEditorFileUploader(props: NestiaEditorFileUploader.IProps): import("react/jsx-runtime").JSX.Element;
15
+ /**
16
+ * Properties of {@link NestiaEditorFileUploader}.
17
+ *
18
+ * @evidence contracts/common.md#principled-implementation The namespace holds the property type of the component with the same name.
19
+ * @evidence contracts/common.md#clear-and-simple-design It contains one interface with one callback.
20
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts It adds no behavior.
21
+ * @evidence contracts/common.md#meaningful-documentation The comment names the component the properties belong to.
22
+ */
3
23
  export declare namespace NestiaEditorFileUploader {
4
24
  interface IProps {
25
+ /** Receives parsed file content, parse failures, or null/null when cleared. */
5
26
  onChange: (swagger: SwaggerV2.IDocument | OpenApiV3.IDocument | OpenApiV3_1.IDocument | null, error: string | null) => void;
6
27
  }
7
28
  }
@@ -17,6 +17,18 @@ const jsx_runtime_1 = require("react/jsx-runtime");
17
17
  const js_yaml_1 = require("js-yaml");
18
18
  const react_1 = __importDefault(require("react"));
19
19
  const react_mui_fileuploader_1 = __importDefault(require("react-mui-fileuploader"));
20
+ /**
21
+ * A file picker that reads an OpenAPI document from a JSON or YAML file.
22
+ *
23
+ * 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.
24
+ *
25
+ * 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.
26
+ *
27
+ * @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.
28
+ * @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`.
29
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts The parser is chosen by the file extension alone; no document name or content is special-cased.
30
+ * @evidence contracts/common.md#meaningful-documentation The comment states which file is read, how it is parsed, and what `onChange` receives in each case.
31
+ */
20
32
  function NestiaEditorFileUploader(props) {
21
33
  const [elements, setElements] = react_1.default.useState([]);
22
34
  const onChange = (array) => __awaiter(this, void 0, void 0, function* () {
@@ -1 +1 @@
1
- {"version":3,"file":"NestiaEditorFileUploader.js","sourceRoot":"","sources":["../../src/internal/NestiaEditorFileUploader.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,qCAA+B;AAC/B,kDAA0B;AAC1B,oFAA4E;AAE5E,kCACE,KAAsC;IAEtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,eAAK,CAAC,QAAQ,CAAsB,EAAE,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,CAAO,KAA0B,EAAE,EAAE;QACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAsB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QACzD,MAAM,MAAM,GAAgB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,OAAO,GAAW,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,SAAS,GAAoB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAEjD,CAAC;QAEX,IAAI,CAAC;YACH,MAAM,IAAI,GAIR,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAA,cAAI,EAAC,OAAO,CAAC,CAAC;YAC7D,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;mBAAO,CAAC;YACP,KAAK,CAAC,QAAQ,CACZ,IAAI,EACJ,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,mBAAmB,CACjE,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAA,CAAC;IACF,OAAO,CACL,uBAAC,gCAAU,IACT,YAAY,EAAE,QAAQ,EACtB,aAAa,EAAE,QAAQ,EACvB,YAAY,EAAC,cAAc,EAC3B,SAAS,EAAE,KAAK,EAChB,SAAS,EAAE,KAAK,EAChB,cAAc,EAAE,CAAC,EACjB,KAAK,EAAC,uBAAuB,EAC7B,MAAM,EAAC,mCAAmC,EAC1C,WAAW,EAAC,YAAY,EACxB,UAAU,EAAC,kCAAkC,EAC7C,iBAAiB,EAAC,OAAO,GACzB,CACH,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"NestiaEditorFileUploader.js","sourceRoot":"","sources":["../../src/internal/NestiaEditorFileUploader.tsx"],"names":[],"mappings":";;;;;;;;;;;;;;;;AACA,qCAA+B;AAC/B,kDAA0B;AAC1B,oFAA4E;AAE5E;;;;;;;;;;;GAWG;AACH,kCACE,KAAsC;IAEtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,eAAK,CAAC,QAAQ,CAAsB,EAAE,CAAC,CAAC;IACxE,MAAM,QAAQ,GAAG,CAAO,KAA0B,EAAE,EAAE;QACpD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAsB,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QACzD,MAAM,MAAM,GAAgB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,OAAO,GAAW,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzD,MAAM,SAAS,GAAoB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAEjD,CAAC;QAEX,IAAI,CAAC;YACH,MAAM,IAAI,GAIR,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAA,cAAI,EAAC,OAAO,CAAC,CAAC;YAC7D,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;mBAAO,CAAC;YACP,KAAK,CAAC,QAAQ,CACZ,IAAI,EACJ,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,mBAAmB,CACjE,CAAC;YACF,OAAO;QACT,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAA,CAAC;IACF,OAAO,CACL,uBAAC,gCAAU,IACT,YAAY,EAAE,QAAQ,EACtB,aAAa,EAAE,QAAQ,EACvB,YAAY,EAAC,cAAc,EAC3B,SAAS,EAAE,KAAK,EAChB,SAAS,EAAE,KAAK,EAChB,cAAc,EAAE,CAAC,EACjB,KAAK,EAAC,uBAAuB,EAC7B,MAAM,EAAC,mCAAmC,EAC1C,WAAW,EAAC,YAAY,EACxB,UAAU,EAAC,kCAAkC,EAC7C,iBAAiB,EAAC,OAAO,GACzB,CACH,CAAC;AACJ,CAAC"}
@@ -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.0",
3
+ "version": "14.0.2",
4
4
  "main": "lib/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -43,8 +43,8 @@
43
43
  "js-yaml": "^4.3.1",
44
44
  "prettier": "^3.8.1",
45
45
  "react-mui-fileuploader": "^0.5.2",
46
- "typia": "^15.0.0",
47
- "@nestia/migrate": "^14.0.0"
46
+ "typia": "15.0.0",
47
+ "@nestia/migrate": "^14.0.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@eslint/js": "^9.13.0",
@@ -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 && tsc --project tsconfig.lib.json --emitDeclarationOnly && rolldown -c && node ../../deploy/verify-package-exports.cjs --esm-static",
85
+ "build:lib": "rimraf lib && ttsc --project tsconfig.lib.json && tsc --project tsconfig.lib.json --emitDeclarationOnly && 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 ?? "@ORGANIZATION/PROJECT",
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
- const response: Response = await fetch(file);
84
- return response.status === 200 ? file : null;
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 ?? "@ORGANIZATION/PROJECT",
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 ?? "@ORGANIZATION/PROJECT",
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,32 +69,21 @@ export namespace NestiaEditorModule {
20
69
  simulate?: boolean;
21
70
  e2e?: boolean;
22
71
  }): Promise<void> => {
23
- const prefix: string =
24
- "/" +
25
- [getGlobalPrefix(props.application), props.path]
26
- .join("/")
27
- .split("/")
28
- .filter((str) => str.length !== 0)
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: await getIndex(props),
36
- },
37
- {
38
- path: "/swagger.json",
39
- type: "application/json",
40
- content: JSON.stringify(
41
- typeof props.swagger === "string"
42
- ? await getSwagger(props.swagger)
43
- : props.swagger,
44
- null,
45
- 2,
46
- ),
84
+ content: index,
47
85
  },
48
- await getJavaScript(),
86
+ ...(await getJavaScripts(index)),
49
87
  ];
50
88
  for (const f of staticFiles) {
51
89
  adaptor.get(prefix + f.path, (_: any, res: any) => {
@@ -53,7 +91,43 @@ export namespace NestiaEditorModule {
53
91
  return res.send(f.content);
54
92
  });
55
93
  }
56
- for (const p of ["", "/"])
94
+
95
+ // A document given by location is read when the editor asks for it, not
96
+ // here: `setup()` runs before `listen()`, so the application's own path,
97
+ // such as `@nestjs/swagger`'s "/api-json", serves nothing yet, and a path
98
+ // is resolved against the address the application listens on.
99
+ let document: string | null =
100
+ typeof props.swagger === "string"
101
+ ? null
102
+ : JSON.stringify(props.swagger, null, 2);
103
+ let pending: Promise<string> | null = null;
104
+ adaptor.get(prefix + "/swagger.json", async (_: any, res: any) => {
105
+ try {
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
+ }
122
+ } catch (error) {
123
+ res.status(502);
124
+ res.type("text/plain");
125
+ return res.send(error instanceof Error ? error.message : String(error));
126
+ }
127
+ res.type("application/json");
128
+ return res.send(document);
129
+ });
130
+ for (const p of prefix.length === 0 ? ["/"] : ["", "/"])
57
131
  adaptor.get(prefix + p, (_: any, res: any) => {
58
132
  return res.redirect(prefix + "/index.html");
59
133
  });
@@ -98,32 +172,58 @@ const getIndex = async (props: {
98
172
  `${__dirname}/../dist/index.html`,
99
173
  "utf8",
100
174
  );
175
+ // Replacement functions, because a replacement string would read its dollar
176
+ // patterns, which a package name may legitimately contain.
101
177
  return content
178
+ .replace(JSON.stringify(NESTIA_EDITOR_DEFAULT_PACKAGE), () =>
179
+ JSON.stringify(props.package ?? NESTIA_EDITOR_DEFAULT_PACKAGE),
180
+ )
102
181
  .replace(
103
- `"@ORGANIZATION/PROJECT"`,
104
- JSON.stringify(props.package ?? "@ORGANIZATION/PROJECT"),
182
+ "window.simulate = false",
183
+ () => `window.simulate = ${!!props.simulate}`,
105
184
  )
106
- .replace("window.simulate = false", `window.simulate = ${!!props.simulate}`)
107
- .replace("window.e2e = false", `window.e2e = ${!!props.e2e}`);
185
+ .replace("window.e2e = false", () => `window.e2e = ${!!props.e2e}`);
108
186
  };
109
187
 
110
- const getJavaScript = async (): Promise<IStaticFile> => {
111
- const directory: string[] = await fs.promises.readdir(
112
- `${__dirname}/../dist/assets`,
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
+ })),
113
215
  );
114
- const path: string | undefined = directory[0];
115
- if (path === undefined)
116
- throw new Error("Unreachable code, no JS file exists.");
117
- return {
118
- path: `/assets/${path}`,
119
- type: "application/javascript",
120
- content: await fs.promises.readFile(
121
- `${__dirname}/../dist/assets/${path}`,
122
- "utf8",
123
- ),
124
- };
125
216
  };
126
217
 
218
+ /** An absolute URL as is; a path of the application at its own address. */
219
+ const resolveLocation = async (
220
+ application: INestApplication,
221
+ location: string,
222
+ ): Promise<string> =>
223
+ /^[a-z][a-z\d+\-.]*:/i.test(location)
224
+ ? location
225
+ : new URL(location, await application.getUrl()).href;
226
+
127
227
  const getSwagger = async (
128
228
  url: string,
129
229
  ): Promise<
@@ -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");
@@ -63,6 +73,15 @@ export function NestiaEditorUploader(props: NestiaEditorUploader.IProps) {
63
73
  name: NestiaEditorArchiver.name(name),
64
74
  files: result.data.files,
65
75
  });
76
+ if (result.data.skipped.length !== 0)
77
+ handleError(
78
+ [
79
+ "The project leaves out operations that could not be converted:",
80
+ ...result.data.skipped.map(
81
+ (s) => ` - ${s.method} ${s.path}: ${s.messages.join(" ")}`,
82
+ ),
83
+ ].join("\n"),
84
+ );
66
85
  } else {
67
86
  handleError(JSON.stringify(result.errors, null, 2));
68
87
  }
@@ -140,8 +159,17 @@ export function NestiaEditorUploader(props: NestiaEditorUploader.IProps) {
140
159
  </>
141
160
  );
142
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
+ */
143
170
  export namespace NestiaEditorUploader {
144
171
  export interface IProps {
172
+ /** Receives conversion errors and skipped-operation warnings; absent uses alert. */
145
173
  onError?: (error: string) => void;
146
174
  }
147
175
  }