swallowkit 2.0.0-alpha.1 → 2.0.0-alpha.10
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/README.ja.md +14 -0
- package/README.md +14 -0
- package/dist/cli/commands/add-auth.js +37 -6
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +8 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +67 -132
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +85 -3
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +43 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +10 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +38 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +46 -25
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +59 -28
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/dist/utils/package-manager.js +2 -2
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +6 -3
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +116 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +36 -6
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +73 -138
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +105 -3
- package/src/cli/index.ts +40 -7
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +89 -6
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +49 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +50 -28
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +65 -34
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +2 -2
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import * as z from "zod/v4";
|
|
6
|
+
import { FileOperationSession } from "../core/operations/file-session";
|
|
7
|
+
import { parseModelFile } from "../core/scaffold/model-parser";
|
|
8
|
+
import { generateLanguageSchemaArtifacts } from "../core/scaffold/native-schema-generator";
|
|
9
|
+
import { generateOpenApiDocument } from "../core/scaffold/openapi-generator";
|
|
10
|
+
|
|
11
|
+
const MODEL_SOURCE = `import { z } from 'zod/v4';
|
|
12
|
+
export const ProbeItem = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
userId: z.string(),
|
|
15
|
+
title: z.string().min(1).max(40),
|
|
16
|
+
code: z.string().regex(/^[a-z]+-\\d+$/i).optional(),
|
|
17
|
+
tags: z.array(z.string().min(2)).max(3),
|
|
18
|
+
score: z.number().min(0).max(1),
|
|
19
|
+
count: z.number().int().nonnegative(),
|
|
20
|
+
kind: z.enum(['a', 'b']),
|
|
21
|
+
nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
|
|
22
|
+
publishedAt: z.iso.datetime().optional(),
|
|
23
|
+
note: z.string().nullable(),
|
|
24
|
+
rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
|
|
25
|
+
items: z.array(z.object({ x: z.string().min(1) })).optional(),
|
|
26
|
+
kinds: z.array(z.enum(['a', 'b'])).optional(),
|
|
27
|
+
metadata: z.object({}).optional(),
|
|
28
|
+
createdAt: z.string().optional(),
|
|
29
|
+
updatedAt: z.string().optional(),
|
|
30
|
+
});
|
|
31
|
+
export type ProbeItem = z.infer<typeof ProbeItem>;
|
|
32
|
+
export const partitionKey = '/userId';
|
|
33
|
+
export const displayName = 'Probe';
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
// Same schema as MODEL_SOURCE; used to validate C# output exactly as the BFF responseSchema would.
|
|
37
|
+
const ProbeItem = z.object({
|
|
38
|
+
id: z.string(),
|
|
39
|
+
userId: z.string(),
|
|
40
|
+
title: z.string().min(1).max(40),
|
|
41
|
+
code: z.string().regex(/^[a-z]+-\d+$/i).optional(),
|
|
42
|
+
tags: z.array(z.string().min(2)).max(3),
|
|
43
|
+
score: z.number().min(0).max(1),
|
|
44
|
+
count: z.number().int().nonnegative(),
|
|
45
|
+
kind: z.enum(["a", "b"]),
|
|
46
|
+
nested: z.object({ x: z.string(), depth: z.object({ level: z.number().int() }).optional() }).optional(),
|
|
47
|
+
publishedAt: z.iso.datetime().optional(),
|
|
48
|
+
note: z.string().nullable(),
|
|
49
|
+
rating: z.number().gt(0).min(0).lt(5).max(5).nullable(),
|
|
50
|
+
items: z.array(z.object({ x: z.string().min(1) })).optional(),
|
|
51
|
+
kinds: z.array(z.enum(["a", "b"])).optional(),
|
|
52
|
+
metadata: z.object({}).optional(),
|
|
53
|
+
createdAt: z.string().optional(),
|
|
54
|
+
updatedAt: z.string().optional(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const VALID_INPUTS = [
|
|
58
|
+
{
|
|
59
|
+
id: "1",
|
|
60
|
+
userId: "user-1",
|
|
61
|
+
title: "Probe",
|
|
62
|
+
code: "ABC-12",
|
|
63
|
+
tags: ["aa", "bb"],
|
|
64
|
+
score: 0.25,
|
|
65
|
+
count: 3,
|
|
66
|
+
kind: "b",
|
|
67
|
+
nested: { x: "value", depth: { level: 2 } },
|
|
68
|
+
publishedAt: "2024-05-06T07:08:09.123Z",
|
|
69
|
+
note: "note",
|
|
70
|
+
rating: 2.5,
|
|
71
|
+
items: [{ x: "a" }, { x: "b" }],
|
|
72
|
+
kinds: ["a", "b"],
|
|
73
|
+
metadata: {},
|
|
74
|
+
createdAt: "2024-05-06T07:08:09.123456+09:00",
|
|
75
|
+
updatedAt: "not a timestamp but still a string",
|
|
76
|
+
},
|
|
77
|
+
{ id: "2", userId: "user-2", title: "Minimal", tags: [], score: 1, count: 0, kind: "a", note: null, rating: null },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const INVALID_INPUTS: Array<{ input: Record<string, unknown>; members: string[] }> = [
|
|
81
|
+
{ input: { ...VALID_INPUTS[1], title: "" }, members: ["title"] },
|
|
82
|
+
{ input: { ...VALID_INPUTS[1], title: "x".repeat(41) }, members: ["title"] },
|
|
83
|
+
{ input: { ...VALID_INPUTS[1], code: "no-digits" }, members: ["code"] },
|
|
84
|
+
{ input: { ...VALID_INPUTS[1], tags: ["a"] }, members: ["tags[0]"] },
|
|
85
|
+
{ input: { ...VALID_INPUTS[1], tags: ["aa", "bb", "cc", "dd"] }, members: ["tags"] },
|
|
86
|
+
{ input: { ...VALID_INPUTS[1], score: 1.5 }, members: ["score"] },
|
|
87
|
+
{ input: { ...VALID_INPUTS[1], count: -1 }, members: ["count"] },
|
|
88
|
+
{ input: { ...VALID_INPUTS[1], nested: { depth: { level: 1 } } }, members: ["nested.x"] },
|
|
89
|
+
{ input: { ...VALID_INPUTS[1], rating: 0 }, members: ["rating"] },
|
|
90
|
+
{ input: { ...VALID_INPUTS[1], rating: 5 }, members: ["rating"] },
|
|
91
|
+
{ input: { ...VALID_INPUTS[1], items: [{ x: "ok" }, { x: "" }] }, members: ["items[1].x"] },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function createModel(): { root: string; modelPath: string } {
|
|
95
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-csharp-schema-"));
|
|
96
|
+
const modelPath = path.join(root, "shared", "models", "probe-item.ts");
|
|
97
|
+
fs.mkdirSync(path.dirname(modelPath), { recursive: true });
|
|
98
|
+
fs.writeFileSync(modelPath, MODEL_SOURCE);
|
|
99
|
+
return { root, modelPath };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
describe("Zod -> OpenAPI -> C# conversion", () => {
|
|
103
|
+
let root: string;
|
|
104
|
+
let modelPath: string;
|
|
105
|
+
const originalCwd = process.cwd();
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
({ root, modelPath } = createModel());
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
process.chdir(originalCwd);
|
|
113
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("records formats, integers, constraints, and inline object shapes from Zod", async () => {
|
|
117
|
+
const model = await parseModelFile(modelPath);
|
|
118
|
+
const field = (name: string) => model.fields.find((candidate) => candidate.name === name)!;
|
|
119
|
+
expect(model.parserMode).toBe("static-ast");
|
|
120
|
+
expect(field("createdAt")).toMatchObject({ type: "string", isOptional: true });
|
|
121
|
+
expect(field("createdAt").format).toBeUndefined();
|
|
122
|
+
expect(field("publishedAt").format).toBe("date-time");
|
|
123
|
+
expect(field("title").constraints).toEqual({ minLength: 1, maxLength: 40 });
|
|
124
|
+
expect(field("code").constraints).toEqual({ pattern: "^[a-z]+-\\d+$", patternFlags: "i" });
|
|
125
|
+
expect(field("tags").constraints).toEqual({ minLength: 2, maxItems: 3 });
|
|
126
|
+
expect(field("score")).toMatchObject({ constraints: { minimum: 0, maximum: 1 } });
|
|
127
|
+
expect(field("score").isInteger).toBeUndefined();
|
|
128
|
+
expect(field("count")).toMatchObject({ isInteger: true, constraints: { minimum: 0 } });
|
|
129
|
+
expect(field("nested").objectFields?.map((child) => child.name)).toEqual(["x", "depth"]);
|
|
130
|
+
expect(field("nested").objectFields?.[1].objectFields?.[0]).toMatchObject({ name: "level", isInteger: true });
|
|
131
|
+
expect(field("rating")).toMatchObject({
|
|
132
|
+
isNullable: true,
|
|
133
|
+
constraints: { minimum: 0, exclusiveMinimum: true, maximum: 5, exclusiveMaximum: true },
|
|
134
|
+
});
|
|
135
|
+
expect(field("metadata").objectFields).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("emits typed OpenAPI components without guessing formats from names", async () => {
|
|
139
|
+
const model = await parseModelFile(modelPath);
|
|
140
|
+
const schemas = JSON.parse(generateOpenApiDocument([model], model)).components.schemas;
|
|
141
|
+
const properties = schemas.ProbeItem.properties;
|
|
142
|
+
expect(properties.createdAt).toEqual({ type: "string" });
|
|
143
|
+
expect(properties.publishedAt).toEqual({ type: "string", format: "date-time" });
|
|
144
|
+
expect(properties.score).toEqual({ type: "number", format: "double", minimum: 0, maximum: 1 });
|
|
145
|
+
expect(properties.count).toEqual({ type: "integer", format: "int64", minimum: 0 });
|
|
146
|
+
expect(properties.title).toEqual({ type: "string", minLength: 1, maxLength: 40 });
|
|
147
|
+
expect(properties.code).toEqual({ type: "string", pattern: "^[a-z]+-\\d+$" });
|
|
148
|
+
expect(properties.tags).toEqual({ type: "array", items: { type: "string", minLength: 2 }, maxItems: 3 });
|
|
149
|
+
expect(properties.nested).toEqual({ $ref: "#/components/schemas/ProbeItemNested" });
|
|
150
|
+
expect(schemas.ProbeItemNested).toEqual({
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: { x: { type: "string" }, depth: { $ref: "#/components/schemas/ProbeItemNestedDepth" } },
|
|
153
|
+
required: ["x"],
|
|
154
|
+
});
|
|
155
|
+
expect(schemas.ProbeItemNestedDepth.properties.level).toEqual({ type: "integer", format: "int64" });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("generates C# types and validation from the Zod contract", async () => {
|
|
159
|
+
const model = await parseModelFile(modelPath);
|
|
160
|
+
process.chdir(root);
|
|
161
|
+
await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
|
|
162
|
+
const modelDir = path.join(root, "functions", "generated", "csharp-models", "src", "SwallowKitBackendModels", "Model");
|
|
163
|
+
const source = fs.readFileSync(path.join(modelDir, "ProbeItem.cs"), "utf8");
|
|
164
|
+
|
|
165
|
+
expect(source).toContain("public string? CreatedAt");
|
|
166
|
+
expect(source).toContain("public DateTime? PublishedAt");
|
|
167
|
+
expect(source).toContain("public double Score");
|
|
168
|
+
expect(source).toContain("public long Count");
|
|
169
|
+
expect(source).toContain("public ProbeItemNested? Nested");
|
|
170
|
+
expect(source).not.toMatch(/Dictionary<string, object>|decimal|DateTime\? CreatedAt/);
|
|
171
|
+
expect(source).not.toContain("yield break;");
|
|
172
|
+
expect(source).toContain('Title.Length < 1');
|
|
173
|
+
expect(source).toContain('Regex.IsMatch(Code, "^[a-z]+-\\\\d+$", RegexOptions.IgnoreCase)');
|
|
174
|
+
expect(source).toContain("Score > 1");
|
|
175
|
+
expect(source).toContain("Count < 0");
|
|
176
|
+
expect(fs.readFileSync(path.join(modelDir, "ProbeItemNested.cs"), "utf8")).toContain("public ProbeItemNestedDepth? Depth");
|
|
177
|
+
expect(fs.readFileSync(path.join(modelDir, "ProbeItemNestedDepth.cs"), "utf8")).toContain("public long Level");
|
|
178
|
+
expect(source).toContain("public string? Note");
|
|
179
|
+
expect(source).not.toContain("if (Note is null)");
|
|
180
|
+
expect(source).toContain("public double? Rating");
|
|
181
|
+
expect(source).toContain("Rating.HasValue && Rating.Value <= 0");
|
|
182
|
+
expect(source).toContain("public List<ProbeItemItems>? Items");
|
|
183
|
+
expect(source).toContain("public List<KindsEnum>? Kinds");
|
|
184
|
+
const metadata = fs.readFileSync(path.join(modelDir, "ProbeItemMetadata.cs"), "utf8");
|
|
185
|
+
expect(metadata.match(/public ProbeItemMetadata\(\)/g)).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const dotnetVersion = spawnSync("dotnet", ["--version"], { encoding: "utf8" });
|
|
190
|
+
const itWithDotnet = dotnetVersion.status === 0 ? it : it.skip;
|
|
191
|
+
|
|
192
|
+
describe("generated C# JSON round trip", () => {
|
|
193
|
+
itWithDotnet("serializes back to JSON that passes the same Zod schema, and rejects what Zod rejects", async () => {
|
|
194
|
+
const { root, modelPath } = createModel();
|
|
195
|
+
const originalCwd = process.cwd();
|
|
196
|
+
try {
|
|
197
|
+
const model = await parseModelFile(modelPath);
|
|
198
|
+
process.chdir(root);
|
|
199
|
+
await generateLanguageSchemaArtifacts([model], model, "functions", "csharp", new FileOperationSession("commit", root));
|
|
200
|
+
process.chdir(originalCwd);
|
|
201
|
+
|
|
202
|
+
const app = path.join(root, "roundtrip");
|
|
203
|
+
fs.mkdirSync(app);
|
|
204
|
+
const major = String(dotnetVersion.stdout).trim().split(".")[0];
|
|
205
|
+
fs.writeFileSync(path.join(app, "RoundTrip.csproj"), `<Project Sdk="Microsoft.NET.Sdk">
|
|
206
|
+
<PropertyGroup>
|
|
207
|
+
<OutputType>Exe</OutputType>
|
|
208
|
+
<TargetFramework>net${major}.0</TargetFramework>
|
|
209
|
+
<Nullable>enable</Nullable>
|
|
210
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
211
|
+
</PropertyGroup>
|
|
212
|
+
<ItemGroup>
|
|
213
|
+
<Compile Include="../functions/generated/csharp-models/**/*.cs" />
|
|
214
|
+
</ItemGroup>
|
|
215
|
+
</Project>
|
|
216
|
+
`);
|
|
217
|
+
fs.writeFileSync(path.join(app, "Program.cs"), `using System.ComponentModel.DataAnnotations;
|
|
218
|
+
using System.Text.Json;
|
|
219
|
+
using SwallowKitBackendModels.Model;
|
|
220
|
+
|
|
221
|
+
var results = new List<object>();
|
|
222
|
+
foreach (var file in args)
|
|
223
|
+
{
|
|
224
|
+
var item = JsonSerializer.Deserialize<ProbeItem>(File.ReadAllText(file))!;
|
|
225
|
+
var errors = new List<ValidationResult>();
|
|
226
|
+
Validator.TryValidateObject(item, new ValidationContext(item), errors, true);
|
|
227
|
+
results.Add(new { json = JsonSerializer.Serialize(item), members = errors.SelectMany(error => error.MemberNames).ToArray() });
|
|
228
|
+
}
|
|
229
|
+
Console.WriteLine(JsonSerializer.Serialize(results));
|
|
230
|
+
`);
|
|
231
|
+
const inputs = [...VALID_INPUTS, ...INVALID_INPUTS.map((entry) => entry.input)];
|
|
232
|
+
const files = inputs.map((input, index) => {
|
|
233
|
+
const file = path.join(root, `input-${index}.json`);
|
|
234
|
+
fs.writeFileSync(file, JSON.stringify(input));
|
|
235
|
+
return file;
|
|
236
|
+
});
|
|
237
|
+
const run = spawnSync("dotnet", ["run", "--project", app, "--", ...files], {
|
|
238
|
+
encoding: "utf8",
|
|
239
|
+
timeout: 240000,
|
|
240
|
+
env: { ...process.env, DOTNET_CLI_TELEMETRY_OPTOUT: "1", DOTNET_NOLOGO: "1" },
|
|
241
|
+
});
|
|
242
|
+
if (run.status !== 0) throw new Error(`dotnet run failed (${run.status}):\n${run.stdout}\n${run.stderr}`);
|
|
243
|
+
const output = JSON.parse(run.stdout.trim().split(/\r?\n/).pop()!) as Array<{ json: string; members: string[] }>;
|
|
244
|
+
|
|
245
|
+
VALID_INPUTS.forEach((input, index) => {
|
|
246
|
+
expect(ProbeItem.safeParse(input).success).toBe(true);
|
|
247
|
+
expect(output[index].members).toEqual([]);
|
|
248
|
+
const roundTripped = JSON.parse(output[index].json);
|
|
249
|
+
expect(ProbeItem.safeParse(roundTripped).success).toBe(true);
|
|
250
|
+
expect(roundTripped).toEqual(input);
|
|
251
|
+
});
|
|
252
|
+
INVALID_INPUTS.forEach(({ input, members }, offset) => {
|
|
253
|
+
expect(ProbeItem.safeParse(input).success).toBe(false);
|
|
254
|
+
expect(output[VALID_INPUTS.length + offset].members).toEqual(members);
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
process.chdir(originalCwd);
|
|
258
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
}, 300000);
|
|
261
|
+
});
|
|
@@ -32,6 +32,7 @@ describe("dev command helpers", () => {
|
|
|
32
32
|
|
|
33
33
|
it("passes the requested port to Azure Functions Core Tools", () => {
|
|
34
34
|
expect(buildFunctionsStartArgs("7076")).toEqual(["start", "--port", "7076"]);
|
|
35
|
+
expect(buildFunctionsStartArgs("7076", "csharp")).toEqual(["--port", "7076"]);
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it("parses the Core Tools version from CLI output", () => {
|
|
@@ -47,16 +48,22 @@ describe("dev command helpers", () => {
|
|
|
47
48
|
it("falls back to npm Core Tools for older C# isolated hosts", () => {
|
|
48
49
|
expect(buildFunctionsCoreToolsCommand("csharp", "4.0.5198")).toEqual({
|
|
49
50
|
command: "npm",
|
|
50
|
-
argsPrefix: ["exec", "--yes", "azure-functions-core-tools@4", "--"],
|
|
51
|
-
label: "npm
|
|
51
|
+
argsPrefix: ["exec", "--yes", "--package", "azure-functions-core-tools@4", "--", "dotnet", "run", "--no-launch-profile", "--"],
|
|
52
|
+
label: "dotnet run with npm azure-functions-core-tools@4 (installed func 4.0.5198 is too old for C# isolated)",
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
it("
|
|
56
|
+
it("uses the .NET SDK launch target for C# so extensions load from the output directory", () => {
|
|
56
57
|
expect(buildFunctionsCoreToolsCommand("csharp", "4.6.0")).toEqual({
|
|
57
|
-
command: "
|
|
58
|
-
argsPrefix: [],
|
|
59
|
-
label: "
|
|
58
|
+
command: "dotnet",
|
|
59
|
+
argsPrefix: ["run", "--no-launch-profile", "--"],
|
|
60
|
+
label: "dotnet run",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it.each(["typescript", "python"] as const)("keeps func start for %s", language => {
|
|
65
|
+
expect(buildFunctionsCoreToolsCommand(language, "4.14.0")).toEqual({
|
|
66
|
+
command: "func", argsPrefix: [], label: "func 4.14.0",
|
|
60
67
|
});
|
|
61
68
|
});
|
|
62
69
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const args = process.argv.slice(2);
|
|
3
|
+
const emit = (value) => process.stdout.write(JSON.stringify(value));
|
|
4
|
+
|
|
5
|
+
if (args[0] === "process") {
|
|
6
|
+
const mode = args[1];
|
|
7
|
+
if (mode === "warning" || mode === "slow") {
|
|
8
|
+
console.error("WARNING: fixture Bicep warning");
|
|
9
|
+
if (mode === "slow") setTimeout(() => emit({ done: true }), Number(args[2] ?? 600));
|
|
10
|
+
} else if (mode === "flood") {
|
|
11
|
+
process.stdout.write("x".repeat(2 * 1024 * 1024));
|
|
12
|
+
process.stderr.write("y".repeat(2 * 1024 * 1024));
|
|
13
|
+
} else if (mode === "communication") {
|
|
14
|
+
console.error("ConnectionResetError: fixture communication interrupted");
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else if (mode === "nonzero") {
|
|
17
|
+
console.error("ERROR: fixture az failure");
|
|
18
|
+
process.exitCode = 7;
|
|
19
|
+
} else if (mode === "echo") {
|
|
20
|
+
emit(args.slice(2));
|
|
21
|
+
} else if (mode === "terminate") {
|
|
22
|
+
process.kill(process.pid, "SIGTERM");
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const file = process.env.SWALLOWKIT_AZ_FIXTURE_STATE;
|
|
28
|
+
if (!file) throw new Error("This fixture requires its isolated state file; it never invokes Azure.");
|
|
29
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
30
|
+
state.calls.push(args);
|
|
31
|
+
const save = () => {
|
|
32
|
+
const temporary = `${file}.tmp-${process.pid}`;
|
|
33
|
+
fs.writeFileSync(temporary, JSON.stringify(state));
|
|
34
|
+
fs.renameSync(temporary, file);
|
|
35
|
+
};
|
|
36
|
+
const value = (flag) => args[args.indexOf(flag) + 1];
|
|
37
|
+
const subscription = state.subscription;
|
|
38
|
+
const resourceGroup = state.resourceGroup;
|
|
39
|
+
const groupId = `/subscriptions/${subscription}/resourceGroups/${resourceGroup}`;
|
|
40
|
+
save();
|
|
41
|
+
|
|
42
|
+
if (args[0] === "--version") {
|
|
43
|
+
process.stdout.write("azure-cli fixture");
|
|
44
|
+
} else if (args[0] === "account" && args[1] === "show") {
|
|
45
|
+
process.stdout.write(subscription);
|
|
46
|
+
} else if (args[0] === "bicep" && args[1] === "build") {
|
|
47
|
+
emit(state.template);
|
|
48
|
+
} else if (args[0] === "group" && args[1] === "exists") {
|
|
49
|
+
process.stdout.write(String(state.groupExists));
|
|
50
|
+
} else if (args[0] === "group" && args[1] === "show") {
|
|
51
|
+
emit({ id: groupId, tags: state.tags });
|
|
52
|
+
} else if (args[0] === "group" && args[1] === "create") {
|
|
53
|
+
state.groupExists = true;
|
|
54
|
+
state.groupCreates++;
|
|
55
|
+
save();
|
|
56
|
+
emit(groupId);
|
|
57
|
+
} else if ((args[0] === "tag" && args[1] === "update") || (args[0] === "group" && args[1] === "update")) {
|
|
58
|
+
state.tagAttempts++;
|
|
59
|
+
const assignment = value(args[0] === "tag" ? "--tags" : "--set").replace(/^tags\./, "");
|
|
60
|
+
const index = assignment.indexOf("=");
|
|
61
|
+
const apply = () => { state.tags[assignment.slice(0, index)] = assignment.slice(index + 1); };
|
|
62
|
+
if (state.failTags > 0) {
|
|
63
|
+
state.failTags--;
|
|
64
|
+
if (state.applyTagBeforeFailure) apply();
|
|
65
|
+
save();
|
|
66
|
+
console.error("ERROR: fixture tag update failed");
|
|
67
|
+
process.exitCode = 9;
|
|
68
|
+
} else {
|
|
69
|
+
apply();
|
|
70
|
+
save();
|
|
71
|
+
}
|
|
72
|
+
} else if (args[0] === "staticwebapp" && args[1] === "secrets" && args[2] === "list") {
|
|
73
|
+
if (state.secretQueryFails) {
|
|
74
|
+
console.error("ERROR: fixture secret query failed");
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
} else {
|
|
77
|
+
process.stdout.write(`fixture-swa-token-for-${value("--name")}\n`);
|
|
78
|
+
}
|
|
79
|
+
} else if (args[0] === "webapp" && args[1] === "deployment" && args[2] === "list-publishing-profiles") {
|
|
80
|
+
if (state.secretQueryFails) {
|
|
81
|
+
console.error("ERROR: fixture publish profile query failed");
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
} else {
|
|
84
|
+
process.stdout.write(`<publishData><publishProfile publishUrl="${value("--name")}.scm.azurewebsites.net" /></publishData>\n`);
|
|
85
|
+
}
|
|
86
|
+
} else if (args[0] === "deployment" && args[1] === "operation") {
|
|
87
|
+
emit(state.operations ?? []);
|
|
88
|
+
} else if (args[0] === "deployment" && args[2] === "export") {
|
|
89
|
+
emit(state.remoteTemplate ?? state.template);
|
|
90
|
+
} else if (args[0] === "deployment" && args[2] === "what-if") {
|
|
91
|
+
emit({ changes: [] });
|
|
92
|
+
} else if (args[0] === "deployment" && args[2] === "create") {
|
|
93
|
+
const name = value("--name");
|
|
94
|
+
state.deploymentCreates++;
|
|
95
|
+
state.deployments[name] = {
|
|
96
|
+
correlationId: `fixture-correlation-${state.deploymentCreates}`,
|
|
97
|
+
templateHash: "fixture-template-hash",
|
|
98
|
+
};
|
|
99
|
+
save();
|
|
100
|
+
if (state.warning) console.error("WARNING: The configuration value of bicep.use_binary_from_path has been set to 'false'.");
|
|
101
|
+
if (state.submitStderr) console.error(state.submitStderr);
|
|
102
|
+
if (state.submitMode === "communication") {
|
|
103
|
+
console.error("ConnectionResetError: fixture connection closed after acceptance");
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
} else if (state.submitMode === "nonzero") {
|
|
106
|
+
console.error("ERROR: fixture submit exited nonzero");
|
|
107
|
+
process.exitCode = 7;
|
|
108
|
+
} else if (state.submitMode === "flood") {
|
|
109
|
+
process.stdout.write(JSON.stringify({ properties: { outputs: { apiKey: { value: "fixture-secret-do-not-print" } } } }));
|
|
110
|
+
process.stdout.write("x".repeat(2 * 1024 * 1024));
|
|
111
|
+
process.stderr.write("y".repeat(2 * 1024 * 1024));
|
|
112
|
+
} else if (state.submitMode === "slow") {
|
|
113
|
+
setTimeout(() => emit({ accepted: true }), state.submitDelay ?? 2000);
|
|
114
|
+
} else {
|
|
115
|
+
emit({ accepted: true });
|
|
116
|
+
}
|
|
117
|
+
} else if (args[0] === "deployment" && args[2] === "show") {
|
|
118
|
+
const name = value("--name");
|
|
119
|
+
const deployment = state.deployments[name];
|
|
120
|
+
if (!deployment || state.queryMode === "not-found") {
|
|
121
|
+
console.error("ERROR: (DeploymentNotFound) fixture deployment is not visible");
|
|
122
|
+
process.exitCode = 3;
|
|
123
|
+
} else if (state.queryMode === "communication" || (state.transientQueryFailures ?? 0) > 0) {
|
|
124
|
+
if (state.transientQueryFailures) { state.transientQueryFailures -= 1; save(); }
|
|
125
|
+
console.error("ConnectionResetError: fixture status query interrupted");
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
} else if (state.queryMode === "invalid") {
|
|
128
|
+
process.stdout.write("{invalid JSON");
|
|
129
|
+
} else if (value("--query") === "properties.parameters") {
|
|
130
|
+
emit(state.parameters);
|
|
131
|
+
} else if (value("--query").includes("properties.outputs.")) {
|
|
132
|
+
if (state.outputQueryFails) {
|
|
133
|
+
console.error("ERROR: resource output query failed");
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
} else {
|
|
136
|
+
const outputs = state.outputs ?? {};
|
|
137
|
+
emit(Object.fromEntries(["staticWebAppName", "staticWebAppUrl", "functionsAppName", "functionsAppUrl", "cosmosDbAccountName", "cosmosDatabaseName"].map(key => [key, outputs[key] ?? null])));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
const states = state.states ?? ["Succeeded"];
|
|
141
|
+
const provisioningState = states.length > 1 ? states.shift() : states[0];
|
|
142
|
+
save();
|
|
143
|
+
emit({
|
|
144
|
+
id: `${groupId}/providers/Microsoft.Resources/deployments/${name}`,
|
|
145
|
+
name, state: provisioningState,
|
|
146
|
+
correlationId: deployment.correlationId, templateHash: deployment.templateHash,
|
|
147
|
+
error: state.error ?? null,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.error("ERROR: fixture does not support this command");
|
|
152
|
+
process.exitCode = 4;
|
|
153
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const ts = require("typescript");
|
|
4
|
+
const root = path.resolve(__dirname, "..", "..", "..");
|
|
5
|
+
require.extensions[".ts"] = (module, file) => {
|
|
6
|
+
module._compile(ts.transpileModule(fs.readFileSync(file, "utf8"), {
|
|
7
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true },
|
|
8
|
+
fileName: file,
|
|
9
|
+
}).outputText, file);
|
|
10
|
+
};
|
|
11
|
+
const azure = require(path.join(root, "src", "utils", "azure-cli.ts"));
|
|
12
|
+
azure.setAzCliInvocationForTesting({
|
|
13
|
+
command: process.execPath,
|
|
14
|
+
prefixArgs: [path.join(__dirname, "azure-cli.cjs")],
|
|
15
|
+
source: "isolated fixture",
|
|
16
|
+
});
|
|
17
|
+
const provision = require(path.join(root, "src", "core", "operations", "provision-operations.ts"));
|
|
18
|
+
const state = JSON.parse(fs.readFileSync(process.env.SWALLOWKIT_AZ_FIXTURE_STATE, "utf8"));
|
|
19
|
+
void (async () => {
|
|
20
|
+
const plan = await provision.planProvisionOperation({
|
|
21
|
+
resourceGroup: state.resourceGroup, location: "japaneast", swaLocation: "eastasia",
|
|
22
|
+
});
|
|
23
|
+
const result = await provision.executeProvisionPlan(plan, { approve: true });
|
|
24
|
+
console.log(JSON.stringify(result));
|
|
25
|
+
})().catch((error) => {
|
|
26
|
+
console.error(error.message);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
});
|