@warlock.js/devtools 5.26.0 → 5.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/cjs/index.cjs +37 -2
- package/cjs/index.cjs.map +1 -1
- package/cjs/{server-DHYSVive.cjs → server-6mVWfWkG.cjs} +125 -68
- package/cjs/server-6mVWfWkG.cjs.map +1 -0
- package/esm/devtools-connector.mjs +36 -1
- package/esm/devtools-connector.mjs.map +1 -1
- package/esm/server/index.mjs +3 -1
- package/esm/server/index.mjs.map +1 -1
- package/esm/server/openapi-routes.mjs +36 -0
- package/esm/server/openapi-routes.mjs.map +1 -0
- package/esm/server/ui-assets.mjs +40 -14
- package/esm/server/ui-assets.mjs.map +1 -1
- package/esm/ui-root.mjs +1 -1
- package/llms-full.txt +13 -1
- package/llms.txt +1 -1
- package/package.json +5 -5
- package/skills/devtools-overview/SKILL.md +13 -1
- package/ui/app.css +14 -0
- package/ui/app.js +71 -1
- package/ui/docs-swagger.html +15 -0
- package/ui/docs-swagger.js +10 -0
- package/ui/docs.html +17 -0
- package/ui/index.html +1 -0
- package/ui/vendor/scalar/LICENSE.md +34 -0
- package/ui/vendor/scalar/standalone.js +2519 -0
- package/ui/vendor/swagger-ui/LICENSE +202 -0
- package/ui/vendor/swagger-ui/NOTICE +2 -0
- package/ui/vendor/swagger-ui/README.md +28 -0
- package/ui/vendor/swagger-ui/swagger-ui-bundle.js +2 -0
- package/ui/vendor/swagger-ui/swagger-ui-bundle.js.LICENSE.txt +129 -0
- package/ui/vendor/swagger-ui/swagger-ui.css +3 -0
- package/cjs/server-DHYSVive.cjs.map +0 -1
|
@@ -52,12 +52,15 @@ function devtoolsConnector(options = {}) {
|
|
|
52
52
|
await attachSource(log, "mail", () => attachMailSource(collector));
|
|
53
53
|
await attachSource(log, "log", () => attachLogSource(collector));
|
|
54
54
|
const { mountDevtoolsRoutes } = await import("./server/index.mjs");
|
|
55
|
+
const postmanBuilder = await findPostmanBuilder();
|
|
55
56
|
mountDevtoolsRoutes(getHttpServer(), {
|
|
56
57
|
collector,
|
|
57
58
|
options: resolved,
|
|
58
59
|
isDevelopment: () => Application.runtimeStrategy === "development",
|
|
59
60
|
explain: (query) => explainQuery(query),
|
|
60
|
-
listRoutes: () => listRoutes()
|
|
61
|
+
listRoutes: () => listRoutes(),
|
|
62
|
+
getOpenApiDocument: () => getOpenApiDocument(),
|
|
63
|
+
getPostmanCollection: postmanBuilder ? async () => unwrapCollection(await postmanBuilder()) : void 0
|
|
61
64
|
});
|
|
62
65
|
disposers.push(collector.subscribe((event) => {
|
|
63
66
|
if (event.type !== "request" || event.data.warningCount === 0) return;
|
|
@@ -106,6 +109,38 @@ async function listRoutes() {
|
|
|
106
109
|
middleware: route.middleware?.map((middleware) => middleware.name || "anonymous")
|
|
107
110
|
}));
|
|
108
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Builds the OpenAPI 3.1 document from the routes registered in this process —
|
|
114
|
+
* the same generator behind `warlock generate.openapi`. Core is imported
|
|
115
|
+
* lazily so this module stays free of a load-time dependency on it.
|
|
116
|
+
*/
|
|
117
|
+
async function getOpenApiDocument() {
|
|
118
|
+
const { getDevelopmentOpenApiDocument } = await import("@warlock.js/core");
|
|
119
|
+
const { document } = await getDevelopmentOpenApiDocument();
|
|
120
|
+
return document;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.
|
|
124
|
+
* Older cores do not export it; the route then answers 503. The lookup is
|
|
125
|
+
* guarded because a namespace without the export may throw on access. Core is
|
|
126
|
+
* imported lazily, like the OpenAPI document above.
|
|
127
|
+
*/
|
|
128
|
+
async function findPostmanBuilder() {
|
|
129
|
+
try {
|
|
130
|
+
const build = (await import("@warlock.js/core")).getDevelopmentPostmanCollection;
|
|
131
|
+
return typeof build === "function" ? build : void 0;
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Core may return the collection itself or wrap it in `{ collection, warnings }`,
|
|
138
|
+
* like the OpenAPI generator's `{ document, warnings }`.
|
|
139
|
+
*/
|
|
140
|
+
function unwrapCollection(result) {
|
|
141
|
+
if (result && typeof result === "object" && "collection" in result) return result.collection;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
109
144
|
|
|
110
145
|
//#endregion
|
|
111
146
|
export { devtoolsConnector };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-connector.mjs","names":[],"sources":["../../../../../../devtools/src/devtools-connector.ts"],"sourcesContent":["/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAa,8BAA8B;;;;;;;;;;;;;;;;;AAqB3C,SAAgB,kBAAkB,UAAoC,CAAC,GAAc;CACnF,IAAI,SAAS;CACb,MAAM,YAAgC,CAAC;CAEvC,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;EAChB,MAAM,OAAO;GACX,MAAM,EAAE,aAAa,kBAAkB,MAAM,OAAO;GACpD,MAAM,EAAE,QAAQ,MAAM,OAAO;GAE7B,IAAI,YAAY,oBAAoB,eAAe;IACjD,IAAI,KACF,YACA,YACA,kEACF;IACA;GACF;GAEA,MAAM,WAAW,uBAAuB,OAAO;GAC/C,MAAM,YAAY,gBAAgB,QAAQ;GAE1C,MAAM,aAAa,KAAK,iBAAiB,oBAAoB,WAAW,QAAQ,CAAC;GACjF,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,cAAc,iBAAiB,SAAS,CAAC;GACjE,MAAM,aAAa,KAAK,aAAa,gBAAgB,SAAS,CAAC;GAE/D,MAAM,EAAE,wBAAwB,MAAM,OAAO;GAE7C,oBAAoB,cAAc,GAAG;IACnC;IACA,SAAS;IACT,qBAAqB,YAAY,oBAAoB;IACrD,UAAU,UAAU,aAAa,KAAK;IACtC,kBAAkB,WAAW;GAC/B,CAAC;GAKD,UAAU,KACR,UAAU,WAAW,UAAU;IAC7B,IAAI,MAAM,SAAS,aAAa,MAAM,KAAK,iBAAiB,GAAG;IAE/D,KAAK,MAAM,WAAW,UAAU,WAAW,MAAM,KAAK,EAAE,CAAC,EAAE,YAAY,CAAC,GAAG;KACzE,MAAM,QAAQ,QAAQ,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG,QAAQ,OAAO,SAAS;KAEpF,IAAI,KACF,YACA,OACA,GAAG,QAAQ,UAAU,MAAM,IAAI,MAAM,KAAK,OAAO,GAAG,MAAM,KAAK,KAAK,EACtE;IACF;GACF,CAAC,CACH;GAEA,SAAS;GAET,IAAI,KAAK,YAAY,SAAS,qBAAqB,SAAS,MAAM;EACpE;EACA,MAAM,QAAQ,CAAC;EACf,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACnD,SAAS;EACX;EACA,qBAAqB;CACvB;;CAGA,eAAe,aACb,QACA,MACA,QACe;EACf,IAAI;GACF,UAAU,KAAK,MAAM,OAAO,CAAC;EAC/B,SAAS,OAAO;GACd,OAAO,KACL,YACA,iBACA,aAAa,KAAK,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrG;EACF;CACF;CAEA,OAAO;AACT;;AAGA,eAAe,aAAa,OAAwC;CAClE,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,EAAE,SAAS,MADE,mBAAmB,IAAI,MAAM,UAChB,CAAC,CAAC,IAAI,yBAAyB,MAAM,OAAO,MAAM,QAAQ;CAE1F,OAAO;AACT;;AAGA,eAAe,aAA0C;CACvD,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,WAAW;EACnC,QAAQ,MAAM,OAAO,YAAY;EACjC,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,YAAY,MAAM,YAAY,KAAK,eAAe,WAAW,QAAQ,WAAW;CAClF,EAAE;AACJ"}
|
|
1
|
+
{"version":3,"file":"devtools-connector.mjs","names":[],"sources":["../../../../../../devtools/src/devtools-connector.ts"],"sourcesContent":["/**\n * The devtools connector for `warlock.config.ts > connectors`.\n *\n * Deliberately a plain object with TYPE-ONLY imports from core: the config\n * file that constructs it must not drag core's runtime graph in at\n * config-load time. Core is imported lazily inside `boot()`, where the app\n * has already loaded it — see `queue/src/queue-connector.ts`, the pattern\n * this mirrors.\n */\nimport type { Connector, ConnectorLifecyclePhase } from \"@warlock.js/core\";\nimport { createCollector } from \"./collector\";\nimport { resolveDevtoolsOptions } from \"./options\";\nimport type { DevtoolsRouteRow } from \"./server\";\nimport { attachCacheSource } from \"./sources/cache-source\";\nimport { attachLogSource } from \"./sources/log-source\";\nimport { attachMailSource } from \"./sources/mail-source\";\nimport { attachQuerySource } from \"./sources/query-source\";\nimport { attachTracingSource } from \"./sources/tracing-source\";\nimport type { DevtoolsDisposer, DevtoolsOptions, DevtoolsQuery } from \"./types\";\n\n/**\n * Boots after the queue connector (priority 11), which is the last\n * built-in/framework connector to claim a priority; devtools has nothing\n * that depends on it, so it simply goes last.\n */\nexport const DEVTOOLS_CONNECTOR_PRIORITY = 12;\n\n/** Options accepted by `devtoolsConnector()`. */\nexport type DevtoolsConnectorOptions = DevtoolsOptions;\n\n/**\n * Construct the devtools connector.\n *\n * Refuses to run outside development: `boot()` checks\n * `Application.runtimeStrategy` and, when it isn't `\"development\"`, logs one\n * warning and returns without mounting a route or subscribing to anything.\n * In development it attaches every source (tracing, queries, cache, mail,\n * logs), then mounts the dashboard's routes on the HTTP server — before it\n * starts listening, the only point in the boot sequence where mounting is\n * possible (see `queue/src/dashboard-boot.ts` for the same constraint).\n *\n * Apps don't call this: `warlock dev` registers it automatically when the\n * package is installed (`warlock add devtools`). Keep it OUT of\n * `warlock.config.ts` — devtools is a dev dependency, so a production\n * install doesn't have it and a config import of it would fail there.\n */\nexport function devtoolsConnector(options: DevtoolsConnectorOptions = {}): Connector {\n let active = false;\n const disposers: DevtoolsDisposer[] = [];\n\n const connector: Connector = {\n name: \"devtools\",\n priority: DEVTOOLS_CONNECTOR_PRIORITY,\n // Core's `ConnectorLifecyclePhase.Late`; spelled out so this module stays\n // free of a runtime import of core.\n lifecyclePhase: \"late\" as ConnectorLifecyclePhase,\n isActive: () => active,\n async boot() {\n const { Application, getHttpServer } = await import(\"@warlock.js/core\");\n const { log } = await import(\"@warlock.js/logger\");\n\n if (Application.runtimeStrategy !== \"development\") {\n log.warn(\n \"devtools\",\n \"disabled\",\n \"@warlock.js/devtools only runs in development; it is not mounted\",\n );\n return;\n }\n\n const resolved = resolveDevtoolsOptions(options);\n const collector = createCollector(resolved);\n\n await attachSource(log, \"tracing\", () => attachTracingSource(collector, resolved));\n await attachSource(log, \"query\", () => attachQuerySource(collector));\n await attachSource(log, \"cache\", () => attachCacheSource(collector));\n await attachSource(log, \"mail\", () => attachMailSource(collector));\n await attachSource(log, \"log\", () => attachLogSource(collector));\n\n const { mountDevtoolsRoutes } = await import(\"./server\");\n const postmanBuilder = await findPostmanBuilder();\n\n mountDevtoolsRoutes(getHttpServer(), {\n collector,\n options: resolved,\n isDevelopment: () => Application.runtimeStrategy === \"development\",\n explain: (query) => explainQuery(query),\n listRoutes: () => listRoutes(),\n getOpenApiDocument: () => getOpenApiDocument(),\n getPostmanCollection: postmanBuilder\n ? async () => unwrapCollection(await postmanBuilder())\n : undefined,\n });\n\n // An N+1 is worth a terminal line, not just a badge the developer has\n // to go looking for: one warning per offending query shape, with the\n // line that issued it.\n disposers.push(\n collector.subscribe((event) => {\n if (event.type !== \"request\" || event.data.warningCount === 0) return;\n\n for (const warning of collector.getRequest(event.data.id)?.warnings ?? []) {\n const where = warning.caller ? ` — ${warning.caller.file}:${warning.caller.line}` : \"\";\n\n log.warn(\n \"devtools\",\n \"n+1\",\n `${warning.message}${where} (${event.data.method} ${event.data.path})`,\n );\n }\n }),\n );\n\n active = true;\n\n log.info(\"devtools\", \"ready\", `devtools ready at ${resolved.path}`);\n },\n async start() {},\n async restart() {\n await connector.shutdown();\n await connector.start();\n },\n async shutdown() {\n for (const dispose of disposers.splice(0)) dispose();\n active = false;\n },\n shouldRestart: () => false,\n };\n\n /** Attaches one source, logging and continuing when it fails to attach. */\n async function attachSource(\n logger: { warn: (module: string, action: string, message: string) => unknown },\n name: string,\n attach: () => Promise<DevtoolsDisposer>,\n ): Promise<void> {\n try {\n disposers.push(await attach());\n } catch (error) {\n logger.warn(\n \"devtools\",\n \"source-failed\",\n `devtools' ${name} source failed to attach: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return connector;\n}\n\n/** Runs `EXPLAIN (FORMAT JSON)` for a stored Postgres query on its own connection. */\nasync function explainQuery(query: DevtoolsQuery): Promise<unknown> {\n const { dataSourceRegistry } = await import(\"@warlock.js/cascade\");\n const dataSource = dataSourceRegistry.get(query.connection);\n const { rows } = await dataSource.raw(`EXPLAIN (FORMAT JSON) ${query.sql}`, query.bindings);\n\n return rows;\n}\n\n/** Lists every registered HTTP route — the same source `warlock routes --json` reads. */\nasync function listRoutes(): Promise<DevtoolsRouteRow[]> {\n const { router } = await import(\"@warlock.js/core\");\n\n return router.list().map((route) => ({\n method: route.method.toUpperCase(),\n path: route.path,\n name: route.name,\n middleware: route.middleware?.map((middleware) => middleware.name || \"anonymous\"),\n }));\n}\n\n/**\n * Builds the OpenAPI 3.1 document from the routes registered in this process —\n * the same generator behind `warlock generate.openapi`. Core is imported\n * lazily so this module stays free of a load-time dependency on it.\n */\nasync function getOpenApiDocument(): Promise<object> {\n const { getDevelopmentOpenApiDocument } = await import(\"@warlock.js/core\");\n const { document } = await getDevelopmentOpenApiDocument();\n\n return document;\n}\n\ntype PostmanBuilder = () => Promise<unknown>;\n\n/**\n * Looks up core's Postman collection builder, `getDevelopmentPostmanCollection`.\n * Older cores do not export it; the route then answers 503. The lookup is\n * guarded because a namespace without the export may throw on access. Core is\n * imported lazily, like the OpenAPI document above.\n */\nasync function findPostmanBuilder(): Promise<PostmanBuilder | undefined> {\n try {\n const core: Record<string, unknown> = await import(\"@warlock.js/core\");\n const build = core.getDevelopmentPostmanCollection;\n\n return typeof build === \"function\" ? (build as PostmanBuilder) : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Core may return the collection itself or wrap it in `{ collection, warnings }`,\n * like the OpenAPI generator's `{ document, warnings }`.\n */\nfunction unwrapCollection(result: unknown): object {\n if (result && typeof result === \"object\" && \"collection\" in result) {\n return (result as { collection: object }).collection;\n }\n\n return result as object;\n}\n"],"mappings":";;;;;;;;;;;;;;AAyBA,MAAa,8BAA8B;;;;;;;;;;;;;;;;;AAqB3C,SAAgB,kBAAkB,UAAoC,CAAC,GAAc;CACnF,IAAI,SAAS;CACb,MAAM,YAAgC,CAAC;CAEvC,MAAM,YAAuB;EAC3B,MAAM;EACN;EAGA,gBAAgB;EAChB,gBAAgB;EAChB,MAAM,OAAO;GACX,MAAM,EAAE,aAAa,kBAAkB,MAAM,OAAO;GACpD,MAAM,EAAE,QAAQ,MAAM,OAAO;GAE7B,IAAI,YAAY,oBAAoB,eAAe;IACjD,IAAI,KACF,YACA,YACA,kEACF;IACA;GACF;GAEA,MAAM,WAAW,uBAAuB,OAAO;GAC/C,MAAM,YAAY,gBAAgB,QAAQ;GAE1C,MAAM,aAAa,KAAK,iBAAiB,oBAAoB,WAAW,QAAQ,CAAC;GACjF,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,eAAe,kBAAkB,SAAS,CAAC;GACnE,MAAM,aAAa,KAAK,cAAc,iBAAiB,SAAS,CAAC;GACjE,MAAM,aAAa,KAAK,aAAa,gBAAgB,SAAS,CAAC;GAE/D,MAAM,EAAE,wBAAwB,MAAM,OAAO;GAC7C,MAAM,iBAAiB,MAAM,mBAAmB;GAEhD,oBAAoB,cAAc,GAAG;IACnC;IACA,SAAS;IACT,qBAAqB,YAAY,oBAAoB;IACrD,UAAU,UAAU,aAAa,KAAK;IACtC,kBAAkB,WAAW;IAC7B,0BAA0B,mBAAmB;IAC7C,sBAAsB,iBAClB,YAAY,iBAAiB,MAAM,eAAe,CAAC,IACnD;GACN,CAAC;GAKD,UAAU,KACR,UAAU,WAAW,UAAU;IAC7B,IAAI,MAAM,SAAS,aAAa,MAAM,KAAK,iBAAiB,GAAG;IAE/D,KAAK,MAAM,WAAW,UAAU,WAAW,MAAM,KAAK,EAAE,CAAC,EAAE,YAAY,CAAC,GAAG;KACzE,MAAM,QAAQ,QAAQ,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG,QAAQ,OAAO,SAAS;KAEpF,IAAI,KACF,YACA,OACA,GAAG,QAAQ,UAAU,MAAM,IAAI,MAAM,KAAK,OAAO,GAAG,MAAM,KAAK,KAAK,EACtE;IACF;GACF,CAAC,CACH;GAEA,SAAS;GAET,IAAI,KAAK,YAAY,SAAS,qBAAqB,SAAS,MAAM;EACpE;EACA,MAAM,QAAQ,CAAC;EACf,MAAM,UAAU;GACd,MAAM,UAAU,SAAS;GACzB,MAAM,UAAU,MAAM;EACxB;EACA,MAAM,WAAW;GACf,KAAK,MAAM,WAAW,UAAU,OAAO,CAAC,GAAG,QAAQ;GACnD,SAAS;EACX;EACA,qBAAqB;CACvB;;CAGA,eAAe,aACb,QACA,MACA,QACe;EACf,IAAI;GACF,UAAU,KAAK,MAAM,OAAO,CAAC;EAC/B,SAAS,OAAO;GACd,OAAO,KACL,YACA,iBACA,aAAa,KAAK,4BAA4B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GACrG;EACF;CACF;CAEA,OAAO;AACT;;AAGA,eAAe,aAAa,OAAwC;CAClE,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,EAAE,SAAS,MADE,mBAAmB,IAAI,MAAM,UAChB,CAAC,CAAC,IAAI,yBAAyB,MAAM,OAAO,MAAM,QAAQ;CAE1F,OAAO;AACT;;AAGA,eAAe,aAA0C;CACvD,MAAM,EAAE,WAAW,MAAM,OAAO;CAEhC,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,WAAW;EACnC,QAAQ,MAAM,OAAO,YAAY;EACjC,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,YAAY,MAAM,YAAY,KAAK,eAAe,WAAW,QAAQ,WAAW;CAClF,EAAE;AACJ;;;;;;AAOA,eAAe,qBAAsC;CACnD,MAAM,EAAE,kCAAkC,MAAM,OAAO;CACvD,MAAM,EAAE,aAAa,MAAM,8BAA8B;CAEzD,OAAO;AACT;;;;;;;AAUA,eAAe,qBAA0D;CACvE,IAAI;EAEF,MAAM,SAAQ,MAD8B,OAAO,oBACjC,CAAC;EAEnB,OAAO,OAAO,UAAU,aAAc,QAA2B;CACnE,QAAQ;EACN;CACF;AACF;;;;;AAMA,SAAS,iBAAiB,QAAyB;CACjD,IAAI,UAAU,OAAO,WAAW,YAAY,gBAAgB,QAC1D,OAAQ,OAAkC;CAG5C,OAAO;AACT"}
|
package/esm/server/index.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import { registerExplainRoute } from "./explain-route.mjs";
|
|
|
3
3
|
import { registerDevtoolsGuard } from "./guard.mjs";
|
|
4
4
|
import { registerLogRoutes } from "./log-routes.mjs";
|
|
5
5
|
import { registerMailRoutes } from "./mail-routes.mjs";
|
|
6
|
+
import { registerUiAssets } from "./ui-assets.mjs";
|
|
7
|
+
import { registerOpenApiRoutes } from "./openapi-routes.mjs";
|
|
6
8
|
import { registerRequestsRoutes } from "./requests-routes.mjs";
|
|
7
9
|
import { registerRouteRoutes } from "./route-routes.mjs";
|
|
8
10
|
import { registerStreamRoute } from "./stream-route.mjs";
|
|
9
|
-
import { registerUiAssets } from "./ui-assets.mjs";
|
|
10
11
|
|
|
11
12
|
//#region ../devtools/src/server/index.ts
|
|
12
13
|
/**
|
|
@@ -27,6 +28,7 @@ function mountDevtoolsRoutes(server, deps) {
|
|
|
27
28
|
registerMailRoutes(instance, deps);
|
|
28
29
|
registerLogRoutes(instance, deps);
|
|
29
30
|
registerRouteRoutes(instance, deps);
|
|
31
|
+
registerOpenApiRoutes(instance, deps);
|
|
30
32
|
registerExplainRoute(instance, deps);
|
|
31
33
|
registerStreamRoute(instance, deps);
|
|
32
34
|
}, { prefix: deps.options.path });
|
package/esm/server/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../devtools/src/server/index.ts"],"sourcesContent":["import type { FastifyError, FastifyInstance } from \"fastify\";\nimport { registerBackgroundRoutes } from \"./background-routes\";\nimport { registerExplainRoute } from \"./explain-route\";\nimport { registerDevtoolsGuard } from \"./guard\";\nimport { registerLogRoutes } from \"./log-routes\";\nimport { registerMailRoutes } from \"./mail-routes\";\nimport { registerRequestsRoutes } from \"./requests-routes\";\nimport { registerRouteRoutes } from \"./route-routes\";\nimport { registerStreamRoute } from \"./stream-route\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { registerUiAssets } from \"./ui-assets\";\n\nexport type { DevtoolsRouteRow, MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * Mounts the devtools dashboard and its API under `deps.options.path` on\n * `server`. Every route is registered inside a single encapsulated Fastify\n * context so the dev-only, loopback-only guard never reaches the app's\n * other routes.\n */\nexport function mountDevtoolsRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.register(\n async (instance) => {\n registerDevtoolsGuard(instance, deps);\n\n instance.setErrorHandler((error: FastifyError, _request, reply) => {\n reply.code(error.statusCode ?? 500).send({ error: error.message });\n });\n\n registerUiAssets(instance);\n registerRequestsRoutes(instance, deps);\n registerBackgroundRoutes(instance, deps);\n registerMailRoutes(instance, deps);\n registerLogRoutes(instance, deps);\n registerRouteRoutes(instance, deps);\n registerExplainRoute(instance, deps);\n registerStreamRoute(instance, deps);\n },\n { prefix: deps.options.path },\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../devtools/src/server/index.ts"],"sourcesContent":["import type { FastifyError, FastifyInstance } from \"fastify\";\nimport { registerBackgroundRoutes } from \"./background-routes\";\nimport { registerExplainRoute } from \"./explain-route\";\nimport { registerDevtoolsGuard } from \"./guard\";\nimport { registerLogRoutes } from \"./log-routes\";\nimport { registerMailRoutes } from \"./mail-routes\";\nimport { registerOpenApiRoutes } from \"./openapi-routes\";\nimport { registerRequestsRoutes } from \"./requests-routes\";\nimport { registerRouteRoutes } from \"./route-routes\";\nimport { registerStreamRoute } from \"./stream-route\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { registerUiAssets } from \"./ui-assets\";\n\nexport type { DevtoolsRouteRow, MountDevtoolsRoutesDeps } from \"./types\";\n\n/**\n * Mounts the devtools dashboard and its API under `deps.options.path` on\n * `server`. Every route is registered inside a single encapsulated Fastify\n * context so the dev-only, loopback-only guard never reaches the app's\n * other routes.\n */\nexport function mountDevtoolsRoutes(server: FastifyInstance, deps: MountDevtoolsRoutesDeps): void {\n server.register(\n async (instance) => {\n registerDevtoolsGuard(instance, deps);\n\n instance.setErrorHandler((error: FastifyError, _request, reply) => {\n reply.code(error.statusCode ?? 500).send({ error: error.message });\n });\n\n registerUiAssets(instance);\n registerRequestsRoutes(instance, deps);\n registerBackgroundRoutes(instance, deps);\n registerMailRoutes(instance, deps);\n registerLogRoutes(instance, deps);\n registerRouteRoutes(instance, deps);\n registerOpenApiRoutes(instance, deps);\n registerExplainRoute(instance, deps);\n registerStreamRoute(instance, deps);\n },\n { prefix: deps.options.path },\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAqBA,SAAgB,oBAAoB,QAAyB,MAAqC;CAChG,OAAO,SACL,OAAO,aAAa;EAClB,sBAAsB,UAAU,IAAI;EAEpC,SAAS,iBAAiB,OAAqB,UAAU,UAAU;GACjE,MAAM,KAAK,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,EAAE,OAAO,MAAM,QAAQ,CAAC;EACnE,CAAC;EAED,iBAAiB,QAAQ;EACzB,uBAAuB,UAAU,IAAI;EACrC,yBAAyB,UAAU,IAAI;EACvC,mBAAmB,UAAU,IAAI;EACjC,kBAAkB,UAAU,IAAI;EAChC,oBAAoB,UAAU,IAAI;EAClC,sBAAsB,UAAU,IAAI;EACpC,qBAAqB,UAAU,IAAI;EACnC,oBAAoB,UAAU,IAAI;CACpC,GACA,EAAE,QAAQ,KAAK,QAAQ,KAAK,CAC9B;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveUiRoot } from "../ui-root.mjs";
|
|
2
|
+
import { sendFile } from "./ui-assets.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region ../devtools/src/server/openapi-routes.ts
|
|
6
|
+
/**
|
|
7
|
+
* Registers the API docs: `GET /api/openapi.json` returns the document core
|
|
8
|
+
* builds from the registered routes (`deps.getOpenApiDocument`), and
|
|
9
|
+
* `GET /docs` serves the page that renders it with Scalar and
|
|
10
|
+
* `GET /docs/swagger` the same document in Swagger UI;
|
|
11
|
+
* `GET /api/postman.json` returns the Postman collection core builds
|
|
12
|
+
* (`deps.getPostmanCollection`). The pages use relative URLs, so it works under whatever base path the dashboard is
|
|
13
|
+
* mounted on.
|
|
14
|
+
*/
|
|
15
|
+
function registerOpenApiRoutes(server, deps) {
|
|
16
|
+
server.get("/api/openapi.json", async (_request, reply) => {
|
|
17
|
+
if (!deps.getOpenApiDocument) return reply.code(503).header("Cache-Control", "no-store").send({ error: "The OpenAPI document is not available: the host did not provide a generator." });
|
|
18
|
+
reply.header("Cache-Control", "no-store");
|
|
19
|
+
return deps.getOpenApiDocument();
|
|
20
|
+
});
|
|
21
|
+
server.get("/api/postman.json", async (_request, reply) => {
|
|
22
|
+
if (!deps.getPostmanCollection) return reply.code(503).header("Cache-Control", "no-store").send({ error: "The Postman collection is not available: the host did not provide a generator." });
|
|
23
|
+
reply.header("Cache-Control", "no-store");
|
|
24
|
+
return deps.getPostmanCollection();
|
|
25
|
+
});
|
|
26
|
+
server.get("/docs", (_request, reply) => {
|
|
27
|
+
sendFile(reply, path.join(resolveUiRoot(), "docs.html"), "text/html; charset=utf-8");
|
|
28
|
+
});
|
|
29
|
+
server.get("/docs/swagger", (_request, reply) => {
|
|
30
|
+
sendFile(reply, path.join(resolveUiRoot(), "docs-swagger.html"), "text/html; charset=utf-8");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
export { registerOpenApiRoutes };
|
|
36
|
+
//# sourceMappingURL=openapi-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi-routes.mjs","names":[],"sources":["../../../../../../../devtools/src/server/openapi-routes.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { FastifyInstance } from \"fastify\";\nimport { resolveUiRoot } from \"../ui-root\";\nimport type { MountDevtoolsRoutesDeps } from \"./types\";\nimport { sendFile } from \"./ui-assets\";\n\n/**\n * Registers the API docs: `GET /api/openapi.json` returns the document core\n * builds from the registered routes (`deps.getOpenApiDocument`), and\n * `GET /docs` serves the page that renders it with Scalar and\n * `GET /docs/swagger` the same document in Swagger UI;\n * `GET /api/postman.json` returns the Postman collection core builds\n * (`deps.getPostmanCollection`). The pages use relative URLs, so it works under whatever base path the dashboard is\n * mounted on.\n */\nexport function registerOpenApiRoutes(\n server: FastifyInstance,\n deps: MountDevtoolsRoutesDeps,\n): void {\n server.get(\"/api/openapi.json\", async (_request, reply) => {\n if (!deps.getOpenApiDocument) {\n return reply.code(503).header(\"Cache-Control\", \"no-store\").send({\n error: \"The OpenAPI document is not available: the host did not provide a generator.\",\n });\n }\n\n reply.header(\"Cache-Control\", \"no-store\");\n\n return deps.getOpenApiDocument();\n });\n\n server.get(\"/api/postman.json\", async (_request, reply) => {\n if (!deps.getPostmanCollection) {\n return reply.code(503).header(\"Cache-Control\", \"no-store\").send({\n error: \"The Postman collection is not available: the host did not provide a generator.\",\n });\n }\n\n reply.header(\"Cache-Control\", \"no-store\");\n\n return deps.getPostmanCollection();\n });\n\n server.get(\"/docs\", (_request, reply) => {\n sendFile(reply, path.join(resolveUiRoot(), \"docs.html\"), \"text/html; charset=utf-8\");\n });\n\n server.get(\"/docs/swagger\", (_request, reply) => {\n sendFile(reply, path.join(resolveUiRoot(), \"docs-swagger.html\"), \"text/html; charset=utf-8\");\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAAgB,sBACd,QACA,MACM;CACN,OAAO,IAAI,qBAAqB,OAAO,UAAU,UAAU;EACzD,IAAI,CAAC,KAAK,oBACR,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,iBAAiB,UAAU,CAAC,CAAC,KAAK,EAC9D,OAAO,+EACT,CAAC;EAGH,MAAM,OAAO,iBAAiB,UAAU;EAExC,OAAO,KAAK,mBAAmB;CACjC,CAAC;CAED,OAAO,IAAI,qBAAqB,OAAO,UAAU,UAAU;EACzD,IAAI,CAAC,KAAK,sBACR,OAAO,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,iBAAiB,UAAU,CAAC,CAAC,KAAK,EAC9D,OAAO,iFACT,CAAC;EAGH,MAAM,OAAO,iBAAiB,UAAU;EAExC,OAAO,KAAK,qBAAqB;CACnC,CAAC;CAED,OAAO,IAAI,UAAU,UAAU,UAAU;EACvC,SAAS,OAAO,KAAK,KAAK,cAAc,GAAG,WAAW,GAAG,0BAA0B;CACrF,CAAC;CAED,OAAO,IAAI,kBAAkB,UAAU,UAAU;EAC/C,SAAS,OAAO,KAAK,KAAK,cAAc,GAAG,mBAAmB,GAAG,0BAA0B;CAC7F,CAAC;AACH"}
|
package/esm/server/ui-assets.mjs
CHANGED
|
@@ -1,38 +1,64 @@
|
|
|
1
1
|
import { resolveUiRoot } from "../ui-root.mjs";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
//#region ../devtools/src/server/ui-assets.ts
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const uiFile = (name) => () => path.join(resolveUiRoot(), name);
|
|
7
|
+
/**
|
|
8
|
+
* The only assets that can be requested. The request's asset name is looked
|
|
9
|
+
* up here and never joined into a filesystem path.
|
|
10
|
+
*/
|
|
11
|
+
const UI_ASSETS = {
|
|
12
|
+
"app.js": {
|
|
13
|
+
contentType: "text/javascript; charset=utf-8",
|
|
14
|
+
file: uiFile("app.js")
|
|
15
|
+
},
|
|
16
|
+
"app.css": {
|
|
17
|
+
contentType: "text/css; charset=utf-8",
|
|
18
|
+
file: uiFile("app.css")
|
|
19
|
+
},
|
|
20
|
+
"scalar.js": {
|
|
21
|
+
contentType: "text/javascript; charset=utf-8",
|
|
22
|
+
file: uiFile("vendor/scalar/standalone.js")
|
|
23
|
+
},
|
|
24
|
+
"swagger-ui-bundle.js": {
|
|
25
|
+
contentType: "text/javascript; charset=utf-8",
|
|
26
|
+
file: uiFile("vendor/swagger-ui/swagger-ui-bundle.js")
|
|
27
|
+
},
|
|
28
|
+
"swagger-ui.css": {
|
|
29
|
+
contentType: "text/css; charset=utf-8",
|
|
30
|
+
file: uiFile("vendor/swagger-ui/swagger-ui.css")
|
|
31
|
+
},
|
|
32
|
+
"docs-swagger.js": {
|
|
33
|
+
contentType: "text/javascript; charset=utf-8",
|
|
34
|
+
file: uiFile("docs-swagger.js")
|
|
35
|
+
}
|
|
9
36
|
};
|
|
37
|
+
/** Sends `file` with `contentType` and no caching. */
|
|
10
38
|
function sendFile(reply, file, contentType) {
|
|
11
39
|
reply.header("Cache-Control", "no-store").header("Content-Type", contentType).send(readFileSync(file));
|
|
12
40
|
}
|
|
13
41
|
/**
|
|
14
42
|
* Serves the static dashboard: `index.html` at the dashboard root and the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
43
|
+
* whitelisted assets (`app.js`, `app.css`, the vendored Scalar bundle as
|
|
44
|
+
* `scalar.js`, and the vendored Swagger UI files plus their init script). The asset name is matched against a fixed whitelist — never
|
|
45
|
+
* joined into a filesystem path — so no request can traverse outside it.
|
|
18
46
|
*/
|
|
19
47
|
function registerUiAssets(server) {
|
|
20
48
|
const serveIndex = (_request, reply) => {
|
|
21
|
-
|
|
22
|
-
sendFile(reply, path.join(uiRoot, "index.html"), "text/html; charset=utf-8");
|
|
49
|
+
sendFile(reply, path.join(resolveUiRoot(), "index.html"), "text/html; charset=utf-8");
|
|
23
50
|
};
|
|
24
51
|
server.get("/", serveIndex);
|
|
25
52
|
server.get("/assets/:name", (request, reply) => {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
53
|
+
const asset = Object.hasOwn(UI_ASSETS, request.params.name) ? UI_ASSETS[request.params.name] : void 0;
|
|
54
|
+
if (!asset) {
|
|
28
55
|
reply.code(404).header("Cache-Control", "no-store").send();
|
|
29
56
|
return;
|
|
30
57
|
}
|
|
31
|
-
|
|
32
|
-
sendFile(reply, path.join(uiRoot, request.params.name), contentType);
|
|
58
|
+
sendFile(reply, asset.file(), asset.contentType);
|
|
33
59
|
});
|
|
34
60
|
}
|
|
35
61
|
|
|
36
62
|
//#endregion
|
|
37
|
-
export { registerUiAssets };
|
|
63
|
+
export { registerUiAssets, sendFile };
|
|
38
64
|
//# sourceMappingURL=ui-assets.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-assets.mjs","names":[],"sources":["../../../../../../../devtools/src/server/ui-assets.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { FastifyInstance, FastifyReply } from \"fastify\";\nimport { resolveUiRoot } from \"../ui-root\";\n\nconst
|
|
1
|
+
{"version":3,"file":"ui-assets.mjs","names":[],"sources":["../../../../../../../devtools/src/server/ui-assets.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { FastifyInstance, FastifyReply } from \"fastify\";\nimport { resolveUiRoot } from \"../ui-root\";\n\n/**\n * Where an asset's bytes come from: a file in this package's `ui/` folder.\n */\ntype UiAsset = {\n contentType: string;\n file: () => string;\n};\n\nconst uiFile = (name: string) => () => path.join(resolveUiRoot(), name);\n\n/**\n * The only assets that can be requested. The request's asset name is looked\n * up here and never joined into a filesystem path.\n */\nconst UI_ASSETS: Record<string, UiAsset> = {\n \"app.js\": { contentType: \"text/javascript; charset=utf-8\", file: uiFile(\"app.js\") },\n \"app.css\": { contentType: \"text/css; charset=utf-8\", file: uiFile(\"app.css\") },\n \"scalar.js\": {\n contentType: \"text/javascript; charset=utf-8\",\n file: uiFile(\"vendor/scalar/standalone.js\"),\n },\n \"swagger-ui-bundle.js\": {\n contentType: \"text/javascript; charset=utf-8\",\n file: uiFile(\"vendor/swagger-ui/swagger-ui-bundle.js\"),\n },\n \"swagger-ui.css\": {\n contentType: \"text/css; charset=utf-8\",\n file: uiFile(\"vendor/swagger-ui/swagger-ui.css\"),\n },\n \"docs-swagger.js\": {\n contentType: \"text/javascript; charset=utf-8\",\n file: uiFile(\"docs-swagger.js\"),\n },\n};\n\n/** Sends `file` with `contentType` and no caching. */\nexport function sendFile(reply: FastifyReply, file: string, contentType: string): void {\n reply\n .header(\"Cache-Control\", \"no-store\")\n .header(\"Content-Type\", contentType)\n .send(readFileSync(file));\n}\n\n/**\n * Serves the static dashboard: `index.html` at the dashboard root and the\n * whitelisted assets (`app.js`, `app.css`, the vendored Scalar bundle as\n * `scalar.js`, and the vendored Swagger UI files plus their init script). The asset name is matched against a fixed whitelist — never\n * joined into a filesystem path — so no request can traverse outside it.\n */\nexport function registerUiAssets(server: FastifyInstance): void {\n const serveIndex = (_request: unknown, reply: FastifyReply): void => {\n sendFile(reply, path.join(resolveUiRoot(), \"index.html\"), \"text/html; charset=utf-8\");\n };\n\n server.get(\"/\", serveIndex);\n\n server.get<{ Params: { name: string } }>(\"/assets/:name\", (request, reply) => {\n const asset = Object.hasOwn(UI_ASSETS, request.params.name)\n ? UI_ASSETS[request.params.name]\n : undefined;\n\n if (!asset) {\n reply.code(404).header(\"Cache-Control\", \"no-store\").send();\n return;\n }\n\n sendFile(reply, asset.file(), asset.contentType);\n });\n}\n"],"mappings":";;;;;AAaA,MAAM,UAAU,eAAuB,KAAK,KAAK,cAAc,GAAG,IAAI;;;;;AAMtE,MAAM,YAAqC;CACzC,UAAU;EAAE,aAAa;EAAkC,MAAM,OAAO,QAAQ;CAAE;CAClF,WAAW;EAAE,aAAa;EAA2B,MAAM,OAAO,SAAS;CAAE;CAC7E,aAAa;EACX,aAAa;EACb,MAAM,OAAO,6BAA6B;CAC5C;CACA,wBAAwB;EACtB,aAAa;EACb,MAAM,OAAO,wCAAwC;CACvD;CACA,kBAAkB;EAChB,aAAa;EACb,MAAM,OAAO,kCAAkC;CACjD;CACA,mBAAmB;EACjB,aAAa;EACb,MAAM,OAAO,iBAAiB;CAChC;AACF;;AAGA,SAAgB,SAAS,OAAqB,MAAc,aAA2B;CACrF,MACG,OAAO,iBAAiB,UAAU,CAAC,CACnC,OAAO,gBAAgB,WAAW,CAAC,CACnC,KAAK,aAAa,IAAI,CAAC;AAC5B;;;;;;;AAQA,SAAgB,iBAAiB,QAA+B;CAC9D,MAAM,cAAc,UAAmB,UAA8B;EACnE,SAAS,OAAO,KAAK,KAAK,cAAc,GAAG,YAAY,GAAG,0BAA0B;CACtF;CAEA,OAAO,IAAI,KAAK,UAAU;CAE1B,OAAO,IAAkC,kBAAkB,SAAS,UAAU;EAC5E,MAAM,QAAQ,OAAO,OAAO,WAAW,QAAQ,OAAO,IAAI,IACtD,UAAU,QAAQ,OAAO,QACzB;EAEJ,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,GAAG,CAAC,CAAC,OAAO,iBAAiB,UAAU,CAAC,CAAC,KAAK;GACzD;EACF;EAEA,SAAS,OAAO,MAAM,KAAK,GAAG,MAAM,WAAW;CACjD,CAAC;AACH"}
|
package/esm/ui-root.mjs
CHANGED
package/llms-full.txt
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
name: devtools-overview
|
|
11
|
-
description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations and the
|
|
11
|
+
description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# `@warlock.js/devtools` — overview
|
|
@@ -53,10 +53,22 @@ of it would fail to resolve there.
|
|
|
53
53
|
| **Logs** | Log entries per request, filterable by level and module | a logger channel |
|
|
54
54
|
| **Cache** | hit / miss / set / removed / `invalidated` (tags, keys removed) | cache `cache.on(...)` |
|
|
55
55
|
| **Routes** | Every HTTP route with its method, path, name and middleware | core router |
|
|
56
|
+
| **API** | The app's API reference: every route with its parameters, request body, responses and auth | core `getDevelopmentOpenApiDocument()` |
|
|
56
57
|
|
|
57
58
|
Work outside a request (boot, scheduler ticks, queue jobs) goes to a separate
|
|
58
59
|
background list, so it never clutters a request's timeline.
|
|
59
60
|
|
|
61
|
+
## API docs
|
|
62
|
+
|
|
63
|
+
`/__warlock/docs` renders the app's OpenAPI 3.1 document with [Scalar](https://github.com/scalar/scalar); the dashboard's **API** tab shows the same page and has a **Scalar | Swagger UI** switch (remembered in the browser). `/__warlock/docs/swagger` renders the same document with [Swagger UI](https://github.com/swagger-api/swagger-ui). The document comes from `GET /__warlock/api/openapi.json`, built in-process by core's `getDevelopmentOpenApiDocument()` from the routes already registered, so it is the same generator as `warlock generate.openapi` and follows your edits with no CLI run. It covers API routes (not pages): paths, parameters, request bodies from Seal validation, responses from each handler's `responseSchema`, `401`/`422` and bearer/cookie security from `authMiddleware()`. See core's `generate-openapi` topic for what is documented and how to declare it.
|
|
64
|
+
|
|
65
|
+
- It answers `503` from `/__warlock/api/openapi.json` when the host provides no generator.
|
|
66
|
+
- Same guard as the rest of devtools: dev-only and loopback-only.
|
|
67
|
+
- Scalar's prebuilt bundle is vendored and served as `assets/scalar.js`, so apps install nothing extra. The page fetches nothing from a CDN, and Scalar's web fonts, telemetry, AI agent and MCP are turned off.
|
|
68
|
+
- Swagger UI's prebuilt bundle (swagger-ui-dist, Apache-2.0) is vendored too, served as `assets/swagger-ui-bundle.js` and `assets/swagger-ui.css`; its schema validator badge is off, so it fetches nothing from outside either.
|
|
69
|
+
- The API tab links `OpenAPI JSON` (`/__warlock/api/openapi.json`) and `Postman collection` (`/__warlock/api/postman.json`, built by core's `getDevelopmentPostmanCollection()`; `503` when core cannot build one) for importing the routes into Postman or Insomnia.
|
|
70
|
+
- To write a file instead, run `warlock generate.openapi` (default `storage/openapi/openapi.json`).
|
|
71
|
+
|
|
60
72
|
## Options
|
|
61
73
|
|
|
62
74
|
`warlock dev` uses the defaults: path `/__warlock`, the last 200 requests
|
package/llms.txt
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [devtools-overview](@warlock.js/devtools/devtools-overview/SKILL.md): Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations and the
|
|
9
|
+
- [devtools-overview](@warlock.js/devtools/devtools-overview/SKILL.md): Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.
|
package/package.json
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"environment": "server"
|
|
6
6
|
},
|
|
7
7
|
"peerDependencies": {
|
|
8
|
-
"@warlock.js/cache": "5.
|
|
9
|
-
"@warlock.js/cascade": "5.
|
|
10
|
-
"@warlock.js/core": "5.
|
|
11
|
-
"@warlock.js/logger": "5.
|
|
8
|
+
"@warlock.js/cache": "5.28.0",
|
|
9
|
+
"@warlock.js/cascade": "5.28.0",
|
|
10
|
+
"@warlock.js/core": "5.28.0",
|
|
11
|
+
"@warlock.js/logger": "5.28.0"
|
|
12
12
|
},
|
|
13
13
|
"peerDependenciesMeta": {
|
|
14
14
|
"@warlock.js/cache": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"author": "hassanzohdy",
|
|
35
35
|
"license": "MIT",
|
|
36
|
-
"version": "5.
|
|
36
|
+
"version": "5.28.0",
|
|
37
37
|
"main": "./cjs/index.cjs",
|
|
38
38
|
"module": "./esm/index.mjs",
|
|
39
39
|
"types": "./esm/index.d.mts",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: devtools-overview
|
|
3
|
-
description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations and the
|
|
3
|
+
description: 'Development-only dashboard for Warlock.js apps at `/__warlock`: request timeline (router + page middleware, loaders, render, deferred values), every SQL/Mongo query with EXPLAIN, N+1 warnings naming the file and line, a dev mailbox, logs, cache operations, the route map and an API docs page (`/__warlock/docs`, the OpenAPI 3.1 document of the app rendered with Scalar or Swagger UI at `/__warlock/docs/swagger`, with OpenAPI and Postman downloads). Use for: "why is this page slow", "which queries does this request run", "find N+1 queries", "see the email my app just sent in dev", "open the dashboard", "browse my API docs in dev", "see the OpenAPI for my routes", "open Swagger UI for my API", "export my routes to Postman". Install with `warlock add devtools`; never registered in warlock.config.ts. Not this package: production monitoring/APM or OpenTelemetry export — use `http.tracing` hooks in @warlock.js/core; slow-query logging in production — subscribe to cascade `dataSourceRegistry.on("query")` yourself.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# `@warlock.js/devtools` — overview
|
|
@@ -45,10 +45,22 @@ of it would fail to resolve there.
|
|
|
45
45
|
| **Logs** | Log entries per request, filterable by level and module | a logger channel |
|
|
46
46
|
| **Cache** | hit / miss / set / removed / `invalidated` (tags, keys removed) | cache `cache.on(...)` |
|
|
47
47
|
| **Routes** | Every HTTP route with its method, path, name and middleware | core router |
|
|
48
|
+
| **API** | The app's API reference: every route with its parameters, request body, responses and auth | core `getDevelopmentOpenApiDocument()` |
|
|
48
49
|
|
|
49
50
|
Work outside a request (boot, scheduler ticks, queue jobs) goes to a separate
|
|
50
51
|
background list, so it never clutters a request's timeline.
|
|
51
52
|
|
|
53
|
+
## API docs
|
|
54
|
+
|
|
55
|
+
`/__warlock/docs` renders the app's OpenAPI 3.1 document with [Scalar](https://github.com/scalar/scalar); the dashboard's **API** tab shows the same page and has a **Scalar | Swagger UI** switch (remembered in the browser). `/__warlock/docs/swagger` renders the same document with [Swagger UI](https://github.com/swagger-api/swagger-ui). The document comes from `GET /__warlock/api/openapi.json`, built in-process by core's `getDevelopmentOpenApiDocument()` from the routes already registered, so it is the same generator as `warlock generate.openapi` and follows your edits with no CLI run. It covers API routes (not pages): paths, parameters, request bodies from Seal validation, responses from each handler's `responseSchema`, `401`/`422` and bearer/cookie security from `authMiddleware()`. See core's `generate-openapi` topic for what is documented and how to declare it.
|
|
56
|
+
|
|
57
|
+
- It answers `503` from `/__warlock/api/openapi.json` when the host provides no generator.
|
|
58
|
+
- Same guard as the rest of devtools: dev-only and loopback-only.
|
|
59
|
+
- Scalar's prebuilt bundle is vendored and served as `assets/scalar.js`, so apps install nothing extra. The page fetches nothing from a CDN, and Scalar's web fonts, telemetry, AI agent and MCP are turned off.
|
|
60
|
+
- Swagger UI's prebuilt bundle (swagger-ui-dist, Apache-2.0) is vendored too, served as `assets/swagger-ui-bundle.js` and `assets/swagger-ui.css`; its schema validator badge is off, so it fetches nothing from outside either.
|
|
61
|
+
- The API tab links `OpenAPI JSON` (`/__warlock/api/openapi.json`) and `Postman collection` (`/__warlock/api/postman.json`, built by core's `getDevelopmentPostmanCollection()`; `503` when core cannot build one) for importing the routes into Postman or Insomnia.
|
|
62
|
+
- To write a file instead, run `warlock generate.openapi` (default `storage/openapi/openapi.json`).
|
|
63
|
+
|
|
52
64
|
## Options
|
|
53
65
|
|
|
54
66
|
`warlock dev` uses the defaults: path `/__warlock`, the last 200 requests
|
package/ui/app.css
CHANGED
|
@@ -330,6 +330,20 @@ details summary {
|
|
|
330
330
|
.log-success {
|
|
331
331
|
color: var(--good);
|
|
332
332
|
}
|
|
333
|
+
.toolbar-link {
|
|
334
|
+
margin-left: 6px;
|
|
335
|
+
}
|
|
336
|
+
.toolbar-link:first-of-type {
|
|
337
|
+
margin-left: auto;
|
|
338
|
+
}
|
|
339
|
+
.docs-frame {
|
|
340
|
+
display: block;
|
|
341
|
+
width: 100%;
|
|
342
|
+
height: calc(100vh - 165px);
|
|
343
|
+
border: 1px solid var(--line);
|
|
344
|
+
border-radius: 7px;
|
|
345
|
+
background: var(--surface);
|
|
346
|
+
}
|
|
333
347
|
@media (max-width: 700px) {
|
|
334
348
|
.topbar {
|
|
335
349
|
gap: 10px;
|
package/ui/app.js
CHANGED
|
@@ -467,6 +467,74 @@ async function routesView() {
|
|
|
467
467
|
return section;
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
const DOCS_VIEWERS = [
|
|
471
|
+
{ id: "scalar", label: "Scalar", src: "docs" },
|
|
472
|
+
{ id: "swagger", label: "Swagger UI", src: "docs/swagger" },
|
|
473
|
+
];
|
|
474
|
+
const DOCS_VIEWER_KEY = "warlock-devtools-docs-viewer";
|
|
475
|
+
|
|
476
|
+
function savedDocsViewer() {
|
|
477
|
+
try {
|
|
478
|
+
const id = localStorage.getItem(DOCS_VIEWER_KEY);
|
|
479
|
+
return DOCS_VIEWERS.find((viewer) => viewer.id === id) || DOCS_VIEWERS[0];
|
|
480
|
+
} catch {
|
|
481
|
+
return DOCS_VIEWERS[0];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function saveDocsViewer(viewer) {
|
|
486
|
+
try {
|
|
487
|
+
localStorage.setItem(DOCS_VIEWER_KEY, viewer.id);
|
|
488
|
+
} catch {
|
|
489
|
+
// Storage can be blocked; the choice then lasts until the tab changes.
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function apiView() {
|
|
494
|
+
const section = el("section");
|
|
495
|
+
const frame = el("iframe", { class: "docs-frame", title: "API reference" });
|
|
496
|
+
const buttons = DOCS_VIEWERS.map((viewer) =>
|
|
497
|
+
el("button", {
|
|
498
|
+
class: "toggle",
|
|
499
|
+
type: "button",
|
|
500
|
+
text: viewer.label,
|
|
501
|
+
}),
|
|
502
|
+
);
|
|
503
|
+
const show = (viewer) => {
|
|
504
|
+
frame.src = viewer.src;
|
|
505
|
+
buttons.forEach((button, index) =>
|
|
506
|
+
button.classList.toggle("is-active", DOCS_VIEWERS[index] === viewer),
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
const pick = (viewer) => {
|
|
510
|
+
saveDocsViewer(viewer);
|
|
511
|
+
show(viewer);
|
|
512
|
+
};
|
|
513
|
+
buttons.forEach((button, index) =>
|
|
514
|
+
button.addEventListener("click", () => pick(DOCS_VIEWERS[index])),
|
|
515
|
+
);
|
|
516
|
+
section.append(
|
|
517
|
+
el("div", { class: "toolbar" }, [
|
|
518
|
+
...buttons,
|
|
519
|
+
el("a", {
|
|
520
|
+
class: "toolbar-link",
|
|
521
|
+
href: "api/openapi.json",
|
|
522
|
+
download: "openapi.json",
|
|
523
|
+
text: "OpenAPI JSON",
|
|
524
|
+
}),
|
|
525
|
+
el("a", {
|
|
526
|
+
class: "toolbar-link",
|
|
527
|
+
href: "api/postman.json",
|
|
528
|
+
download: "postman.json",
|
|
529
|
+
text: "Postman collection",
|
|
530
|
+
}),
|
|
531
|
+
]),
|
|
532
|
+
frame,
|
|
533
|
+
);
|
|
534
|
+
show(savedDocsViewer());
|
|
535
|
+
return section;
|
|
536
|
+
}
|
|
537
|
+
|
|
470
538
|
async function render() {
|
|
471
539
|
try {
|
|
472
540
|
const view =
|
|
@@ -478,7 +546,9 @@ async function render() {
|
|
|
478
546
|
? await mailView()
|
|
479
547
|
: state.tab === "logs"
|
|
480
548
|
? await logsView()
|
|
481
|
-
:
|
|
549
|
+
: state.tab === "routes"
|
|
550
|
+
? await routesView()
|
|
551
|
+
: apiView();
|
|
482
552
|
app.replaceChildren(view);
|
|
483
553
|
} catch (error) {
|
|
484
554
|
app.replaceChildren(errorView(error));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>API reference (Swagger UI)</title>
|
|
7
|
+
<!-- Every URL is relative to /docs/swagger, so the page works under any dashboard base path. -->
|
|
8
|
+
<link rel="stylesheet" href="../assets/swagger-ui.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="swagger-ui"></div>
|
|
12
|
+
<script src="../assets/swagger-ui-bundle.js"></script>
|
|
13
|
+
<script src="../assets/docs-swagger.js"></script>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Starts Swagger UI on the page. The document URL is relative to /docs/swagger,
|
|
2
|
+
// so it works under any dashboard base path. The validator badge is off
|
|
3
|
+
// (`validatorUrl: null`): this is a loopback dev tool and must not call out.
|
|
4
|
+
window.ui = SwaggerUIBundle({
|
|
5
|
+
url: "../api/openapi.json",
|
|
6
|
+
dom_id: "#swagger-ui",
|
|
7
|
+
validatorUrl: null,
|
|
8
|
+
deepLinking: true,
|
|
9
|
+
persistAuthorization: false,
|
|
10
|
+
});
|
package/ui/docs.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>API reference</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<!-- Scalar reads its settings from this element. Every URL is relative to /docs, so the page works under any dashboard base path. Fonts, telemetry, the AI agent and MCP are off: this is a loopback dev tool and must not call out. -->
|
|
10
|
+
<script
|
|
11
|
+
id="api-reference"
|
|
12
|
+
data-url="api/openapi.json"
|
|
13
|
+
data-configuration='{"withDefaultFonts":false,"telemetry":false,"showDeveloperTools":"never","agent":{"disabled":true},"mcp":{"disabled":true}}'
|
|
14
|
+
></script>
|
|
15
|
+
<script src="assets/scalar.js"></script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/ui/index.html
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<button class="tab" data-tab="mail">Mail</button>
|
|
16
16
|
<button class="tab" data-tab="logs">Logs</button>
|
|
17
17
|
<button class="tab" data-tab="routes">Routes</button>
|
|
18
|
+
<button class="tab" data-tab="api">API</button>
|
|
18
19
|
</nav>
|
|
19
20
|
<button id="clear" class="clear" type="button">Clear</button>
|
|
20
21
|
</header>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Scalar API Reference (vendored)
|
|
2
|
+
|
|
3
|
+
`standalone.js` is the unmodified prebuilt browser bundle of
|
|
4
|
+
[`@scalar/api-reference`](https://github.com/scalar/scalar) **1.72.2**
|
|
5
|
+
(`dist/browser/standalone.js` in the npm package), vendored so apps that add
|
|
6
|
+
`@warlock.js/devtools` install nothing extra. It is served only by the
|
|
7
|
+
devtools API docs page (`/__warlock/docs`), in development, to loopback clients.
|
|
8
|
+
|
|
9
|
+
To update: `npm pack @scalar/api-reference@<version>`, extract
|
|
10
|
+
`package/dist/browser/standalone.js` over this file, and update the version above.
|
|
11
|
+
|
|
12
|
+
## License
|
|
13
|
+
|
|
14
|
+
MIT License
|
|
15
|
+
|
|
16
|
+
Copyright (c) Scalar (https://github.com/scalar)
|
|
17
|
+
|
|
18
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
19
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
20
|
+
in the Software without restriction, including without limitation the rights
|
|
21
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
22
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
23
|
+
furnished to do so, subject to the following conditions:
|
|
24
|
+
|
|
25
|
+
The above copyright notice and this permission notice shall be included in all
|
|
26
|
+
copies or substantial portions of the Software.
|
|
27
|
+
|
|
28
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
29
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
30
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
31
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
32
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
33
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
34
|
+
SOFTWARE.
|