@jxsuite/compiler 5.1.2 → 5.1.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.
Files changed (40) hide show
  1. package/dist/cli.js +220 -351
  2. package/dist/compiler.js +126 -257
  3. package/package.json +4 -4
  4. package/src/cli.ts +12 -14
  5. package/src/compiler.ts +45 -23
  6. package/src/shadow.ts +9 -8
  7. package/src/shared.ts +350 -296
  8. package/src/site/asset-mounts.ts +22 -12
  9. package/src/site/bundler.ts +38 -12
  10. package/src/site/client-runtime.ts +31 -17
  11. package/src/site/csp.ts +20 -11
  12. package/src/site/db-push.ts +38 -24
  13. package/src/site/dev-command.ts +3 -3
  14. package/src/site/format-host.ts +56 -45
  15. package/src/site/headers-emitter.ts +7 -4
  16. package/src/site/i18n.ts +20 -19
  17. package/src/site/image-cache.ts +16 -3
  18. package/src/site/image-optimizer.ts +19 -5
  19. package/src/site/image-transform.ts +56 -60
  20. package/src/site/img-loading.ts +11 -5
  21. package/src/site/layout-resolver.ts +7 -6
  22. package/src/site/link-relations.ts +10 -6
  23. package/src/site/locale-negotiation.ts +1 -1
  24. package/src/site/pages-discovery.ts +65 -47
  25. package/src/site/preview-server.ts +22 -19
  26. package/src/site/project-sections.ts +12 -3
  27. package/src/site/prototype-resolver.ts +94 -42
  28. package/src/site/schema-command.ts +26 -13
  29. package/src/site/service-worker.ts +2 -2
  30. package/src/site/site-build.ts +379 -262
  31. package/src/site/site-loader.ts +6 -3
  32. package/src/site/static-host.ts +3 -3
  33. package/src/site/validate-command.ts +64 -34
  34. package/src/site/well-known.ts +57 -12
  35. package/src/targets/compile-class.ts +17 -5
  36. package/src/targets/compile-client.ts +141 -106
  37. package/src/targets/compile-element.ts +157 -140
  38. package/src/targets/compile-server.ts +11 -5
  39. package/src/targets/compile-static.ts +57 -63
  40. package/src/types.ts +5 -6
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { existsSync, readFileSync } from "node:fs";
9
- import type { ImageConfig, ProjectConfig } from "@jxsuite/schema/types";
9
+ import type { ImageConfig, JxDocumentValue, ProjectConfig } from "@jxsuite/schema/types";
10
10
  import { errorMessage, parseProjectConfig } from "@jxsuite/schema/parse";
11
11
  import { resolve } from "node:path";
12
12
 
@@ -24,9 +24,10 @@ export interface ResolvedProjectConfig extends ProjectConfig {
24
24
  adapter?: string;
25
25
  outDir: string;
26
26
  format: string;
27
- trailingSlash: string;
27
+ /** `null` when project.json writes it so explicitly: the merge below keeps an author's null. */
28
+ trailingSlash: string | null;
28
29
  provider?: string | null;
29
- [key: string]: unknown;
30
+ [key: string]: JxDocumentValue | undefined;
30
31
  };
31
32
  }
32
33
 
@@ -92,6 +93,8 @@ export function loadProjectConfig(projectRoot: string) {
92
93
  build: { ...DEFAULTS.build, ...raw.build },
93
94
  defaults: { ...DEFAULTS.defaults, ...raw.defaults },
94
95
  images: { ...DEFAULTS.images, ...raw.images },
96
+ // An explicit JSON `null` url means no site URL, which every consumer reads as "".
97
+ url: raw.url ?? DEFAULTS.url,
95
98
  };
96
99
 
97
100
  // Preserve arrays and objects that shouldn't be shallow-merged
