@warlock.js/core 5.34.3 → 5.35.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 +44 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +14 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +6 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +36 -1
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +31 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +48 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +48 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +3 -0
- package/esm/http/error-response.d.mts.map +1 -1
- package/esm/http/error-response.mjs +8 -2
- package/esm/http/error-response.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +3 -2
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/server.mjs +1 -1
- package/esm/index.d.mts +8 -6
- package/esm/index.mjs +3 -2
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +46 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +2 -1
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +48 -6
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +20 -4
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +763 -9
- package/llms.txt +2 -0
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +7 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +2 -2
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +50 -0
- package/skills/test-browser/SKILL.md +53 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +18 -0
- package/skills/use-localization/SKILL.md +21 -0
- package/skills/warlock-check/SKILL.md +72 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/write-cli-command/SKILL.md +18 -0
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
//#region ../../packages/core/src/cli/commands/generate/templates/web-stubs.ts
|
|
2
|
+
/** A page's executable contract stays in the server setup companion. */
|
|
3
|
+
function pageFiles(name, route, interactive = false) {
|
|
4
|
+
return {
|
|
5
|
+
[`${name.kebab}.setup.ts`]: `import type { PageConfig } from "@warlock.js/web";
|
|
6
|
+
|
|
7
|
+
export function loader() {
|
|
8
|
+
return { title: ${JSON.stringify(name.pascal)} };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const config = {
|
|
12
|
+
route: { path: ${JSON.stringify(route)}, name: ${JSON.stringify(route.slice(1).replaceAll("/", ".") || "index")} },
|
|
13
|
+
metadata: { title: ${JSON.stringify(name.pascal)} },
|
|
14
|
+
} as const satisfies PageConfig<typeof loader>;
|
|
15
|
+
`,
|
|
16
|
+
[`${name.kebab}.page.tsx`]: `import type { PageProps } from "@warlock.js/web";
|
|
17
|
+
import type { loader } from "./${name.kebab}.setup";
|
|
18
|
+
${interactive ? `import { use${name.pascal} } from "./hooks/use-${name.kebab}";\n` : ""}
|
|
19
|
+
export default function ${name.pascal}Page({ data }: PageProps<typeof loader>) {
|
|
20
|
+
${interactive ? `const { count, increment } = use${name.pascal}();` : ""}
|
|
21
|
+
return <main><h1>{data.title}</h1>${interactive ? "<button type=\"button\" onClick={increment}>Count: {count}</button>" : ""}</main>;
|
|
22
|
+
}
|
|
23
|
+
`,
|
|
24
|
+
...interactive ? { [`hooks/use-${name.kebab}.ts`]: `import { useState } from "react";
|
|
25
|
+
|
|
26
|
+
export function use${name.pascal}() {
|
|
27
|
+
const [count, setCount] = useState(0);
|
|
28
|
+
return { count, increment: () => setCount((value) => value + 1) };
|
|
29
|
+
}
|
|
30
|
+
` } : {}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function inputImport(component) {
|
|
34
|
+
return `import ${component} from "web/design-system/components/${component.replace(/[A-Z]/g, (letter, index) => `${index ? "-" : ""}${letter.toLowerCase()}`)}";`;
|
|
35
|
+
}
|
|
36
|
+
/** Forms share the same schema value with their controller, with no server imports in the UI. */
|
|
37
|
+
function formFiles(name, schemaImport, schemaName, fields, endpoint) {
|
|
38
|
+
const components = [...new Set(fields.map((field) => field.component))];
|
|
39
|
+
return {
|
|
40
|
+
[`types/${name.kebab}-form-props.ts`]: `import type { Infer } from "@warlock.js/seal";
|
|
41
|
+
import type { ${schemaName} } from "${schemaImport}";
|
|
42
|
+
|
|
43
|
+
export type ${name.pascal}FormProps = {
|
|
44
|
+
path?: string;
|
|
45
|
+
method?: "POST" | "PUT";
|
|
46
|
+
defaultValue?: Partial<Infer<typeof ${schemaName}>>;
|
|
47
|
+
};
|
|
48
|
+
`,
|
|
49
|
+
[`hooks/use-${name.kebab}-form.ts`]: `import { useSubmitForm } from "@warlock.js/web/form";
|
|
50
|
+
import type { ${schemaName} } from "${schemaImport}";
|
|
51
|
+
import type { ${name.pascal}FormProps } from "../types/${name.kebab}-form-props";
|
|
52
|
+
|
|
53
|
+
export function use${name.pascal}Form({ path = ${JSON.stringify(endpoint)}, method = "POST" }: ${name.pascal}FormProps) {
|
|
54
|
+
return useSubmitForm<typeof ${schemaName}>({ path, method });
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
[`${name.kebab}-form.tsx`]: `import { Form } from "@mongez/react-form";
|
|
58
|
+
${components.map(inputImport).join("\n")}
|
|
59
|
+
${inputImport("SubmitButton")}
|
|
60
|
+
import { ${schemaName} } from "${schemaImport}";
|
|
61
|
+
import { use${name.pascal}Form } from "./hooks/use-${name.kebab}-form";
|
|
62
|
+
import type { ${name.pascal}FormProps } from "./types/${name.kebab}-form-props";
|
|
63
|
+
|
|
64
|
+
export default function ${name.pascal}Form(props: ${name.pascal}FormProps) {
|
|
65
|
+
const { submit, isLoading, formErrors, data } = use${name.pascal}Form(props);
|
|
66
|
+
return <Form schema={${schemaName}} defaultValue={props.defaultValue} onSubmit={submit}>
|
|
67
|
+
${fields.map((field) => ` <${field.component} name=${JSON.stringify(field.name)} label=${JSON.stringify(field.name)} />`).join("\n")}
|
|
68
|
+
{formErrors.map((message) => <p role="alert" key={message}>{message}</p>)}
|
|
69
|
+
{data !== null && <p role="status">Saved successfully.</p>}
|
|
70
|
+
<SubmitButton disabled={isLoading}>{isLoading ? "Saving…" : "Save"}</SubmitButton>
|
|
71
|
+
</Form>;
|
|
72
|
+
}
|
|
73
|
+
`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** The UI talks to the existing module endpoints, so their authorization remains authoritative. */
|
|
77
|
+
function crudFiles(module, filters) {
|
|
78
|
+
const base = module.plural.kebab;
|
|
79
|
+
const entity = module.singular;
|
|
80
|
+
const files = {
|
|
81
|
+
"types/record.ts": `export type RecordData = { id: string | number; [field: string]: unknown };
|
|
82
|
+
export type ListData = { data: RecordData[]; pagination: { page: number; pages: number } };
|
|
83
|
+
`,
|
|
84
|
+
"hooks/use-list.ts": `import { http } from "@mongez/http";
|
|
85
|
+
import type { FormSubmitOptions } from "@mongez/react-form";
|
|
86
|
+
import { useEffect, useState } from "react";
|
|
87
|
+
import type { ListData } from "../types/record";
|
|
88
|
+
|
|
89
|
+
export function useList() {
|
|
90
|
+
const [query, setQuery] = useState<Record<string, string | number>>({ page: 1 });
|
|
91
|
+
const [revision, setRevision] = useState(0);
|
|
92
|
+
const [result, setResult] = useState<ListData | null>(null);
|
|
93
|
+
const [error, setError] = useState<string | null>(null);
|
|
94
|
+
const [isLoading, setLoading] = useState(true);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let active = true;
|
|
97
|
+
setLoading(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
const pending = http.get<ListData>("/${base}", { params: query });
|
|
100
|
+
void pending.then((response) => {
|
|
101
|
+
if (!active) { return; }
|
|
102
|
+
if (response.error) { setError(response.error.message); }
|
|
103
|
+
else { setResult(response.data); }
|
|
104
|
+
}).catch(() => { if (active) { setError("Unable to load records."); } })
|
|
105
|
+
.finally(() => { if (active) { setLoading(false); } });
|
|
106
|
+
return () => { active = false; pending.cancel(); };
|
|
107
|
+
}, [query, revision]);
|
|
108
|
+
const filter = ({ values }: FormSubmitOptions) => {
|
|
109
|
+
const next: Record<string, string | number> = { page: 1 };
|
|
110
|
+
for (const [key, value] of Object.entries(values)) {
|
|
111
|
+
if (typeof value === "string" && value !== "") { next[key] = value; }
|
|
112
|
+
}
|
|
113
|
+
setQuery(next);
|
|
114
|
+
};
|
|
115
|
+
return { result, error, isLoading, filter,
|
|
116
|
+
page: (page: number) => setQuery((value) => ({ ...value, page })),
|
|
117
|
+
refresh: () => setRevision((value) => value + 1) };
|
|
118
|
+
}
|
|
119
|
+
`,
|
|
120
|
+
"hooks/use-edit.ts": `import { http } from "@mongez/http";
|
|
121
|
+
import { useEffect, useState } from "react";
|
|
122
|
+
import type { Update${entity.pascal}Schema } from "app/${base}/schema/update-${entity.kebab}.schema";
|
|
123
|
+
|
|
124
|
+
export function useEdit(id: string) {
|
|
125
|
+
const [record, setRecord] = useState<{ id: string; values: Partial<Update${entity.pascal}Schema> } | null>(null);
|
|
126
|
+
const [error, setError] = useState<string | null>(null);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
let active = true;
|
|
129
|
+
setRecord(null);
|
|
130
|
+
setError(null);
|
|
131
|
+
const pending = http.get<{ ${entity.camel}: Partial<Update${entity.pascal}Schema> }>(\`/${base}/\${encodeURIComponent(id)}\`);
|
|
132
|
+
void pending.then((response) => {
|
|
133
|
+
if (!active) { return; }
|
|
134
|
+
if (response.error) { setError(response.error.message); }
|
|
135
|
+
else { setRecord({ id, values: response.data.${entity.camel} }); }
|
|
136
|
+
}).catch(() => { if (active) { setError("Unable to load record."); } });
|
|
137
|
+
return () => { active = false; pending.cancel(); };
|
|
138
|
+
}, [id]);
|
|
139
|
+
return { values: record?.id === id ? record.values : null, error };
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
"hooks/use-delete.ts": `import { useSubmitForm } from "@warlock.js/web/form";
|
|
143
|
+
|
|
144
|
+
export function useDelete(id: string | number, onDeleted: () => void) {
|
|
145
|
+
return useSubmitForm({ path: \`/${base}/\${encodeURIComponent(id)}\`, method: "DELETE", onSuccess: onDeleted });
|
|
146
|
+
}
|
|
147
|
+
`,
|
|
148
|
+
"types/delete-action-props.ts": `export type DeleteActionProps = { id: string | number; onDeleted: () => void };
|
|
149
|
+
`,
|
|
150
|
+
"delete-action.tsx": `import { Form } from "@mongez/react-form";
|
|
151
|
+
import SubmitButton from "web/design-system/components/submit-button";
|
|
152
|
+
import { useDelete } from "./hooks/use-delete";
|
|
153
|
+
import type { DeleteActionProps } from "./types/delete-action-props";
|
|
154
|
+
|
|
155
|
+
export default function DeleteAction({ id, onDeleted }: DeleteActionProps) {
|
|
156
|
+
const { submit, isLoading, formErrors } = useDelete(id, onDeleted);
|
|
157
|
+
return <Form onSubmit={submit}>
|
|
158
|
+
{formErrors.map((message) => <p role="alert" key={message}>{message}</p>)}
|
|
159
|
+
<SubmitButton disabled={isLoading}>Delete</SubmitButton>
|
|
160
|
+
</Form>;
|
|
161
|
+
}
|
|
162
|
+
`,
|
|
163
|
+
"index.page.tsx": `import { Form } from "@mongez/react-form";
|
|
164
|
+
${filters.length ? inputImport("TextInput") : ""}
|
|
165
|
+
import SubmitButton from "web/design-system/components/submit-button";
|
|
166
|
+
import DeleteAction from "./delete-action";
|
|
167
|
+
import { useList } from "./hooks/use-list";
|
|
168
|
+
|
|
169
|
+
export default function ${module.pascal}ListPage() {
|
|
170
|
+
const { result, error, isLoading, filter, page, refresh } = useList();
|
|
171
|
+
return <main><h1>${module.pascal}</h1><a href="/${base}/create">Create</a>
|
|
172
|
+
<Form onSubmit={filter}>
|
|
173
|
+
${filters.map((field) => ` <TextInput name=${JSON.stringify(field)} label=${JSON.stringify(field)} />`).join("\n")}
|
|
174
|
+
<SubmitButton>Filter</SubmitButton>
|
|
175
|
+
</Form>
|
|
176
|
+
{error && <p role="alert">{error}</p>}
|
|
177
|
+
{isLoading && <p role="status">Loading…</p>}
|
|
178
|
+
<ul>{result?.data.map((record) => <li key={record.id}>
|
|
179
|
+
<a href={\`/${base}/\${encodeURIComponent(record.id)}/edit\`}>{record.id}</a>
|
|
180
|
+
<DeleteAction id={record.id} onDeleted={refresh} />
|
|
181
|
+
</li>)}</ul>
|
|
182
|
+
<button type="button" disabled={isLoading || !result || result.pagination.page <= 1} onClick={() => page((result?.pagination.page ?? 1) - 1)}>Previous</button>
|
|
183
|
+
<button type="button" disabled={isLoading || !result || result.pagination.page >= result.pagination.pages} onClick={() => page((result?.pagination.page ?? 1) + 1)}>Next</button>
|
|
184
|
+
</main>;
|
|
185
|
+
}
|
|
186
|
+
`,
|
|
187
|
+
"create.page.tsx": `import Create${entity.pascal}Form from "./create-${entity.kebab}-form";
|
|
188
|
+
|
|
189
|
+
export default function Create${entity.pascal}Page() {
|
|
190
|
+
return <main><h1>Create ${entity.pascal}</h1><Create${entity.pascal}Form /></main>;
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
"edit.page.tsx": `import type { PageProps } from "@warlock.js/web";
|
|
194
|
+
import type { loader } from "./edit.setup";
|
|
195
|
+
import { useEdit } from "./hooks/use-edit";
|
|
196
|
+
import Update${entity.pascal}Form from "./update-${entity.kebab}-form";
|
|
197
|
+
|
|
198
|
+
export default function Edit${entity.pascal}Page({ data }: PageProps<typeof loader>) {
|
|
199
|
+
const { values, error } = useEdit(data.id);
|
|
200
|
+
return <main><h1>Edit ${entity.pascal}</h1>
|
|
201
|
+
{error && <p role="alert">{error}</p>}
|
|
202
|
+
{!values && !error && <p role="status">Loading…</p>}
|
|
203
|
+
{values && <Update${entity.pascal}Form key={data.id} path={\`/${base}/\${encodeURIComponent(data.id)}\`} method="PUT" defaultValue={values} />}
|
|
204
|
+
</main>;
|
|
205
|
+
}
|
|
206
|
+
`
|
|
207
|
+
};
|
|
208
|
+
for (const [file, route] of [
|
|
209
|
+
["index", `/${base}`],
|
|
210
|
+
["create", `/${base}/create`],
|
|
211
|
+
["edit", `/${base}/:id/edit`]
|
|
212
|
+
]) files[`${file}.setup.ts`] = `import type { PageConfig${file === "edit" ? ", PageLoaderContext" : ""} } from "@warlock.js/web";
|
|
213
|
+
import { requireUser } from "@warlock.js/web/session";
|
|
214
|
+
|
|
215
|
+
export ${file === "edit" ? `async function loader({ request }: PageLoaderContext) {
|
|
216
|
+
const id = String(request.input("id"));
|
|
217
|
+
return { id };
|
|
218
|
+
}` : `function loader() { return {}; }`}
|
|
219
|
+
|
|
220
|
+
export const config = {
|
|
221
|
+
route: { path: "${route}", name: "${base}.${file}" },
|
|
222
|
+
middleware: [requireUser({ userType: "user" })],
|
|
223
|
+
metadata: { title: "${module.pascal}" },
|
|
224
|
+
} as const satisfies PageConfig<typeof loader>;
|
|
225
|
+
`;
|
|
226
|
+
return files;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
export { crudFiles, formFiles, pageFiles };
|
|
231
|
+
//# sourceMappingURL=web-stubs.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-stubs.mjs","names":[],"sources":["../../../../../../../../../../../packages/core/src/cli/commands/generate/templates/web-stubs.ts"],"sourcesContent":["import type { Name } from \"../utils/name-parser\";\n\nexport type WebField = { name: string; component: string };\nexport type WebFiles = Record<string, string>;\n\n/** A page's executable contract stays in the server setup companion. */\nexport function pageFiles(name: Name, route: string, interactive = false): WebFiles {\n return {\n [`${name.kebab}.setup.ts`]: `import type { PageConfig } from \"@warlock.js/web\";\n\nexport function loader() {\n return { title: ${JSON.stringify(name.pascal)} };\n}\n\nexport const config = {\n route: { path: ${JSON.stringify(route)}, name: ${JSON.stringify(route.slice(1).replaceAll(\"/\", \".\") || \"index\")} },\n metadata: { title: ${JSON.stringify(name.pascal)} },\n} as const satisfies PageConfig<typeof loader>;\n`,\n [`${name.kebab}.page.tsx`]: `import type { PageProps } from \"@warlock.js/web\";\nimport type { loader } from \"./${name.kebab}.setup\";\n${interactive ? `import { use${name.pascal} } from \"./hooks/use-${name.kebab}\";\\n` : \"\"}\nexport default function ${name.pascal}Page({ data }: PageProps<typeof loader>) {\n ${interactive ? `const { count, increment } = use${name.pascal}();` : \"\"}\n return <main><h1>{data.title}</h1>${interactive ? '<button type=\"button\" onClick={increment}>Count: {count}</button>' : \"\"}</main>;\n}\n`,\n ...(interactive\n ? {\n [`hooks/use-${name.kebab}.ts`]: `import { useState } from \"react\";\n\nexport function use${name.pascal}() {\n const [count, setCount] = useState(0);\n return { count, increment: () => setCount((value) => value + 1) };\n}\n`,\n }\n : {}),\n };\n}\n\nfunction inputImport(component: string): string {\n const file = component.replace(\n /[A-Z]/g,\n (letter, index) => `${index ? \"-\" : \"\"}${letter.toLowerCase()}`,\n );\n return `import ${component} from \"web/design-system/components/${file}\";`;\n}\n\n/** Forms share the same schema value with their controller, with no server imports in the UI. */\nexport function formFiles(\n name: Name,\n schemaImport: string,\n schemaName: string,\n fields: WebField[],\n endpoint: string,\n): WebFiles {\n const components = [...new Set(fields.map((field) => field.component))];\n return {\n [`types/${name.kebab}-form-props.ts`]: `import type { Infer } from \"@warlock.js/seal\";\nimport type { ${schemaName} } from \"${schemaImport}\";\n\nexport type ${name.pascal}FormProps = {\n path?: string;\n method?: \"POST\" | \"PUT\";\n defaultValue?: Partial<Infer<typeof ${schemaName}>>;\n};\n`,\n [`hooks/use-${name.kebab}-form.ts`]: `import { useSubmitForm } from \"@warlock.js/web/form\";\nimport type { ${schemaName} } from \"${schemaImport}\";\nimport type { ${name.pascal}FormProps } from \"../types/${name.kebab}-form-props\";\n\nexport function use${name.pascal}Form({ path = ${JSON.stringify(endpoint)}, method = \"POST\" }: ${name.pascal}FormProps) {\n return useSubmitForm<typeof ${schemaName}>({ path, method });\n}\n`,\n [`${name.kebab}-form.tsx`]: `import { Form } from \"@mongez/react-form\";\n${components.map(inputImport).join(\"\\n\")}\n${inputImport(\"SubmitButton\")}\nimport { ${schemaName} } from \"${schemaImport}\";\nimport { use${name.pascal}Form } from \"./hooks/use-${name.kebab}-form\";\nimport type { ${name.pascal}FormProps } from \"./types/${name.kebab}-form-props\";\n\nexport default function ${name.pascal}Form(props: ${name.pascal}FormProps) {\n const { submit, isLoading, formErrors, data } = use${name.pascal}Form(props);\n return <Form schema={${schemaName}} defaultValue={props.defaultValue} onSubmit={submit}>\n${fields.map((field) => ` <${field.component} name=${JSON.stringify(field.name)} label=${JSON.stringify(field.name)} />`).join(\"\\n\")}\n {formErrors.map((message) => <p role=\"alert\" key={message}>{message}</p>)}\n {data !== null && <p role=\"status\">Saved successfully.</p>}\n <SubmitButton disabled={isLoading}>{isLoading ? \"Saving…\" : \"Save\"}</SubmitButton>\n </Form>;\n}\n`,\n };\n}\n\n/** The UI talks to the existing module endpoints, so their authorization remains authoritative. */\nexport function crudFiles(module: Name, filters: string[]): WebFiles {\n const base = module.plural.kebab;\n const entity = module.singular;\n const files: WebFiles = {\n \"types/record.ts\": `export type RecordData = { id: string | number; [field: string]: unknown };\nexport type ListData = { data: RecordData[]; pagination: { page: number; pages: number } };\n`,\n \"hooks/use-list.ts\": `import { http } from \"@mongez/http\";\nimport type { FormSubmitOptions } from \"@mongez/react-form\";\nimport { useEffect, useState } from \"react\";\nimport type { ListData } from \"../types/record\";\n\nexport function useList() {\n const [query, setQuery] = useState<Record<string, string | number>>({ page: 1 });\n const [revision, setRevision] = useState(0);\n const [result, setResult] = useState<ListData | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [isLoading, setLoading] = useState(true);\n useEffect(() => {\n let active = true;\n setLoading(true);\n setError(null);\n const pending = http.get<ListData>(\"/${base}\", { params: query });\n void pending.then((response) => {\n if (!active) { return; }\n if (response.error) { setError(response.error.message); }\n else { setResult(response.data); }\n }).catch(() => { if (active) { setError(\"Unable to load records.\"); } })\n .finally(() => { if (active) { setLoading(false); } });\n return () => { active = false; pending.cancel(); };\n }, [query, revision]);\n const filter = ({ values }: FormSubmitOptions) => {\n const next: Record<string, string | number> = { page: 1 };\n for (const [key, value] of Object.entries(values)) {\n if (typeof value === \"string\" && value !== \"\") { next[key] = value; }\n }\n setQuery(next);\n };\n return { result, error, isLoading, filter,\n page: (page: number) => setQuery((value) => ({ ...value, page })),\n refresh: () => setRevision((value) => value + 1) };\n}\n`,\n \"hooks/use-edit.ts\": `import { http } from \"@mongez/http\";\nimport { useEffect, useState } from \"react\";\nimport type { Update${entity.pascal}Schema } from \"app/${base}/schema/update-${entity.kebab}.schema\";\n\nexport function useEdit(id: string) {\n const [record, setRecord] = useState<{ id: string; values: Partial<Update${entity.pascal}Schema> } | null>(null);\n const [error, setError] = useState<string | null>(null);\n useEffect(() => {\n let active = true;\n setRecord(null);\n setError(null);\n const pending = http.get<{ ${entity.camel}: Partial<Update${entity.pascal}Schema> }>(\\`/${base}/\\${encodeURIComponent(id)}\\`);\n void pending.then((response) => {\n if (!active) { return; }\n if (response.error) { setError(response.error.message); }\n else { setRecord({ id, values: response.data.${entity.camel} }); }\n }).catch(() => { if (active) { setError(\"Unable to load record.\"); } });\n return () => { active = false; pending.cancel(); };\n }, [id]);\n return { values: record?.id === id ? record.values : null, error };\n}\n`,\n \"hooks/use-delete.ts\": `import { useSubmitForm } from \"@warlock.js/web/form\";\n\nexport function useDelete(id: string | number, onDeleted: () => void) {\n return useSubmitForm({ path: \\`/${base}/\\${encodeURIComponent(id)}\\`, method: \"DELETE\", onSuccess: onDeleted });\n}\n`,\n \"types/delete-action-props.ts\": `export type DeleteActionProps = { id: string | number; onDeleted: () => void };\n`,\n \"delete-action.tsx\": `import { Form } from \"@mongez/react-form\";\nimport SubmitButton from \"web/design-system/components/submit-button\";\nimport { useDelete } from \"./hooks/use-delete\";\nimport type { DeleteActionProps } from \"./types/delete-action-props\";\n\nexport default function DeleteAction({ id, onDeleted }: DeleteActionProps) {\n const { submit, isLoading, formErrors } = useDelete(id, onDeleted);\n return <Form onSubmit={submit}>\n {formErrors.map((message) => <p role=\"alert\" key={message}>{message}</p>)}\n <SubmitButton disabled={isLoading}>Delete</SubmitButton>\n </Form>;\n}\n`,\n \"index.page.tsx\": `import { Form } from \"@mongez/react-form\";\n${filters.length ? inputImport(\"TextInput\") : \"\"}\nimport SubmitButton from \"web/design-system/components/submit-button\";\nimport DeleteAction from \"./delete-action\";\nimport { useList } from \"./hooks/use-list\";\n\nexport default function ${module.pascal}ListPage() {\n const { result, error, isLoading, filter, page, refresh } = useList();\n return <main><h1>${module.pascal}</h1><a href=\"/${base}/create\">Create</a>\n <Form onSubmit={filter}>\n${filters.map((field) => ` <TextInput name=${JSON.stringify(field)} label=${JSON.stringify(field)} />`).join(\"\\n\")}\n <SubmitButton>Filter</SubmitButton>\n </Form>\n {error && <p role=\"alert\">{error}</p>}\n {isLoading && <p role=\"status\">Loading…</p>}\n <ul>{result?.data.map((record) => <li key={record.id}>\n <a href={\\`/${base}/\\${encodeURIComponent(record.id)}/edit\\`}>{record.id}</a>\n <DeleteAction id={record.id} onDeleted={refresh} />\n </li>)}</ul>\n <button type=\"button\" disabled={isLoading || !result || result.pagination.page <= 1} onClick={() => page((result?.pagination.page ?? 1) - 1)}>Previous</button>\n <button type=\"button\" disabled={isLoading || !result || result.pagination.page >= result.pagination.pages} onClick={() => page((result?.pagination.page ?? 1) + 1)}>Next</button>\n </main>;\n}\n`,\n \"create.page.tsx\": `import Create${entity.pascal}Form from \"./create-${entity.kebab}-form\";\n\nexport default function Create${entity.pascal}Page() {\n return <main><h1>Create ${entity.pascal}</h1><Create${entity.pascal}Form /></main>;\n}\n`,\n \"edit.page.tsx\": `import type { PageProps } from \"@warlock.js/web\";\nimport type { loader } from \"./edit.setup\";\nimport { useEdit } from \"./hooks/use-edit\";\nimport Update${entity.pascal}Form from \"./update-${entity.kebab}-form\";\n\nexport default function Edit${entity.pascal}Page({ data }: PageProps<typeof loader>) {\n const { values, error } = useEdit(data.id);\n return <main><h1>Edit ${entity.pascal}</h1>\n {error && <p role=\"alert\">{error}</p>}\n {!values && !error && <p role=\"status\">Loading…</p>}\n {values && <Update${entity.pascal}Form key={data.id} path={\\`/${base}/\\${encodeURIComponent(data.id)}\\`} method=\"PUT\" defaultValue={values} />}\n </main>;\n}\n`,\n };\n for (const [file, route] of [\n [\"index\", `/${base}`],\n [\"create\", `/${base}/create`],\n [\"edit\", `/${base}/:id/edit`],\n ]) {\n files[`${file}.setup.ts`] =\n `import type { PageConfig${file === \"edit\" ? \", PageLoaderContext\" : \"\"} } from \"@warlock.js/web\";\nimport { requireUser } from \"@warlock.js/web/session\";\n\nexport ${\n file === \"edit\"\n ? `async function loader({ request }: PageLoaderContext) {\n const id = String(request.input(\"id\"));\n return { id };\n}`\n : `function loader() { return {}; }`\n }\n\nexport const config = {\n route: { path: \"${route}\", name: \"${base}.${file}\" },\n middleware: [requireUser({ userType: \"user\" })],\n metadata: { title: \"${module.pascal}\" },\n} as const satisfies PageConfig<typeof loader>;\n`;\n }\n return files;\n}\n"],"mappings":"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"}
|
|
@@ -23,7 +23,8 @@ function formatInspectSummary(report) {
|
|
|
23
23
|
["pages", report.pages?.length],
|
|
24
24
|
["jobs", report.jobs?.length],
|
|
25
25
|
["schedules", report.schedules?.length],
|
|
26
|
-
["packages", report.packages?.length]
|
|
26
|
+
["packages", report.packages?.length],
|
|
27
|
+
["files", report.files?.length]
|
|
27
28
|
];
|
|
28
29
|
for (const [label, count] of counts) if (count !== void 0) lines.push(` ${label.padEnd(12)} ${count}`);
|
|
29
30
|
if (report.routes && report.routes.length > 0) {
|
|
@@ -54,9 +55,9 @@ function formatInspectSummary(report) {
|
|
|
54
55
|
async function inspectCommandAction({ options }) {
|
|
55
56
|
const sections = parseInspectSections(stringOption(options.section));
|
|
56
57
|
const json = options.json === true;
|
|
57
|
-
const boot = json ? await withConsoleOnStderr(() => bootForInspect()) : await bootForInspect();
|
|
58
|
+
const boot = sections.every((section) => section === "files") ? void 0 : json ? await withConsoleOnStderr(() => bootForInspect()) : await bootForInspect();
|
|
58
59
|
const report = await inspectApp({
|
|
59
|
-
...inspectOptionsFromBoot(boot),
|
|
60
|
+
...boot ? inspectOptionsFromBoot(boot) : {},
|
|
60
61
|
sections
|
|
61
62
|
});
|
|
62
63
|
if (json) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/inspect/inspect-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { bootForInspect, inspectOptionsFromBoot } from \"../../../tooling/inspect/boot-for-inspect\";\nimport { inspectApp, parseInspectSections } from \"../../../tooling/inspect/inspect-app\";\nimport type { InspectReport } from \"../../../tooling/inspect/inspect-report.types\";\nimport { withConsoleOnStderr } from \"../routes/with-console-on-stderr\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The human summary `warlock inspect` prints without `--json`: one count per\n * section, the routes as `METHOD path [auth] name`, and the warnings.\n * Returned as lines so it is testable.\n */\nexport function formatInspectSummary(report: InspectReport): string[] {\n const lines: string[] = [\n `${report.app.name ?? \"app\"}${report.app.version ? `@${report.app.version}` : \"\"} (${report.app.runtimeStrategy}, Node ${report.app.node})`,\n \"\",\n ];\n\n const counts: [string, number | undefined][] = [\n [\"routes\", report.routes?.length],\n [\"models\", report.models?.length],\n [\"resources\", report.resources?.length],\n [\"config keys\", report.config?.length],\n [\"events\", report.events?.length],\n [\"pages\", report.pages?.length],\n [\"jobs\", report.jobs?.length],\n [\"schedules\", report.schedules?.length],\n [\"packages\", report.packages?.length],\n ];\n\n for (const [label, count] of counts) {\n if (count !== undefined) lines.push(` ${label.padEnd(12)} ${count}`);\n }\n\n if (report.routes && report.routes.length > 0) {\n lines.push(\"\", \"Routes:\");\n\n for (const route of report.routes) {\n const guard = route.auth.required ? \" [auth]\" : \"\";\n const name = route.name ? ` ${route.name}` : \"\";\n\n lines.push(` ${route.method.padEnd(7)} ${route.path}${guard}${name}`);\n }\n }\n\n if (report.warnings.length > 0) {\n lines.push(\"\", `${report.warnings.length} warning(s):`);\n\n for (const warning of report.warnings) {\n lines.push(` - ${warning.code ? `${warning.code} ` : \"\"}${warning.message}`);\n }\n }\n\n lines.push(\"\", \"Run `warlock inspect --json` for the full report.\");\n\n return lines;\n}\n\n/**\n * Action behind `warlock inspect`.\n *\n * Boots the app read-only (the `warlock doctor` boot pass: connectors booted,\n * none started, no connection, no port), then describes it with\n * `inspectApp()`. With `--json`, stdout carries exactly the report and the\n * boot's progress lines go to stderr.\n *\n * @param data Parsed CLI args: `options.json`, `options.section`.\n */\nexport async function inspectCommandAction({ options }: CommandActionData): Promise<void> {\n // Parsed first, so a typo fails before the app boots.\n const sections = parseInspectSections(stringOption(options.section));\n const json = options.json === true;\n const boot = json
|
|
1
|
+
{"version":3,"file":"inspect-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/inspect/inspect-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport type { CommandActionData } from \"../../../commands/types\";\nimport { bootForInspect, inspectOptionsFromBoot } from \"../../../tooling/inspect/boot-for-inspect\";\nimport { inspectApp, parseInspectSections } from \"../../../tooling/inspect/inspect-app\";\nimport type { InspectReport } from \"../../../tooling/inspect/inspect-report.types\";\nimport { withConsoleOnStderr } from \"../routes/with-console-on-stderr\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The human summary `warlock inspect` prints without `--json`: one count per\n * section, the routes as `METHOD path [auth] name`, and the warnings.\n * Returned as lines so it is testable.\n */\nexport function formatInspectSummary(report: InspectReport): string[] {\n const lines: string[] = [\n `${report.app.name ?? \"app\"}${report.app.version ? `@${report.app.version}` : \"\"} (${report.app.runtimeStrategy}, Node ${report.app.node})`,\n \"\",\n ];\n\n const counts: [string, number | undefined][] = [\n [\"routes\", report.routes?.length],\n [\"models\", report.models?.length],\n [\"resources\", report.resources?.length],\n [\"config keys\", report.config?.length],\n [\"events\", report.events?.length],\n [\"pages\", report.pages?.length],\n [\"jobs\", report.jobs?.length],\n [\"schedules\", report.schedules?.length],\n [\"packages\", report.packages?.length],\n [\"files\", report.files?.length],\n ];\n\n for (const [label, count] of counts) {\n if (count !== undefined) lines.push(` ${label.padEnd(12)} ${count}`);\n }\n\n if (report.routes && report.routes.length > 0) {\n lines.push(\"\", \"Routes:\");\n\n for (const route of report.routes) {\n const guard = route.auth.required ? \" [auth]\" : \"\";\n const name = route.name ? ` ${route.name}` : \"\";\n\n lines.push(` ${route.method.padEnd(7)} ${route.path}${guard}${name}`);\n }\n }\n\n if (report.warnings.length > 0) {\n lines.push(\"\", `${report.warnings.length} warning(s):`);\n\n for (const warning of report.warnings) {\n lines.push(` - ${warning.code ? `${warning.code} ` : \"\"}${warning.message}`);\n }\n }\n\n lines.push(\"\", \"Run `warlock inspect --json` for the full report.\");\n\n return lines;\n}\n\n/**\n * Action behind `warlock inspect`.\n *\n * Boots the app read-only (the `warlock doctor` boot pass: connectors booted,\n * none started, no connection, no port), then describes it with\n * `inspectApp()`. With `--json`, stdout carries exactly the report and the\n * boot's progress lines go to stderr.\n *\n * @param data Parsed CLI args: `options.json`, `options.section`.\n */\nexport async function inspectCommandAction({ options }: CommandActionData): Promise<void> {\n // Parsed first, so a typo fails before the app boots.\n const sections = parseInspectSections(stringOption(options.section));\n const json = options.json === true;\n const filesOnly = sections.every((section) => section === \"files\");\n const boot = filesOnly\n ? undefined\n : json\n ? await withConsoleOnStderr(() => bootForInspect())\n : await bootForInspect();\n const report = await inspectApp({ ...(boot ? inspectOptionsFromBoot(boot) : {}), sections });\n\n if (json) {\n console.log(JSON.stringify(report, null, 2));\n return;\n }\n\n const [heading, ...rest] = formatInspectSummary(report);\n\n console.log(colors.bold(heading ?? \"\"));\n\n for (const line of rest) {\n console.log(line);\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,aAAa,OAAkE;CACtF,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;;;;;;AAOA,SAAgB,qBAAqB,QAAiC;CACpE,MAAM,QAAkB,CACtB,GAAG,OAAO,IAAI,QAAQ,QAAQ,OAAO,IAAI,UAAU,IAAI,OAAO,IAAI,YAAY,GAAG,IAAI,OAAO,IAAI,gBAAgB,SAAS,OAAO,IAAI,KAAK,IACzI,EACF;CAEA,MAAM,SAAyC;EAC7C,CAAC,UAAU,OAAO,QAAQ,MAAM;EAChC,CAAC,UAAU,OAAO,QAAQ,MAAM;EAChC,CAAC,aAAa,OAAO,WAAW,MAAM;EACtC,CAAC,eAAe,OAAO,QAAQ,MAAM;EACrC,CAAC,UAAU,OAAO,QAAQ,MAAM;EAChC,CAAC,SAAS,OAAO,OAAO,MAAM;EAC9B,CAAC,QAAQ,OAAO,MAAM,MAAM;EAC5B,CAAC,aAAa,OAAO,WAAW,MAAM;EACtC,CAAC,YAAY,OAAO,UAAU,MAAM;EACpC,CAAC,SAAS,OAAO,OAAO,MAAM;CAChC;CAEA,KAAK,MAAM,CAAC,OAAO,UAAU,QAC3B,IAAI,UAAU,QAAW,MAAM,KAAK,KAAK,MAAM,OAAO,EAAE,EAAE,GAAG,OAAO;CAGtE,IAAI,OAAO,UAAU,OAAO,OAAO,SAAS,GAAG;EAC7C,MAAM,KAAK,IAAI,SAAS;EAExB,KAAK,MAAM,SAAS,OAAO,QAAQ;GACjC,MAAM,QAAQ,MAAM,KAAK,WAAW,YAAY;GAChD,MAAM,OAAO,MAAM,OAAO,KAAK,MAAM,SAAS;GAE9C,MAAM,KAAK,KAAK,MAAM,OAAO,OAAO,CAAC,EAAE,GAAG,MAAM,OAAO,QAAQ,MAAM;EACvE;CACF;CAEA,IAAI,OAAO,SAAS,SAAS,GAAG;EAC9B,MAAM,KAAK,IAAI,GAAG,OAAO,SAAS,OAAO,aAAa;EAEtD,KAAK,MAAM,WAAW,OAAO,UAC3B,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG,QAAQ,KAAK,KAAK,KAAK,QAAQ,SAAS;CAEhF;CAEA,MAAM,KAAK,IAAI,mDAAmD;CAElE,OAAO;AACT;;;;;;;;;;;AAYA,eAAsB,qBAAqB,EAAE,WAA6C;CAExF,MAAM,WAAW,qBAAqB,aAAa,QAAQ,OAAO,CAAC;CACnE,MAAM,OAAO,QAAQ,SAAS;CAE9B,MAAM,OADY,SAAS,OAAO,YAAY,YAAY,OACrC,IACjB,SACA,OACE,MAAM,0BAA0B,eAAe,CAAC,IAChD,MAAM,eAAe;CAC3B,MAAM,SAAS,MAAM,WAAW;EAAE,GAAI,OAAO,uBAAuB,IAAI,IAAI,CAAC;EAAI;CAAS,CAAC;CAE3F,IAAI,MAAM;EACR,QAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;EAC3C;CACF;CAEA,MAAM,CAAC,SAAS,GAAG,QAAQ,qBAAqB,MAAM;CAEtD,QAAQ,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;CAEtC,KAAK,MAAM,QAAQ,MACjB,QAAQ,IAAI,IAAI;AAEpB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { command } from "../../commands/cli-command.mjs";
|
|
2
|
+
import { parseInspectSections } from "../../tooling/inspect/inspect-app.mjs";
|
|
2
3
|
import { inspectCommandAction } from "./inspect/inspect-command.action.mjs";
|
|
3
4
|
|
|
4
5
|
//#region ../../packages/core/src/cli/commands/inspect.command.ts
|
|
@@ -11,17 +12,21 @@ import { inspectCommandAction } from "./inspect/inspect-command.action.mjs";
|
|
|
11
12
|
*
|
|
12
13
|
* Boots like `warlock doctor`: connectors boot (routes register) but none
|
|
13
14
|
* starts, so no database, cache or socket connection opens and no port binds.
|
|
14
|
-
*
|
|
15
|
+
* Files-only requests skip preload and boot and read source statically.
|
|
16
|
+
* Other requests use `runtimeStrategy: "development"`, like doctor.
|
|
15
17
|
*/
|
|
16
18
|
const inspectCommand = command({
|
|
17
19
|
name: "inspect",
|
|
18
|
-
description: "Print the app's routes, models, resources, config keys, events, pages, jobs, schedules and package versions (read-only); --json for the versioned report",
|
|
20
|
+
description: "Print the app's routes, models, resources, config keys, events, pages, jobs, schedules, files and package versions (read-only); --json for the versioned report",
|
|
19
21
|
action: inspectCommandAction,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
preAction: ({ options }) => {
|
|
23
|
+
const sections = parseInspectSections(typeof options.section === "string" ? options.section : void 0);
|
|
24
|
+
inspectCommand.commandPreload = sections.every((section) => section === "files") ? void 0 : {
|
|
25
|
+
runtimeStrategy: "development",
|
|
26
|
+
config: true,
|
|
27
|
+
env: true,
|
|
28
|
+
bootstrap: true
|
|
29
|
+
};
|
|
25
30
|
},
|
|
26
31
|
options: [{
|
|
27
32
|
text: "--json, -j",
|
|
@@ -29,7 +34,7 @@ const inspectCommand = command({
|
|
|
29
34
|
type: "boolean"
|
|
30
35
|
}, {
|
|
31
36
|
text: "--section, -s",
|
|
32
|
-
description: "Comma-separated sections: routes, models, resources, config, events, pages, jobs, schedules, packages",
|
|
37
|
+
description: "Comma-separated sections: routes, models, resources, config, events, pages, jobs, schedules, packages, files",
|
|
33
38
|
type: "string"
|
|
34
39
|
}]
|
|
35
40
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/inspect.command.ts"],"sourcesContent":["import { command } from \"../../commands/cli-command\";\nimport { inspectCommandAction } from \"./inspect/inspect-command.action\";\n\n/**\n * `warlock inspect` — print the app's facts from its booted registries:\n * routes (middleware, guard, rate limit, logging, request/response schemas),\n * models, resources, config keys, event listeners, pages, queue jobs,\n * scheduled tasks and installed `@warlock.js/*` versions. `--json` prints the\n * versioned report (`schemaVersion: 1`); `--section` limits it.\n *\n * Boots like `warlock doctor`: connectors boot (routes register) but none\n * starts, so no database, cache or socket connection opens and no port binds.\n * `runtimeStrategy: \"development\"
|
|
1
|
+
{"version":3,"file":"inspect.command.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/cli/commands/inspect.command.ts"],"sourcesContent":["import { parseInspectSections } from \"../../tooling/inspect/inspect-app\";\nimport { command } from \"../../commands/cli-command\";\nimport { inspectCommandAction } from \"./inspect/inspect-command.action\";\n\n/**\n * `warlock inspect` — print the app's facts from its booted registries:\n * routes (middleware, guard, rate limit, logging, request/response schemas),\n * models, resources, config keys, event listeners, pages, queue jobs,\n * scheduled tasks and installed `@warlock.js/*` versions. `--json` prints the\n * versioned report (`schemaVersion: 1`); `--section` limits it.\n *\n * Boots like `warlock doctor`: connectors boot (routes register) but none\n * starts, so no database, cache or socket connection opens and no port binds.\n * Files-only requests skip preload and boot and read source statically.\n * Other requests use `runtimeStrategy: \"development\"`, like doctor.\n */\nexport const inspectCommand = command({\n name: \"inspect\",\n description:\n \"Print the app's routes, models, resources, config keys, events, pages, jobs, schedules, files and package versions (read-only); --json for the versioned report\",\n action: inspectCommandAction,\n preAction: ({ options }) => {\n const sections = parseInspectSections(\n typeof options.section === \"string\" ? options.section : undefined,\n );\n inspectCommand.commandPreload = sections.every((section) => section === \"files\")\n ? undefined\n : { runtimeStrategy: \"development\", config: true, env: true, bootstrap: true };\n },\n options: [\n {\n text: \"--json, -j\",\n description: \"Print the report as JSON (schemaVersion 1)\",\n type: \"boolean\",\n },\n {\n text: \"--section, -s\",\n description:\n \"Comma-separated sections: routes, models, resources, config, events, pages, jobs, schedules, packages, files\",\n type: \"string\",\n },\n ],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,MAAa,iBAAiB,QAAQ;CACpC,MAAM;CACN,aACE;CACF,QAAQ;CACR,YAAY,EAAE,cAAc;EAC1B,MAAM,WAAW,qBACf,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,MAC1D;EACA,eAAe,iBAAiB,SAAS,OAAO,YAAY,YAAY,OAAO,IAC3E,SACA;GAAE,iBAAiB;GAAe,QAAQ;GAAM,KAAK;GAAM,WAAW;EAAK;CACjF;CACA,SAAS,CACP;EACE,MAAM;EACN,aAAa;EACb,MAAM;CACR,GACA;EACE,MAAM;EACN,aACE;EACF,MAAM;CACR,CACF;AACF,CAAC"}
|
|
@@ -71,7 +71,7 @@ async function mcpCommandAction({ options }) {
|
|
|
71
71
|
const context = createMcpContext({
|
|
72
72
|
allowWrites,
|
|
73
73
|
inspect: async (sections) => inspectApp({
|
|
74
|
-
...await inspectOptions(),
|
|
74
|
+
...sections.every((section) => section === "files") ? {} : await inspectOptions(),
|
|
75
75
|
sections
|
|
76
76
|
}),
|
|
77
77
|
...process.argv[1] ? { cliEntry: process.argv[1] } : {},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/mcp/mcp-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { featuresMap } from \"../../../generations/features\";\nimport { bootForInspect, inspectOptionsFromBoot } from \"../../../tooling/inspect/boot-for-inspect\";\nimport { inspectApp, type InspectOptions } from \"../../../tooling/inspect/inspect-app\";\nimport { createMcpContext } from \"../../../tooling/mcp/create-mcp-context\";\nimport {\n resolveMcpTransport,\n startWarlockMcpServer,\n type WarlockMcpTransport,\n} from \"../../../tooling/mcp/start-mcp-server\";\nimport { getWarlockVersion } from \"../../../utils/framework-vesion\";\nimport { flushAndExit } from \"../../flush-and-exit\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n if (typeof value === \"number\") return String(value);\n\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The transport `warlock mcp` options describe: stdio by default, HTTP with\n * `--http` (and `--host`, `--port`, `--path`, `--token` / `WARLOCK_MCP_TOKEN`).\n * Throws on a port that is not a number.\n */\nexport function mcpTransportFromOptions(\n options: CommandActionData[\"options\"],\n env: NodeJS.ProcessEnv = process.env,\n): WarlockMcpTransport {\n const http =\n options.http === true || stringOption(options.host) !== undefined || options.port !== undefined;\n\n if (!http) return { type: \"stdio\" };\n\n const portText = stringOption(options.port);\n const port = portText === undefined ? 0 : Number(portText);\n\n if (!Number.isInteger(port) || port < 0 || port > 65_535) {\n throw new Error(`--port must be a number from 0 to 65535, got ${JSON.stringify(portText)}.`);\n }\n\n return {\n type: \"http\",\n host: stringOption(options.host) ?? \"127.0.0.1\",\n port,\n path: stringOption(options.path) ?? \"/mcp\",\n token: stringOption(options.token) ?? stringOption(env.WARLOCK_MCP_TOKEN),\n };\n}\n\n/**\n * Send `console.log` / `console.info` / `console.debug` to stderr for the rest\n * of the process: over stdio, stdout carries only JSON-RPC, so a stray log\n * line from the app (or the framework's logger) would corrupt the stream.\n */\nexport function routeConsoleToStderr(): () => void {\n const { log, info, debug } = console;\n const toStderr = (...args: unknown[]) => console.error(...args);\n\n console.log = toStderr;\n console.info = toStderr;\n console.debug = toStderr;\n\n return () => {\n console.log = log;\n console.info = info;\n console.debug = debug;\n };\n}\n\n/** The read-only boot, run once on the first tool call that needs the app. */\nfunction lazyInspectOptions(): () => Promise<InspectOptions> {\n let boot: Promise<InspectOptions> | undefined;\n\n return () => {\n boot ??= bootForInspect().then(inspectOptionsFromBoot);\n\n return boot;\n };\n}\n\n/**\n * Action behind `warlock mcp`.\n *\n * Serves the app's tools over MCP. The app is booted the `warlock inspect`\n * way — connectors booted, none started, no connection, no port — on the\n * first call that needs it, so the server answers `initialize` at once.\n * Runs until stdin closes (stdio) or SIGINT/SIGTERM.\n */\nexport async function mcpCommandAction({ options }: CommandActionData): Promise<void> {\n const transport = mcpTransportFromOptions(options);\n\n // Refuse an unsafe bind before anything boots.\n resolveMcpTransport(transport);\n\n if (transport.type === \"stdio\") routeConsoleToStderr();\n\n const allowWrites = options.allowWrites === true;\n const inspectOptions = lazyInspectOptions();\n const context = createMcpContext({\n allowWrites,\n inspect: async (sections)
|
|
1
|
+
{"version":3,"file":"mcp-command.action.mjs","names":[],"sources":["../../../../../../../../../../packages/core/src/cli/commands/mcp/mcp-command.action.ts"],"sourcesContent":["import type { CommandActionData } from \"../../../commands/types\";\nimport { featuresMap } from \"../../../generations/features\";\nimport { bootForInspect, inspectOptionsFromBoot } from \"../../../tooling/inspect/boot-for-inspect\";\nimport { inspectApp, type InspectOptions } from \"../../../tooling/inspect/inspect-app\";\nimport { createMcpContext } from \"../../../tooling/mcp/create-mcp-context\";\nimport {\n resolveMcpTransport,\n startWarlockMcpServer,\n type WarlockMcpTransport,\n} from \"../../../tooling/mcp/start-mcp-server\";\nimport { getWarlockVersion } from \"../../../utils/framework-vesion\";\nimport { flushAndExit } from \"../../flush-and-exit\";\n\nfunction stringOption(value: string | number | boolean | undefined): string | undefined {\n if (typeof value === \"number\") return String(value);\n\n return typeof value === \"string\" && value.trim().length > 0 ? value.trim() : undefined;\n}\n\n/**\n * The transport `warlock mcp` options describe: stdio by default, HTTP with\n * `--http` (and `--host`, `--port`, `--path`, `--token` / `WARLOCK_MCP_TOKEN`).\n * Throws on a port that is not a number.\n */\nexport function mcpTransportFromOptions(\n options: CommandActionData[\"options\"],\n env: NodeJS.ProcessEnv = process.env,\n): WarlockMcpTransport {\n const http =\n options.http === true || stringOption(options.host) !== undefined || options.port !== undefined;\n\n if (!http) return { type: \"stdio\" };\n\n const portText = stringOption(options.port);\n const port = portText === undefined ? 0 : Number(portText);\n\n if (!Number.isInteger(port) || port < 0 || port > 65_535) {\n throw new Error(`--port must be a number from 0 to 65535, got ${JSON.stringify(portText)}.`);\n }\n\n return {\n type: \"http\",\n host: stringOption(options.host) ?? \"127.0.0.1\",\n port,\n path: stringOption(options.path) ?? \"/mcp\",\n token: stringOption(options.token) ?? stringOption(env.WARLOCK_MCP_TOKEN),\n };\n}\n\n/**\n * Send `console.log` / `console.info` / `console.debug` to stderr for the rest\n * of the process: over stdio, stdout carries only JSON-RPC, so a stray log\n * line from the app (or the framework's logger) would corrupt the stream.\n */\nexport function routeConsoleToStderr(): () => void {\n const { log, info, debug } = console;\n const toStderr = (...args: unknown[]) => console.error(...args);\n\n console.log = toStderr;\n console.info = toStderr;\n console.debug = toStderr;\n\n return () => {\n console.log = log;\n console.info = info;\n console.debug = debug;\n };\n}\n\n/** The read-only boot, run once on the first tool call that needs the app. */\nfunction lazyInspectOptions(): () => Promise<InspectOptions> {\n let boot: Promise<InspectOptions> | undefined;\n\n return () => {\n boot ??= bootForInspect().then(inspectOptionsFromBoot);\n\n return boot;\n };\n}\n\n/**\n * Action behind `warlock mcp`.\n *\n * Serves the app's tools over MCP. The app is booted the `warlock inspect`\n * way — connectors booted, none started, no connection, no port — on the\n * first call that needs it, so the server answers `initialize` at once.\n * Runs until stdin closes (stdio) or SIGINT/SIGTERM.\n */\nexport async function mcpCommandAction({ options }: CommandActionData): Promise<void> {\n const transport = mcpTransportFromOptions(options);\n\n // Refuse an unsafe bind before anything boots.\n resolveMcpTransport(transport);\n\n if (transport.type === \"stdio\") routeConsoleToStderr();\n\n const allowWrites = options.allowWrites === true;\n const inspectOptions = lazyInspectOptions();\n const context = createMcpContext({\n allowWrites,\n inspect: async (sections) =>\n inspectApp({\n ...(sections.every((section) => section === \"files\") ? {} : await inspectOptions()),\n sections,\n }),\n ...(process.argv[1] ? { cliEntry: process.argv[1] } : {}),\n devtools: {\n ...(stringOption(options.url) ? { url: stringOption(options.url) } : {}),\n ...(stringOption(options.devtoolsPath)\n ? { devtoolsPath: stringOption(options.devtoolsPath) }\n : {}),\n },\n features: Object.keys(featuresMap),\n });\n\n const server = await startWarlockMcpServer({\n context,\n transport,\n version: await getWarlockVersion(),\n });\n\n console.error(\n `warlock mcp: ${server.tools.length} tools on ${server.url ?? \"stdio\"} (${allowWrites ? \"writes allowed\" : \"read-only\"})`,\n );\n\n let closing = false;\n\n const stop = async () => {\n if (closing) return;\n closing = true;\n await server.close();\n await flushAndExit(0);\n };\n\n process.once(\"SIGINT\", () => void stop());\n process.once(\"SIGTERM\", () => void stop());\n\n if (transport.type === \"stdio\") {\n process.stdin.once(\"end\", () => void stop());\n }\n}\n"],"mappings":";;;;;;;;;AAaA,SAAS,aAAa,OAAkE;CACtF,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;CAElD,OAAO,OAAO,UAAU,YAAY,MAAM,KAAK,CAAC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI;AAC/E;;;;;;AAOA,SAAgB,wBACd,SACA,MAAyB,QAAQ,KACZ;CAIrB,IAAI,EAFF,QAAQ,SAAS,QAAQ,aAAa,QAAQ,IAAI,MAAM,UAAa,QAAQ,SAAS,SAE7E,OAAO,EAAE,MAAM,QAAQ;CAElC,MAAM,WAAW,aAAa,QAAQ,IAAI;CAC1C,MAAM,OAAO,aAAa,SAAY,IAAI,OAAO,QAAQ;CAEzD,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,MAAM,gDAAgD,KAAK,UAAU,QAAQ,EAAE,EAAE;CAG7F,OAAO;EACL,MAAM;EACN,MAAM,aAAa,QAAQ,IAAI,KAAK;EACpC;EACA,MAAM,aAAa,QAAQ,IAAI,KAAK;EACpC,OAAO,aAAa,QAAQ,KAAK,KAAK,aAAa,IAAI,iBAAiB;CAC1E;AACF;;;;;;AAOA,SAAgB,uBAAmC;CACjD,MAAM,EAAE,KAAK,MAAM,UAAU;CAC7B,MAAM,YAAY,GAAG,SAAoB,QAAQ,MAAM,GAAG,IAAI;CAE9D,QAAQ,MAAM;CACd,QAAQ,OAAO;CACf,QAAQ,QAAQ;CAEhB,aAAa;EACX,QAAQ,MAAM;EACd,QAAQ,OAAO;EACf,QAAQ,QAAQ;CAClB;AACF;;AAGA,SAAS,qBAAoD;CAC3D,IAAI;CAEJ,aAAa;EACX,SAAS,eAAe,CAAC,CAAC,KAAK,sBAAsB;EAErD,OAAO;CACT;AACF;;;;;;;;;AAUA,eAAsB,iBAAiB,EAAE,WAA6C;CACpF,MAAM,YAAY,wBAAwB,OAAO;CAGjD,oBAAoB,SAAS;CAE7B,IAAI,UAAU,SAAS,SAAS,qBAAqB;CAErD,MAAM,cAAc,QAAQ,gBAAgB;CAC5C,MAAM,iBAAiB,mBAAmB;CAC1C,MAAM,UAAU,iBAAiB;EAC/B;EACA,SAAS,OAAO,aACd,WAAW;GACT,GAAI,SAAS,OAAO,YAAY,YAAY,OAAO,IAAI,CAAC,IAAI,MAAM,eAAe;GACjF;EACF,CAAC;EACH,GAAI,QAAQ,KAAK,KAAK,EAAE,UAAU,QAAQ,KAAK,GAAG,IAAI,CAAC;EACvD,UAAU;GACR,GAAI,aAAa,QAAQ,GAAG,IAAI,EAAE,KAAK,aAAa,QAAQ,GAAG,EAAE,IAAI,CAAC;GACtE,GAAI,aAAa,QAAQ,YAAY,IACjC,EAAE,cAAc,aAAa,QAAQ,YAAY,EAAE,IACnD,CAAC;EACP;EACA,UAAU,OAAO,KAAK,WAAW;CACnC,CAAC;CAED,MAAM,SAAS,MAAM,sBAAsB;EACzC;EACA;EACA,SAAS,MAAM,kBAAkB;CACnC,CAAC;CAED,QAAQ,MACN,gBAAgB,OAAO,MAAM,OAAO,YAAY,OAAO,OAAO,QAAQ,IAAI,cAAc,mBAAmB,YAAY,EACzH;CAEA,IAAI,UAAU;CAEd,MAAM,OAAO,YAAY;EACvB,IAAI,SAAS;EACb,UAAU;EACV,MAAM,OAAO,MAAM;EACnB,MAAM,aAAa,CAAC;CACtB;CAEA,QAAQ,KAAK,gBAAgB,KAAK,KAAK,CAAC;CACxC,QAAQ,KAAK,iBAAiB,KAAK,KAAK,CAAC;CAEzC,IAAI,UAAU,SAAS,SACrB,QAAQ,MAAM,KAAK,aAAa,KAAK,KAAK,CAAC;AAE/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/config/config-loader.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"config-loader.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/config/config-loader.ts"],"mappings":";;;;;YAkBY,wBAAwB,+BAA+B"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { configSpecialHandlers } from "./config-special-handlers.mjs";
|
|
2
2
|
import { InvalidConfigError, validateConfigValue } from "./config-schema.mjs";
|
|
3
|
+
import { isConfigFile } from "../dev-server/special-file-patterns.mjs";
|
|
3
4
|
import { importAppModule } from "../loader/app-module-importer.mjs";
|
|
4
5
|
import { configKeyFromPath } from "./config-key-from-path.mjs";
|
|
5
6
|
import baseConfig from "@mongez/config";
|
|
@@ -40,6 +41,7 @@ var ConfigLoader = class {
|
|
|
40
41
|
* the running app keeps its last valid configuration.
|
|
41
42
|
*/
|
|
42
43
|
async loadConfigFile(file, whenInvalid) {
|
|
44
|
+
if (!isConfigFile(file.relativePath)) return [];
|
|
43
45
|
const configName = this.getConfigName(file.relativePath);
|
|
44
46
|
const fileUrl = pathToFileURL(file.absolutePath).href;
|
|
45
47
|
const configModule = await importAppModule(fileUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.mjs","names":[],"sources":["../../../../../../../../packages/core/src/config/config-loader.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { colors } from \"@mongez/copper\";\nimport { pathToFileURL } from \"node:url\";\nimport type { FileManager } from \"../dev-server/file-manager\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { configKeyFromPath } from \"./config-key-from-path\";\nimport { type ConfigIssue, InvalidConfigError, validateConfigValue } from \"./config-schema\";\nimport { configSpecialHandlers } from \"./config-special-handlers\";\n\n/**\n * Special Config Handler\n * A function that handles special configuration loading\n */\n// Returns unknown, not void: ConfigSpecialHandlers.execute() RETURNS handler(config)\n// rather than discarding it, and a test pins that (\"returns the handler's resolved\n// value\"). The implementation and the test agreed; only this type said handlers\n// produce nothing, which made any value-returning handler fail to typecheck.\nexport type SpecialConfigHandler = (configValue: any) => unknown | Promise<unknown>;\n\n/**\n * Config Loader\n * Dynamically loads all configuration files and registers them with @mongez/config\n * Supports special handlers for configs that require additional processing\n */\nexport class ConfigLoader {\n /**\n * Load every config file, then throw one {@link InvalidConfigError} listing\n * every value that fails its file's `configSchema`. An invalid config is\n * still registered (later files may read it while loading) but its special\n * handler does not run, so nothing connects with a broken configuration.\n */\n public async loadAll(configFiles: FileManager[]): Promise<void> {\n const issues: ConfigIssue[] = [];\n\n for (const file of configFiles) {\n issues.push(...(await this.loadConfigFile(file, \"register-invalid\")));\n }\n\n if (issues.length > 0) throw new InvalidConfigError(issues);\n }\n\n /**\n * Load a single configuration file.\n *\n * The ESM loader hook stamps a version token onto the URL so that each HMR\n * cycle gets a fresh module — no manual cache-busting needed here.\n */\n public async loadConfig(file: FileManager): Promise<void> {\n const issues = await this.loadConfigFile(file, \"keep-previous\");\n\n if (issues.length > 0) throw new InvalidConfigError(issues);\n }\n\n /**\n * Import, validate and register one config file. Returns the schema issues.\n * On a hot reload (`keep-previous`) an invalid value is not registered, so\n * the running app keeps its last valid configuration.\n */\n private async loadConfigFile(\n file: FileManager,\n whenInvalid: \"register-invalid\" | \"keep-previous\",\n ): Promise<ConfigIssue[]> {\n const configName = this.getConfigName(file.relativePath);\n\n const fileUrl = pathToFileURL(file.absolutePath).href;\n const configModule = await importAppModule(fileUrl);\n const configValue = configModule.default;\n\n if (configValue === undefined) {\n console.log(\n colors.red(`config error: `),\n `Config file ${colors.yellow(file.relativePath)} does not have a default export`,\n );\n return [];\n }\n\n const issues = await validateConfigValue(configName, configValue, configModule.configSchema);\n\n if (issues.length > 0) {\n if (whenInvalid === \"register-invalid\") config.set(configName, configValue);\n return issues;\n }\n\n config.set(configName, configValue);\n await configSpecialHandlers.execute(configName, configValue);\n\n return [];\n }\n\n public async reloadConfig(file: FileManager): Promise<void> {\n await this.loadConfig(file);\n }\n\n private getConfigName(relativePath: string): string {\n const configName = configKeyFromPath(relativePath);\n\n if (!configName) {\n throw new Error(`Invalid config file path: ${relativePath}`);\n }\n\n return configName;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"config-loader.mjs","names":[],"sources":["../../../../../../../../packages/core/src/config/config-loader.ts"],"sourcesContent":["import { isConfigFile } from \"../dev-server/special-file-patterns\";\nimport config from \"@mongez/config\";\nimport { colors } from \"@mongez/copper\";\nimport { pathToFileURL } from \"node:url\";\nimport type { FileManager } from \"../dev-server/file-manager\";\nimport { importAppModule } from \"../loader/app-module-importer\";\nimport { configKeyFromPath } from \"./config-key-from-path\";\nimport { type ConfigIssue, InvalidConfigError, validateConfigValue } from \"./config-schema\";\nimport { configSpecialHandlers } from \"./config-special-handlers\";\n\n/**\n * Special Config Handler\n * A function that handles special configuration loading\n */\n// Returns unknown, not void: ConfigSpecialHandlers.execute() RETURNS handler(config)\n// rather than discarding it, and a test pins that (\"returns the handler's resolved\n// value\"). The implementation and the test agreed; only this type said handlers\n// produce nothing, which made any value-returning handler fail to typecheck.\nexport type SpecialConfigHandler = (configValue: any) => unknown | Promise<unknown>;\n\n/**\n * Config Loader\n * Dynamically loads all configuration files and registers them with @mongez/config\n * Supports special handlers for configs that require additional processing\n */\nexport class ConfigLoader {\n /**\n * Load every config file, then throw one {@link InvalidConfigError} listing\n * every value that fails its file's `configSchema`. An invalid config is\n * still registered (later files may read it while loading) but its special\n * handler does not run, so nothing connects with a broken configuration.\n */\n public async loadAll(configFiles: FileManager[]): Promise<void> {\n const issues: ConfigIssue[] = [];\n\n for (const file of configFiles) {\n issues.push(...(await this.loadConfigFile(file, \"register-invalid\")));\n }\n\n if (issues.length > 0) throw new InvalidConfigError(issues);\n }\n\n /**\n * Load a single configuration file.\n *\n * The ESM loader hook stamps a version token onto the URL so that each HMR\n * cycle gets a fresh module — no manual cache-busting needed here.\n */\n public async loadConfig(file: FileManager): Promise<void> {\n const issues = await this.loadConfigFile(file, \"keep-previous\");\n\n if (issues.length > 0) throw new InvalidConfigError(issues);\n }\n\n /**\n * Import, validate and register one config file. Returns the schema issues.\n * On a hot reload (`keep-previous`) an invalid value is not registered, so\n * the running app keeps its last valid configuration.\n */\n private async loadConfigFile(\n file: FileManager,\n whenInvalid: \"register-invalid\" | \"keep-previous\",\n ): Promise<ConfigIssue[]> {\n if (!isConfigFile(file.relativePath)) return [];\n\n const configName = this.getConfigName(file.relativePath);\n\n const fileUrl = pathToFileURL(file.absolutePath).href;\n const configModule = await importAppModule(fileUrl);\n const configValue = configModule.default;\n\n if (configValue === undefined) {\n console.log(\n colors.red(`config error: `),\n `Config file ${colors.yellow(file.relativePath)} does not have a default export`,\n );\n return [];\n }\n\n const issues = await validateConfigValue(configName, configValue, configModule.configSchema);\n\n if (issues.length > 0) {\n if (whenInvalid === \"register-invalid\") config.set(configName, configValue);\n return issues;\n }\n\n config.set(configName, configValue);\n await configSpecialHandlers.execute(configName, configValue);\n\n return [];\n }\n\n public async reloadConfig(file: FileManager): Promise<void> {\n await this.loadConfig(file);\n }\n\n private getConfigName(relativePath: string): string {\n const configName = configKeyFromPath(relativePath);\n\n if (!configName) {\n throw new Error(`Invalid config file path: ${relativePath}`);\n }\n\n return configName;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAyBA,IAAa,eAAb,MAA0B;;;;;;;CAOxB,MAAa,QAAQ,aAA2C;EAC9D,MAAM,SAAwB,CAAC;EAE/B,KAAK,MAAM,QAAQ,aACjB,OAAO,KAAK,GAAI,MAAM,KAAK,eAAe,MAAM,kBAAkB,CAAE;EAGtE,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,mBAAmB,MAAM;CAC5D;;;;;;;CAQA,MAAa,WAAW,MAAkC;EACxD,MAAM,SAAS,MAAM,KAAK,eAAe,MAAM,eAAe;EAE9D,IAAI,OAAO,SAAS,GAAG,MAAM,IAAI,mBAAmB,MAAM;CAC5D;;;;;;CAOA,MAAc,eACZ,MACA,aACwB;EACxB,IAAI,CAAC,aAAa,KAAK,YAAY,GAAG,OAAO,CAAC;EAE9C,MAAM,aAAa,KAAK,cAAc,KAAK,YAAY;EAEvD,MAAM,UAAU,cAAc,KAAK,YAAY,CAAC,CAAC;EACjD,MAAM,eAAe,MAAM,gBAAgB,OAAO;EAClD,MAAM,cAAc,aAAa;EAEjC,IAAI,gBAAgB,QAAW;GAC7B,QAAQ,IACN,OAAO,IAAI,gBAAgB,GAC3B,eAAe,OAAO,OAAO,KAAK,YAAY,EAAE,gCAClD;GACA,OAAO,CAAC;EACV;EAEA,MAAM,SAAS,MAAM,oBAAoB,YAAY,aAAa,aAAa,YAAY;EAE3F,IAAI,OAAO,SAAS,GAAG;GACrB,IAAI,gBAAgB,oBAAoB,WAAO,IAAI,YAAY,WAAW;GAC1E,OAAO;EACT;EAEA,WAAO,IAAI,YAAY,WAAW;EAClC,MAAM,sBAAsB,QAAQ,YAAY,WAAW;EAE3D,OAAO,CAAC;CACV;CAEA,MAAa,aAAa,MAAkC;EAC1D,MAAM,KAAK,WAAW,IAAI;CAC5B;CAEA,AAAQ,cAAc,cAA8B;EAClD,MAAM,aAAa,kBAAkB,YAAY;EAEjD,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,6BAA6B,cAAc;EAG7D,OAAO;CACT;AACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Path } from "../utils/normalized-path.mjs";
|
|
2
2
|
import { srcPath } from "../utils/paths.mjs";
|
|
3
3
|
import "../utils/index.mjs";
|
|
4
|
+
import { isConfigFile } from "../dev-server/special-file-patterns.mjs";
|
|
4
5
|
import { getCertainFilesFromDirectory, getFilesFromDirectory } from "../dev-server/utils.mjs";
|
|
5
6
|
import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
|
|
6
7
|
import { configManager } from "./config-manager.mjs";
|
|
@@ -14,6 +15,9 @@ async function loadConfigFiles(config) {
|
|
|
14
15
|
let relativePaths = [];
|
|
15
16
|
if (config === true) relativePaths = (await getFilesFromDirectory(srcPath("config"))).map((path) => Path.toRelative(path));
|
|
16
17
|
else relativePaths = (await getCertainFilesFromDirectory(srcPath("config"), config)).map((path) => Path.toRelative(path));
|
|
18
|
+
relativePaths = relativePaths.filter(isConfigFile);
|
|
19
|
+
if (relativePaths.length === 0) return;
|
|
20
|
+
await filesOrchestrator.init();
|
|
17
21
|
const configFiles = await Promise.all(relativePaths.map((relativePath) => filesOrchestrator.add(relativePath)));
|
|
18
22
|
filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.files);
|
|
19
23
|
await configManager.loadAll(configFiles);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-config-files.mjs","names":[],"sources":["../../../../../../../../packages/core/src/config/load-config-files.ts"],"sourcesContent":["import { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getCertainFilesFromDirectory, getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { configManager } from \"./config-manager\";\n\n/**\n * Load config files\n * either config file names (without extensions) or true to load all config files\n */\nexport async function loadConfigFiles(config: string[] | true) {\n let relativePaths: string[] = [];\n if (config === true) {\n const configFilesAbsulatePaths = await getFilesFromDirectory(srcPath(\"config\"));\n relativePaths = configFilesAbsulatePaths.map((path) => Path.toRelative(path));\n } else {\n // define only the given config files\n\n const configFilesAbsulatePaths = await getCertainFilesFromDirectory(srcPath(\"config\"), config);\n\n relativePaths = configFilesAbsulatePaths.map((path) => Path.toRelative(path));\n }\n\n const configFiles = await Promise.all(\n relativePaths.map((relativePath) => filesOrchestrator.add(relativePath)),\n );\n\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.files);\n\n await configManager.loadAll(configFiles);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"load-config-files.mjs","names":[],"sources":["../../../../../../../../packages/core/src/config/load-config-files.ts"],"sourcesContent":["import { isConfigFile } from \"../dev-server/special-file-patterns\";\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\nimport { Path } from \"../utils/normalized-path\";\nimport { getCertainFilesFromDirectory, getFilesFromDirectory } from \"../dev-server/utils\";\nimport { srcPath } from \"../utils\";\nimport { configManager } from \"./config-manager\";\n\n/**\n * Load config files\n * either config file names (without extensions) or true to load all config files\n */\nexport async function loadConfigFiles(config: string[] | true) {\n let relativePaths: string[] = [];\n if (config === true) {\n const configFilesAbsulatePaths = await getFilesFromDirectory(srcPath(\"config\"));\n relativePaths = configFilesAbsulatePaths.map((path) => Path.toRelative(path));\n } else {\n // define only the given config files\n\n const configFilesAbsulatePaths = await getCertainFilesFromDirectory(srcPath(\"config\"), config);\n\n relativePaths = configFilesAbsulatePaths.map((path) => Path.toRelative(path));\n }\n\n relativePaths = relativePaths.filter(isConfigFile);\n if (relativePaths.length === 0) return;\n\n // Config-only commands need no loader when the requested files are absent.\n await filesOrchestrator.init();\n\n const configFiles = await Promise.all(\n relativePaths.map((relativePath) => filesOrchestrator.add(relativePath)),\n );\n\n filesOrchestrator.specialFilesCollector.collect(filesOrchestrator.files);\n\n await configManager.loadAll(configFiles);\n}\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAsB,gBAAgB,QAAyB;CAC7D,IAAI,gBAA0B,CAAC;CAC/B,IAAI,WAAW,MAEb,iBAAgB,MADuB,sBAAsB,QAAQ,QAAQ,CAAC,EACtC,CAAC,KAAK,SAAS,KAAK,WAAW,IAAI,CAAC;MAM5E,iBAAgB,MAFuB,6BAA6B,QAAQ,QAAQ,GAAG,MAAM,EAErD,CAAC,KAAK,SAAS,KAAK,WAAW,IAAI,CAAC;CAG9E,gBAAgB,cAAc,OAAO,YAAY;CACjD,IAAI,cAAc,WAAW,GAAG;CAGhC,MAAM,kBAAkB,KAAK;CAE7B,MAAM,cAAc,MAAM,QAAQ,IAChC,cAAc,KAAK,iBAAiB,kBAAkB,IAAI,YAAY,CAAC,CACzE;CAEA,kBAAkB,sBAAsB,QAAQ,kBAAkB,KAAK;CAEvE,MAAM,cAAc,QAAQ,WAAW;AACzC"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Path } from "../utils/normalized-path.mjs";
|
|
2
|
+
import { isCodeFile } from "./special-file-patterns.mjs";
|
|
2
3
|
import { devLogSuccess } from "./dev-logger.mjs";
|
|
3
4
|
import { isTimingsEnabled } from "./flags.mjs";
|
|
4
5
|
import { clearFileExistsCache } from "./parse-imports.mjs";
|
|
5
|
-
import { isCodeFile } from "./special-file-patterns.mjs";
|
|
6
6
|
import events from "@mongez/events";
|
|
7
7
|
|
|
8
8
|
//#region ../../packages/core/src/dev-server/file-event-handler.ts
|
|
@@ -27,6 +27,16 @@ var FileEventHandler = class {
|
|
|
27
27
|
this.batchQueue = Promise.resolve();
|
|
28
28
|
this.includeMatcher = () => false;
|
|
29
29
|
}
|
|
30
|
+
async dispose() {
|
|
31
|
+
clearTimeout(this.debounceTimer);
|
|
32
|
+
clearTimeout(this.maxWaitTimer);
|
|
33
|
+
this.debounceTimer = void 0;
|
|
34
|
+
this.maxWaitTimer = void 0;
|
|
35
|
+
this.pendingAdds.clear();
|
|
36
|
+
this.pendingChanges.clear();
|
|
37
|
+
this.pendingDeletes.clear();
|
|
38
|
+
await this.batchQueue;
|
|
39
|
+
}
|
|
30
40
|
/**
|
|
31
41
|
* Non-code files in the include set (`.sql` fixtures, JSON config) can't be
|
|
32
42
|
* hot-reloaded, so they are reported as restart triggers instead.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE, isTimingsEnabled } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { isCodeFile } from \"./special-file-patterns\";\nimport { Path } from \"../utils/normalized-path\";\n\n/** Lets an isolated editor save reach HMR without the former fixed 50ms delay. */\nconst ISOLATED_SAVE_QUIET_WINDOW_MS = 12;\n\n/** Bounds a sustained formatter or checkout stream so it cannot postpone HMR forever. */\nconst BATCH_MAX_WAIT_MS = 60;\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n /** When this batch's first watcher event arrived — the debounce-wait phase start. */\n private batchStartedAt?: number;\n\n /** Slowest watcher-settle duration seen so far this batch, when `devServer.timings` is on. */\n private watcherSettleMs?: number;\n\n /** Tail of the batch-processing chain; keeps batches strictly sequential. */\n private batchQueue: Promise<void> = Promise.resolve();\n\n private debounceTimer?: ReturnType<typeof setTimeout>;\n private maxWaitTimer?: ReturnType<typeof setTimeout>;\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n /** Matches paths the user listed in `devServer.watch.include`. */\n private includeMatcher: (path: string) => boolean = () => false;\n\n /**\n * Non-code files in the include set (`.sql` fixtures, JSON config) can't be\n * hot-reloaded, so they are reported as restart triggers instead.\n */\n public setIncludeMatcher(matcher: (path: string) => boolean): void {\n this.includeMatcher = matcher;\n }\n\n public handleFileChange(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileAdd(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileDelete(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n private schedulePendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = setTimeout(() => this.flushPendingEvents(), ISOLATED_SAVE_QUIET_WINDOW_MS);\n\n if (this.maxWaitTimer === undefined) {\n this.maxWaitTimer = setTimeout(() => this.flushPendingEvents(), BATCH_MAX_WAIT_MS);\n }\n }\n\n private flushPendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = undefined;\n }\n\n if (this.maxWaitTimer !== undefined) {\n clearTimeout(this.maxWaitTimer);\n this.maxWaitTimer = undefined;\n }\n\n // Serialize: a batch arriving while the previous one is in flight queues\n // behind it (pending sets keep coalescing meanwhile) instead of racing it.\n this.batchQueue = this.batchQueue\n .then(() => this.processBatch())\n .catch((error) => {\n console.error(\"[dev-server] batch processing failed\", error);\n });\n }\n\n /**\n * Mark the debounce-wait phase start on the first event of a batch, and\n * track the slowest watcher-settle duration seen this batch. `settleMs`\n * only arrives when `devServer.timings` is on (see `FilesWatcher`), so\n * this is a no-op past the cheap `performance.now()` call when it's off.\n */\n private recordTimingStart(settleMs?: number): void {\n if (this.batchStartedAt === undefined) {\n this.batchStartedAt = performance.now();\n }\n\n if (settleMs === undefined) return;\n\n this.watcherSettleMs =\n this.watcherSettleMs === undefined ? settleMs : Math.max(this.watcherSettleMs, settleMs);\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n const debounceWaitMs =\n this.batchStartedAt !== undefined ? performance.now() - this.batchStartedAt : undefined;\n const watcherSettleMs = this.watcherSettleMs;\n this.batchStartedAt = undefined;\n this.watcherSettleMs = undefined;\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const isIncludedData = (p: string) => !isCodeFile(p) && this.includeMatcher(p);\n const isExternal = (p: string) => isExternalPath(p) || isIncludedData(p);\n const externalChanges = changes.filter(isExternal);\n const externalAdds = adds.filter(isExternal);\n const codeChanges = changes.filter((p) => !isExternal(p));\n const codeAdds = adds.filter((p) => !isExternal(p));\n const restartTriggers = [...changes, ...adds, ...deletes].filter(isIncludedData);\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them. Paths\n // that vanished between the add event and their read/stat (a rename or\n // move racing the filesystem) are folded into `deleted` instead of\n // `added` — they were already unwound as deletions in processBatchAdds.\n events.trigger(\"dev-server:batch-complete\", {\n added: [...externalAdds, ...addedCodePaths],\n changed: [...externalChanges, ...changedCodePaths],\n deleted: [...deletes.filter((p) => !isIncludedData(p)), ...vanished],\n restartTriggers,\n timings: isTimingsEnabled()\n ? { watcherSettleMs: watcherSettleMs ?? 0, debounceWaitMs: debounceWaitMs ?? 0 }\n : undefined,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n /**\n * Process pending add events. Returns the paths that genuinely became\n * available (`added`) separately from paths that no longer existed by the\n * time they were read/stat'd (`vanished`) — the latter is a rename or move\n * racing the filesystem, not a failure, so it is unwound as a deletion\n * instead of being logged as an error.\n */\n private async processBatchAdds(\n relativePaths: string[],\n ): Promise<{ added: string[]; vanished: string[] }> {\n const added: string[] = [];\n const vanished: string[] = [];\n\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n const fileManager = await this.fileOperations.addFile(path);\n\n if (fileManager.state === \"deleted\") {\n await this.fileOperations.deleteFile(path);\n vanished.push(path);\n return;\n }\n\n added.push(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n\n return { added, vanished };\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../../packages/core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE, isTimingsEnabled } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { isCodeFile } from \"./special-file-patterns\";\nimport { Path } from \"../utils/normalized-path\";\n\n/** Lets an isolated editor save reach HMR without the former fixed 50ms delay. */\nconst ISOLATED_SAVE_QUIET_WINDOW_MS = 12;\n\n/** Bounds a sustained formatter or checkout stream so it cannot postpone HMR forever. */\nconst BATCH_MAX_WAIT_MS = 60;\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n /** When this batch's first watcher event arrived — the debounce-wait phase start. */\n private batchStartedAt?: number;\n\n /** Slowest watcher-settle duration seen so far this batch, when `devServer.timings` is on. */\n private watcherSettleMs?: number;\n\n /** Tail of the batch-processing chain; keeps batches strictly sequential. */\n private batchQueue: Promise<void> = Promise.resolve();\n\n private debounceTimer?: ReturnType<typeof setTimeout>;\n private maxWaitTimer?: ReturnType<typeof setTimeout>;\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n public async dispose(): Promise<void> {\n clearTimeout(this.debounceTimer);\n clearTimeout(this.maxWaitTimer);\n this.debounceTimer = undefined;\n this.maxWaitTimer = undefined;\n this.pendingAdds.clear();\n this.pendingChanges.clear();\n this.pendingDeletes.clear();\n await this.batchQueue;\n }\n\n /** Matches paths the user listed in `devServer.watch.include`. */\n private includeMatcher: (path: string) => boolean = () => false;\n\n /**\n * Non-code files in the include set (`.sql` fixtures, JSON config) can't be\n * hot-reloaded, so they are reported as restart triggers instead.\n */\n public setIncludeMatcher(matcher: (path: string) => boolean): void {\n this.includeMatcher = matcher;\n }\n\n public handleFileChange(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileAdd(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileDelete(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n private schedulePendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = setTimeout(() => this.flushPendingEvents(), ISOLATED_SAVE_QUIET_WINDOW_MS);\n\n if (this.maxWaitTimer === undefined) {\n this.maxWaitTimer = setTimeout(() => this.flushPendingEvents(), BATCH_MAX_WAIT_MS);\n }\n }\n\n private flushPendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = undefined;\n }\n\n if (this.maxWaitTimer !== undefined) {\n clearTimeout(this.maxWaitTimer);\n this.maxWaitTimer = undefined;\n }\n\n // Serialize: a batch arriving while the previous one is in flight queues\n // behind it (pending sets keep coalescing meanwhile) instead of racing it.\n this.batchQueue = this.batchQueue\n .then(() => this.processBatch())\n .catch((error) => {\n console.error(\"[dev-server] batch processing failed\", error);\n });\n }\n\n /**\n * Mark the debounce-wait phase start on the first event of a batch, and\n * track the slowest watcher-settle duration seen this batch. `settleMs`\n * only arrives when `devServer.timings` is on (see `FilesWatcher`), so\n * this is a no-op past the cheap `performance.now()` call when it's off.\n */\n private recordTimingStart(settleMs?: number): void {\n if (this.batchStartedAt === undefined) {\n this.batchStartedAt = performance.now();\n }\n\n if (settleMs === undefined) return;\n\n this.watcherSettleMs =\n this.watcherSettleMs === undefined ? settleMs : Math.max(this.watcherSettleMs, settleMs);\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n const debounceWaitMs =\n this.batchStartedAt !== undefined ? performance.now() - this.batchStartedAt : undefined;\n const watcherSettleMs = this.watcherSettleMs;\n this.batchStartedAt = undefined;\n this.watcherSettleMs = undefined;\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const isIncludedData = (p: string) => !isCodeFile(p) && this.includeMatcher(p);\n const isExternal = (p: string) => isExternalPath(p) || isIncludedData(p);\n const externalChanges = changes.filter(isExternal);\n const externalAdds = adds.filter(isExternal);\n const codeChanges = changes.filter((p) => !isExternal(p));\n const codeAdds = adds.filter((p) => !isExternal(p));\n const restartTriggers = [...changes, ...adds, ...deletes].filter(isIncludedData);\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them. Paths\n // that vanished between the add event and their read/stat (a rename or\n // move racing the filesystem) are folded into `deleted` instead of\n // `added` — they were already unwound as deletions in processBatchAdds.\n events.trigger(\"dev-server:batch-complete\", {\n added: [...externalAdds, ...addedCodePaths],\n changed: [...externalChanges, ...changedCodePaths],\n deleted: [...deletes.filter((p) => !isIncludedData(p)), ...vanished],\n restartTriggers,\n timings: isTimingsEnabled()\n ? { watcherSettleMs: watcherSettleMs ?? 0, debounceWaitMs: debounceWaitMs ?? 0 }\n : undefined,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n /**\n * Process pending add events. Returns the paths that genuinely became\n * available (`added`) separately from paths that no longer existed by the\n * time they were read/stat'd (`vanished`) — the latter is a rename or move\n * racing the filesystem, not a failure, so it is unwound as a deletion\n * instead of being logged as an error.\n */\n private async processBatchAdds(\n relativePaths: string[],\n ): Promise<{ added: string[]; vanished: string[] }> {\n const added: string[] = [];\n const vanished: string[] = [];\n\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n const fileManager = await this.fileOperations.addFile(path);\n\n if (fileManager.state === \"deleted\") {\n await this.fileOperations.deleteFile(path);\n vanished.push(path);\n return;\n }\n\n added.push(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n\n return { added, vanished };\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":";;;;;;;;;AAYA,MAAM,gCAAgC;;AAGtC,MAAM,oBAAoB;;;;;;AAO1B,IAAa,mBAAb,MAA8B;CAiB5B,YACE,AAAiB,gBACjB,AAAiB,UACjB,AAAiB,iBACjB,AAAiB,OACjB;EAJiB;EACA;EACA;EACA;wCApBM,IAAI,IAAY;qCACnB,IAAI,IAAY;wCACb,IAAI,IAAY;oBASL,QAAQ,QAAQ;8BAwBM;CAdvD;CAEH,MAAa,UAAyB;EACpC,aAAa,KAAK,aAAa;EAC/B,aAAa,KAAK,YAAY;EAC9B,KAAK,gBAAgB;EACrB,KAAK,eAAe;EACpB,KAAK,YAAY,MAAM;EACvB,KAAK,eAAe,MAAM;EAC1B,KAAK,eAAe,MAAM;EAC1B,MAAM,KAAK;CACb;;;;;CASA,AAAO,kBAAkB,SAA0C;EACjE,KAAK,iBAAiB;CACxB;CAEA,AAAO,iBAAiB,cAAsB,UAAyB;EACrE,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,eAAe,IAAI,KAAK,WAAW,YAAY,CAAC;EACrD,KAAK,sBAAsB;CAC7B;CAEA,AAAO,cAAc,cAAsB,UAAyB;EAClE,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,YAAY,IAAI,KAAK,WAAW,YAAY,CAAC;EAClD,KAAK,sBAAsB;CAC7B;CAEA,AAAO,iBAAiB,cAAsB,UAAyB;EACrE,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,eAAe,IAAI,KAAK,WAAW,YAAY,CAAC;EACrD,KAAK,sBAAsB;CAC7B;CAEA,AAAQ,wBAA8B;EACpC,IAAI,KAAK,kBAAkB,QACzB,aAAa,KAAK,aAAa;EAGjC,KAAK,gBAAgB,iBAAiB,KAAK,mBAAmB,GAAG,6BAA6B;EAE9F,IAAI,KAAK,iBAAiB,QACxB,KAAK,eAAe,iBAAiB,KAAK,mBAAmB,GAAG,iBAAiB;CAErF;CAEA,AAAQ,qBAA2B;EACjC,IAAI,KAAK,kBAAkB,QAAW;GACpC,aAAa,KAAK,aAAa;GAC/B,KAAK,gBAAgB;EACvB;EAEA,IAAI,KAAK,iBAAiB,QAAW;GACnC,aAAa,KAAK,YAAY;GAC9B,KAAK,eAAe;EACtB;EAIA,KAAK,aAAa,KAAK,WACpB,WAAW,KAAK,aAAa,CAAC,CAAC,CAC/B,OAAO,UAAU;GAChB,QAAQ,MAAM,wCAAwC,KAAK;EAC7D,CAAC;CACL;;;;;;;CAQA,AAAQ,kBAAkB,UAAyB;EACjD,IAAI,KAAK,mBAAmB,QAC1B,KAAK,iBAAiB,YAAY,IAAI;EAGxC,IAAI,aAAa,QAAW;EAE5B,KAAK,kBACH,KAAK,oBAAoB,SAAY,WAAW,KAAK,IAAI,KAAK,iBAAiB,QAAQ;CAC3F;CAEA,MAAc,eAA8B;EAC1C,MAAM,UAAU,MAAM,KAAK,KAAK,cAAc;EAC9C,MAAM,OAAO,MAAM,KAAK,KAAK,WAAW;EACxC,MAAM,UAAU,MAAM,KAAK,KAAK,cAAc;EAE9C,KAAK,eAAe,MAAM;EAC1B,KAAK,YAAY,MAAM;EACvB,KAAK,eAAe,MAAM;EAE1B,MAAM,iBACJ,KAAK,mBAAmB,SAAY,YAAY,IAAI,IAAI,KAAK,iBAAiB;EAChF,MAAM,kBAAkB,KAAK;EAC7B,KAAK,iBAAiB;EACtB,KAAK,kBAAkB;EAEvB,IAAI,QAAQ,WAAW,KAAK,KAAK,WAAW,KAAK,QAAQ,WAAW,GAAG;EAKvE,MAAM,kBAAkB,MAAc,CAAC,WAAW,CAAC,KAAK,KAAK,eAAe,CAAC;EAC7E,MAAM,cAAc,MAAc,eAAe,CAAC,KAAK,eAAe,CAAC;EACvE,MAAM,kBAAkB,QAAQ,OAAO,UAAU;EACjD,MAAM,eAAe,KAAK,OAAO,UAAU;EAC3C,MAAM,cAAc,QAAQ,QAAQ,MAAM,CAAC,WAAW,CAAC,CAAC;EACxD,MAAM,WAAW,KAAK,QAAQ,MAAM,CAAC,WAAW,CAAC,CAAC;EAClD,MAAM,kBAAkB;GAAC,GAAG;GAAS,GAAG;GAAM,GAAG;EAAO,CAAC,CAAC,OAAO,cAAc;EAG/E,IAAI,SAAS,SAAS,YAAY,SAAS,GAAG;GAC5C,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;GACvD,qBAAqB;EACvB;EAEA,MAAM,EAAE,OAAO,gBAAgB,aAAa,MAAM,KAAK,iBAAiB,QAAQ;EAChF,MAAM,mBAAmB,MAAM,KAAK,oBAAoB,WAAW;EACnE,MAAM,KAAK,oBAAoB,OAAO;EAEtC,KAAK,eAAe,qBAAqB;EACzC,KAAK,eAAe,oBAAoB;EACxC,MAAM,KAAK,SAAS,KAAK;EAYzB,OAAO,QAAQ,6BAA6B;GAC1C,OAAO,CAAC,GAAG,cAAc,GAAG,cAAc;GAC1C,SAAS,CAAC,GAAG,iBAAiB,GAAG,gBAAgB;GACjD,SAAS,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,eAAe,CAAC,CAAC,GAAG,GAAG,QAAQ;GACnE;GACA,SAAS,iBAAiB,IACtB;IAAE,iBAAiB,mBAAmB;IAAG,gBAAgB,kBAAkB;GAAE,IAC7E;EACN,CAAC;CACH;;;;;;;;CASA,MAAc,oBAAoB,eAA4C;EAC5E,MAAM,UAAoB,CAAC;EAC3B,MAAM,aAAa,oBAA2C,OAAO,SAAS;GAC5E,IAAI,MAAM,KAAK,eAAe,WAAW,IAAI,GAC3C,QAAQ,KAAK,IAAI;EAErB,CAAC;EACD,OAAO;CACT;;;;;;;;CASA,MAAc,iBACZ,eACkD;EAClD,MAAM,QAAkB,CAAC;EACzB,MAAM,WAAqB,CAAC;EAE5B,MAAM,aAAa,oBAA2C,OAAO,SAAS;GAC5E,IAAI;IAGF,KAAI,MAFsB,KAAK,eAAe,QAAQ,IAAI,EAE3C,CAAC,UAAU,WAAW;KACnC,MAAM,KAAK,eAAe,WAAW,IAAI;KACzC,SAAS,KAAK,IAAI;KAClB;IACF;IAEA,MAAM,KAAK,IAAI;IACf,cAAc,eAAe,MAAM;GACrC,SAAS,OAAO;IACd,QAAQ,MAAM,sBAAsB,KAAK,IAAI,KAAK;GACpD;EACF,CAAC;EAED,OAAO;GAAE;GAAO;EAAS;CAC3B;CAEA,MAAc,oBAAoB,eAAwC;EACxE,KAAK,MAAM,gBAAgB,eAAe;GACxC,MAAM,KAAK,eAAe,WAAW,YAAY;GACjD,cAAc,iBAAiB,cAAc;EAC/C;CACF;AACF;AAEA,SAAS,UAAU,MAAuB;CACxC,MAAM,WAAW,KAAK,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;CAC1C,OAAO,aAAa,UAAU,SAAS,WAAW,OAAO;AAC3D;;AAGA,SAAS,eAAe,MAAuB;CAC7C,OAAO,UAAU,IAAI,KAAK,SAAS;AACrC;AAEA,eAAe,aACb,OACA,MACA,IACe;CACf,IAAI,MAAM,WAAW,GAAG;CACxB,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,MACrC,MAAM,QAAQ,IAAI,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAEtD"}
|