@@ -40,10 +40,10 @@ export function isNotFoundRoute(urlPattern: string): boolean {
40
40
  * `/` either way, and any other setting leaves the route as written.
41
41
  *
42
42
  * @param {string} urlPattern - A concrete route (`/about`, `/kb/a/b`)
43
- * @param {string} [trailingSlash] - `build.trailingSlash`
43
+ * @param {string | null} [trailingSlash] - `build.trailingSlash`
44
44
  * @returns {string} The route as the host serves it
45
45
  */
46
- export function servedRoute(urlPattern: string, trailingSlash?: string): string {
46
+ export function servedRoute(urlPattern: string, trailingSlash?: string | null): string {
47
47
  if (urlPattern === "/") {
48
48
  return "/";
49
49
  }
@@ -93,7 +93,7 @@ export function htmlDeclaresNoindex(html: string): boolean {
93
93
  }
94
94
 
95
95
  /** The `<head>` of a built page, or the whole string when it has no closing tag. */
96
- function headOf(html: string): { head: string; end: number } {
96
+ function headOf(html: string) {
97
97
  const end = html.search(/<\/head\s*>/i);
98
98
  return { end, head: end === -1 ? html : html.slice(0, end) };
99
99
  }
@@ -26,7 +26,8 @@ import { findA11yDefects } from "@jxsuite/schema/a11y";
26
26
  import { findDialogDefects } from "@jxsuite/schema/dialogs";
27
27
  import { findPopoverDefects, overlayScopeFor } from "@jxsuite/schema/overlays";
28
28
  import type { OverlayScope } from "@jxsuite/schema/overlays";
29
- import type { JxElement } from "@jxsuite/schema/types";
29
+ import { isJsonObject, isNonNullObject, isString } from "@jxsuite/schema/guards";
30
+ import type { JsonObject, JsonValue, JxElement } from "@jxsuite/schema/types";
30
31
  import { buildProjectExtensionRegistry } from "./format-host.ts";
31
32
  import { loadProjectConfig } from "./site-loader.ts";
32
33
  import { readBundledProjectSchemas } from "./schema-command.ts";
@@ -88,12 +89,14 @@ export interface ProjectTreeValidationResult {
88
89
 
89
90
  // Minimal structural types for the optional `ajv` dependency (same pattern as validate-project).
90
91
  interface AjvValidateFn {
91
- (doc: unknown): boolean;
92
+ (doc: JsonValue): boolean;
92
93
  errors?: unknown[] | null;
93
94
  }
95
+ /** What `compile` receives: parsed JSON, or the class schema generateClassSchema builds in memory. */
96
+ type AjvSchemaInput = JsonValue | ReturnType<typeof generateClassSchema>;
94
97
  interface AjvInstance {
95
- compile: (schema: unknown) => AjvValidateFn;
96
- addSchema: (schema: unknown) => AjvInstance;
98
+ compile: (schema: AjvSchemaInput) => AjvValidateFn;
99
+ addSchema: (schema: JsonValue) => AjvInstance;
97
100
  }
98
101
  type AjvCtor = new (opts: {
99
102
  allErrors: boolean;
@@ -104,8 +107,18 @@ type AddFormatsFn = (ajv: AjvInstance) => void;
104
107
 
105
108
  async function loadAjv(): Promise<{ Ajv: AjvCtor; addFormats: AddFormatsFn }> {
106
109
  try {
107
- // The schemas are JSON Schema 2020-12, so use the matching Ajv build.
108
- const { default: Ajv } = (await import("ajv/dist/2020")) as unknown as { default: AjvCtor };
110
+ /*
111
+ * The schemas are JSON Schema 2020-12, so use the matching Ajv build.
112
+ * SAFETY: the default export of `ajv/dist/2020` is the Ajv2020 class. AjvCtor restates, in
113
+ * JSON terms, only the three constructor options, `compile`, `addSchema` and the validate
114
+ * function's call and `errors` used below; ajv reports a schema value it cannot use as a thrown
115
+ * error.
116
+ */
117
+ const { default: Ajv } = (await import("ajv/dist/2020")) as { default: AjvCtor };
118
+ /*
119
+ * SAFETY: ajv-formats' default export is the plugin function that takes an Ajv instance and
120
+ * registers the formats on it; AddFormatsFn states only that call.
121
+ */
109
122
  // @ts-expect-error — resolved through @jxsuite/schema's dependency
110
123
  const { default: addFormats } = (await import("ajv-formats")) as { default: AddFormatsFn };
111
124
  return { addFormats, Ajv };
@@ -141,7 +154,7 @@ export async function validateProjectTree(
141
154
  continue;
142
155
  }
143
156
  checked += 1;
144
- const entry = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
157
+ const entry = readJson(path);
145
158
  const unresolvable = collectUnresolvableRefs(entry);
146
159
  if (unresolvable.length > 0) {
147
160
  issues.push({
@@ -180,6 +193,12 @@ export async function validateProjectTree(
180
193
  const scope = overlayScopeFor(
181
194
  walkClassFiles(root).flatMap((file) => {
182
195
  try {
196
+ /*
197
+ * SAFETY: not schema-checked here; the class-schema pass below reports a malformed
198
+ * definition. overlayScopeFor tolerates any parsed JSON in this slot: it skips `null`,
199
+ * reads `tagName` through isString (a primitive or array has none, so it is skipped), and
200
+ * reads `observedAttributes` through Array.isArray.
201
+ */
183
202
  return [JSON.parse(readFileSync(file, "utf8")) as JxElement];
184
203
  } catch {
185
204
  // A malformed definition is reported by the class-schema pass below, not here.
@@ -193,9 +212,13 @@ export async function validateProjectTree(
193
212
  continue; // Validated against the class schema below.
194
213
  }
195
214
  checked += 1;
196
- const doc = JSON.parse(readFileSync(file, "utf8")) as Record<string, unknown>;
215
+ const doc = readJson(file);
197
216
  if (validateDoc(doc)) {
198
- // Only a well-formed document is judged; a schema error is the report for the rest.
217
+ /*
218
+ * Only a well-formed document is judged; a schema error is the report for the rest.
219
+ * SAFETY: validateDoc just accepted `doc` against the bundled document schema, which is
220
+ * the JSON Schema rendering of JxElement.
221
+ */
199
222
  lint.push(...lintDocument(doc as JxElement, relative(root, file), scope));
200
223
  } else {
201
224
  issues.push({ errors: validateDoc.errors ?? [], file: relative(root, file) });
@@ -209,7 +232,7 @@ export async function validateProjectTree(
209
232
  const validateClass = classAjv.compile(generateClassSchema());
210
233
  for (const file of walkClassFiles(root)) {
211
234
  checked += 1;
212
- const doc = JSON.parse(readFileSync(file, "utf8")) as Record<string, unknown>;
235
+ const doc = readJson(file);
213
236
  if (!validateClass(doc)) {
214
237
  issues.push({ errors: validateClass.errors ?? [], file: relative(root, file) });
215
238
  }
@@ -230,11 +253,10 @@ export async function validateProjectTree(
230
253
  addFormats(fragAjv);
231
254
  for (const specifier of SHIPPED_RESOURCE_SPECIFIERS) {
232
255
  const resourcePath = selfRequire.resolve(specifier);
233
- const resource = JSON.parse(readFileSync(resourcePath, "utf8")) as Record<string, unknown>;
234
- fragAjv.addSchema(resource);
256
+ fragAjv.addSchema(readJson(resourcePath));
235
257
  }
236
258
  try {
237
- fragAjv.compile(JSON.parse(readFileSync(fragmentPath, "utf8")));
259
+ fragAjv.compile(readJson(fragmentPath));
238
260
  } catch (error) {
239
261
  issues.push({
240
262
  errors: [{ message: error instanceof Error ? error.message : String(error) }],
@@ -248,6 +270,14 @@ export async function validateProjectTree(
248
270
  return { checked, issues, lint, valid: issues.length === 0 && !fatalLint };
249
271
  }
250
272
 
273
+ /**
274
+ * Read and parse a JSON file. `JSON.parse` without a reviver only ever produces JSON values, so the
275
+ * declared type is exact; what shape of JSON it is stays for the caller to check.
276
+ */
277
+ function readJson(path: string): JsonValue {
278
+ return JSON.parse(readFileSync(path, "utf8"));
279
+ }
280
+
251
281
  /** The three lints over one document, as findings that name their file. */
252
282
  function lintDocument(doc: JxElement, file: string, scope: OverlayScope): ProjectTreeLintFinding[] {
253
283
  const findings: ProjectTreeLintFinding[] = [];
@@ -300,26 +330,29 @@ export function formatProjectTreeLint(result: ProjectTreeValidationResult): stri
300
330
  * Walks only subschema keyword positions, since `examples`/`default`/`const`/`enum` hold instance
301
331
  * data and Jx document examples legitimately contain `$ref` strings of their own.
302
332
  *
303
- * @param {Record<string, unknown>} entry - Parsed committed entry document
333
+ * @param {JsonValue} entry - Parsed committed entry document
304
334
  * @returns {{ ref: string; reason: string }[]} One entry per offending `$ref`
305
335
  */
306
- function collectUnresolvableRefs(
307
- entry: Record<string, unknown>,
308
- ): { ref: string; reason: string }[] {
336
+ function collectUnresolvableRefs(entry: JsonValue): { ref: string; reason: string }[] {
309
337
  const out: { ref: string; reason: string }[] = [];
310
- const seen = new Set<Record<string, unknown>>();
338
+ const seen = new Set<JsonObject>();
311
339
 
312
340
  const resolvePointer = (pointer: string): boolean => {
313
- let current: unknown = entry;
341
+ let current: JsonValue | undefined = entry;
314
342
  for (const raw of pointer.slice(1).split("/")) {
315
343
  if (raw === "") {
316
344
  continue;
317
345
  }
318
346
  const segment = raw.replaceAll("~1", "/").replaceAll("~0", "~");
319
- if (typeof current !== "object" || current === null) {
347
+ if (!isNonNullObject(current)) {
320
348
  return false;
321
349
  }
322
- current = (current as Record<string, unknown>)[segment];
350
+ /*
351
+ * SAFETY: `current` is a JSON object or array. Both are read by string key here, exactly as
352
+ * the original property access did (an array segment such as "0" is a string key too); the
353
+ * result is only compared with `undefined`, so JsonObject merely types the lookup.
354
+ */
355
+ current = (current as JsonObject)[segment];
323
356
  if (current === undefined) {
324
357
  return false;
325
358
  }
@@ -327,13 +360,13 @@ function collectUnresolvableRefs(
327
360
  return true;
328
361
  };
329
362
 
330
- const walk = (node: unknown): void => {
331
- if (!isRecord(node) || seen.has(node)) {
363
+ const walk = (node: JsonValue): void => {
364
+ if (!isJsonObject(node) || seen.has(node)) {
332
365
  return;
333
366
  }
334
367
  seen.add(node);
335
368
  const ref = node.$ref;
336
- if (typeof ref === "string") {
369
+ if (isString(ref)) {
337
370
  if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(ref)) {
338
371
  out.push({ reason: "is an absolute URI editors resolve over the network", ref });
339
372
  } else if (!ref.startsWith("#")) {
@@ -352,7 +385,7 @@ function collectUnresolvableRefs(
352
385
  for (const item of value) {
353
386
  walk(item);
354
387
  }
355
- } else if (isRecord(value)) {
388
+ } else if (isJsonObject(value)) {
356
389
  // Map-valued keywords hold subschemas one level down; single-valued ones are the schema.
357
390
  walk(value);
358
391
  if (MAP_SUBSCHEMA_KEYWORDS.has(keyword)) {
@@ -367,11 +400,6 @@ function collectUnresolvableRefs(
367
400
  return out;
368
401
  }
369
402
 
370
- /** True for a plain object (a candidate subschema node). */
371
- function isRecord(value: unknown): value is Record<string, unknown> {
372
- return typeof value === "object" && value !== null && !Array.isArray(value);
373
- }
374
-
375
403
  /** Keywords whose values map names to subschemas. */
376
404
  const MAP_SUBSCHEMA_KEYWORDS = new Set([
377
405
  "$defs",
@@ -446,10 +474,12 @@ export function formatProjectTreeIssues(result: ProjectTreeValidationResult): st
446
474
  for (const issue of result.issues) {
447
475
  lines.push(`${issue.file}:`);
448
476
  for (const error of issue.errors) {
449
- const { instancePath, message } = (error ?? {}) as {
450
- instancePath?: string;
451
- message?: string;
452
- };
477
+ /*
478
+ * An ajv error object or a `{ message }` record. Destructuring a primitive, an array or a
479
+ * null slot (via `{}`) yields neither member, so the fallback covers every non-object.
480
+ */
481
+ const fields: JsonObject = isJsonObject(error) ? error : {};
482
+ const { instancePath, message } = fields;
453
483
  lines.push(` - ${instancePath || "/"}: ${message ?? JSON.stringify(error)}`);
454
484
  }
455
485
  }
@@ -13,6 +13,7 @@ import { siteBasePath, withBase } from "@jxsuite/schema/asset-paths";
13
13
  import { mkdirSync, writeFileSync } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { canonicalizeLocale } from "@jxsuite/schema/locale";
16
+ import { isString } from "@jxsuite/schema/guards";
16
17
  import type { ManifestConfig, ProjectConfig, SecurityTxtConfig } from "@jxsuite/schema/types";
17
18
 
18
19
  /** The two sizes a browser wants before it will offer to install a site. */
@@ -24,6 +25,42 @@ export interface GeneratedFile {
24
25
  content: string;
25
26
  }
26
27
 
28
+ type ManifestIcon = NonNullable<ManifestConfig["icons"]>[number];
29
+
30
+ /**
31
+ * The emitted `manifest.webmanifest`, member names as the W3C Web App Manifest spells them. Members
32
+ * are assigned in this order, which is the order they serialize in.
33
+ */
34
+ interface WebAppManifest {
35
+ display: NonNullable<ManifestConfig["display"]>;
36
+ icons: ManifestIcon[];
37
+ name: string;
38
+ short_name: string;
39
+ start_url: string;
40
+ description?: string;
41
+ theme_color?: string;
42
+ background_color?: string;
43
+ scope?: string;
44
+ orientation?: string;
45
+ lang?: string;
46
+ dir?: NonNullable<ManifestConfig["dir"]>;
47
+ categories?: string[];
48
+ }
49
+
50
+ /**
51
+ * Add an optional manifest member only when the project set it, so an unset one is omitted rather
52
+ * than serialized. Members land in call order, which is the order they serialize in.
53
+ */
54
+ function setIfDefined<K extends keyof WebAppManifest>(
55
+ manifest: WebAppManifest,
56
+ key: K,
57
+ value: WebAppManifest[K] | undefined,
58
+ ): void {
59
+ if (value !== undefined) {
60
+ manifest[key] = value;
61
+ }
62
+ }
63
+
27
64
  export interface WellKnownOutput {
28
65
  files: GeneratedFile[];
29
66
  errors: string[];
@@ -56,25 +93,29 @@ export function buildManifest(projectConfig: ProjectConfig): WellKnownOutput {
56
93
  * `start_url` at the old root just opens the wrong page from the home screen.
57
94
  */
58
95
  const base = siteBasePath(projectConfig.url);
59
- const icons = [];
96
+ const icons: ManifestIcon[] = [];
60
97
  for (const icon of config.icons ?? []) {
61
- icons.push(typeof icon.src === "string" ? { ...icon, src: withBase(base, icon.src) } : icon);
98
+ icons.push(isString(icon.src) ? { ...icon, src: withBase(base, icon.src) } : icon);
62
99
  }
63
- const manifest: Record<string, unknown> = {
100
+ const manifest: WebAppManifest = {
64
101
  display: config.display ?? "standalone",
65
102
  icons,
66
103
  name,
67
104
  short_name: config.shortName ?? name,
68
105
  start_url: withBase(base, config.startUrl ?? "/"),
69
- ...(config.description === undefined ? {} : { description: config.description }),
70
- ...(config.themeColor === undefined ? {} : { theme_color: config.themeColor }),
71
- ...(config.backgroundColor === undefined ? {} : { background_color: config.backgroundColor }),
72
- ...(config.scope === undefined ? {} : { scope: withBase(base, config.scope) }),
73
- ...(config.orientation === undefined ? {} : { orientation: config.orientation }),
74
- ...(config.lang === undefined ? {} : { lang: config.lang }),
75
- ...(config.dir === undefined ? {} : { dir: config.dir }),
76
- ...(config.categories === undefined ? {} : { categories: config.categories }),
77
106
  };
107
+ setIfDefined(manifest, "description", config.description);
108
+ setIfDefined(manifest, "theme_color", config.themeColor);
109
+ setIfDefined(manifest, "background_color", config.backgroundColor);
110
+ setIfDefined(
111
+ manifest,
112
+ "scope",
113
+ config.scope === undefined ? undefined : withBase(base, config.scope),
114
+ );
115
+ setIfDefined(manifest, "orientation", config.orientation);
116
+ setIfDefined(manifest, "lang", config.lang);
117
+ setIfDefined(manifest, "dir", config.dir);
118
+ setIfDefined(manifest, "categories", config.categories);
78
119
 
79
120
  /*
80
121
  * A warning, not an error. The manifest is valid and useful without these — it still supplies
@@ -183,6 +224,10 @@ export function buildSecurityTxt(projectConfig: ProjectConfig, now: Date): WellK
183
224
  const lines: string[] = [
184
225
  "# Generated by @jxsuite/compiler — configure via securityTxt in project.json.",
185
226
  ...contact.map((value) => `Contact: ${value}`),
227
+ /*
228
+ * SAFETY: `config.expires === undefined` pushed an error above, and any error returned before
229
+ * this point, so `expires` is a string here.
230
+ */
186
231
  `Expires: ${new Date(config.expires as string).toISOString().replace(/\.\d{3}Z$/, "Z")}`,
187
232
  ...(config.encryption ?? []).map((value) => `Encryption: ${value}`),
188
233
  ...(config.acknowledgments ?? []).map((value) => `Acknowledgments: ${value}`),
@@ -215,7 +260,7 @@ export function writeWellKnown(
215
260
  files: readonly GeneratedFile[],
216
261
  outDir: string,
217
262
  exists: (path: string) => boolean,
218
- ): { written: number; skipped: string[] } {
263
+ ) {
219
264
  let written = 0;
220
265
  const skipped: string[] = [];
221
266
  for (const file of files) {
@@ -8,18 +8,19 @@
8
8
  import type {
9
9
  JxClassCtorDef,
10
10
  JxClassDef,
11
+ JxClassDefs,
11
12
  JxClassMethodDef,
12
13
  JxClassParamDef,
13
14
  } from "@jxsuite/schema/types";
15
+ import { isString } from "@jxsuite/schema/guards";
14
16
 
15
17
  /**
16
18
  * Compile a .class.json schema to a JavaScript ES module string.
17
19
  *
18
20
  * @param {JxClassDef} classDef - Parsed .class.json content (must have $prototype: "Class")
19
- * @param {Record<string, unknown>} [_opts]
20
21
  * @returns {string} JavaScript module source code
21
22
  */
22
- export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unknown> = {}) {
23
+ export function compileClassJson(classDef: JxClassDef) {
23
24
  const className = classDef.title;
24
25
  if (!className) {
25
26
  throw new Error("compileClassJson: missing title (class name)");
@@ -30,7 +31,7 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
30
31
  const fields = defs?.fields ?? {};
31
32
  // JSON objects inherit Object.prototype.constructor — only an own object value counts.
32
33
  const rawCtor = defs?.constructor;
33
- const ctor = typeof rawCtor === "object" ? rawCtor : undefined;
34
+ const ctor = isOwnCtorDef(rawCtor) ? rawCtor : undefined;
34
35
  const methods = defs?.methods ?? {};
35
36
 
36
37
  const lines: string[] = [
@@ -153,6 +154,17 @@ export function compileClassJson(classDef: JxClassDef, _opts: Record<string, unk
153
154
  return lines.join("\n");
154
155
  }
155
156
 
157
+ /**
158
+ * Whether a `$defs.constructor` read is the document's own constructor definition rather than the
159
+ * `Object.prototype.constructor` function every JSON object inherits.
160
+ *
161
+ * @param {JxClassDefs["constructor"]} value
162
+ * @returns {boolean}
163
+ */
164
+ function isOwnCtorDef(value: JxClassDefs["constructor"]): value is JxClassCtorDef {
165
+ return typeof value === "object";
166
+ }
167
+
156
168
  /**
157
169
  * Resolve the base class name from an extends value.
158
170
  *
@@ -163,7 +175,7 @@ function resolveBaseClass(ext: JxClassDef["extends"]) {
163
175
  if (!ext) {
164
176
  return "Object";
165
177
  }
166
- if (typeof ext === "string") {
178
+ if (isString(ext)) {
167
179
  return ext;
168
180
  }
169
181
  if (ext.$ref) {
@@ -186,7 +198,7 @@ function resolveParams(params: JxClassParamDef[]) {
186
198
  return param.$ref.split("/").pop();
187
199
  }
188
200
  const name = param.identifier ?? param.name;
189
- return typeof name === "string" ? name : "arg";
201
+ return isString(name) ? name : "arg";
190
202
  })
191
203
  .join(", ");
192
204
  }