swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +27 -118
- package/dist/cli/commands/init.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/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/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +45 -24
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/scaffold/auth-generator.js +8 -8
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +6 -16
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/utils/package-manager.js +1 -1
- 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/package.json +1 -1
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +57 -0
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/plan-apply.test.ts +9 -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/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +29 -121
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/react-project.ts +49 -27
- package/src/core/scaffold/auth-generator.ts +8 -8
- package/src/core/scaffold/native-schema-generator.ts +8 -27
- package/src/utils/package-manager.ts +1 -1
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
package/dist/utils/python-uv.js
CHANGED
|
@@ -33,16 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.DEFAULT_AZURE_PYTHON_VERSION = void 0;
|
|
37
37
|
exports.getPythonProjectRoot = getPythonProjectRoot;
|
|
38
38
|
exports.getProjectLocalUvPaths = getProjectLocalUvPaths;
|
|
39
|
-
exports.buildProjectLocalUvEnv = buildProjectLocalUvEnv;
|
|
40
39
|
exports.buildProjectLocalUvInstallerEnv = buildProjectLocalUvInstallerEnv;
|
|
41
40
|
exports.getProjectLocalUvInstallerCommand = getProjectLocalUvInstallerCommand;
|
|
42
41
|
exports.buildUvVenvArgs = buildUvVenvArgs;
|
|
42
|
+
exports.getVirtualEnvPythonPath = getVirtualEnvPythonPath;
|
|
43
|
+
exports.ensureUvVirtualEnvironment = ensureUvVirtualEnvironment;
|
|
43
44
|
exports.buildUvPipInstallArgs = buildUvPipInstallArgs;
|
|
45
|
+
exports.buildPythonCodegenInstallEnv = buildPythonCodegenInstallEnv;
|
|
44
46
|
const path = __importStar(require("path"));
|
|
45
|
-
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const util_1 = require("util");
|
|
50
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
51
|
+
exports.DEFAULT_AZURE_PYTHON_VERSION = "3.14";
|
|
46
52
|
function getPythonProjectRoot(functionsDir) {
|
|
47
53
|
return path.dirname(functionsDir);
|
|
48
54
|
}
|
|
@@ -51,39 +57,28 @@ function getProjectLocalUvPaths(projectRoot) {
|
|
|
51
57
|
const localUvInstallDir = path.join(stateDir, "bin");
|
|
52
58
|
return {
|
|
53
59
|
stateDir,
|
|
54
|
-
cacheDir: path.join(stateDir, "cache"),
|
|
55
|
-
pythonInstallDir: path.join(stateDir, "python"),
|
|
56
|
-
toolDir: path.join(stateDir, "tools"),
|
|
57
|
-
toolBinDir: path.join(stateDir, "tools", "bin"),
|
|
58
60
|
localUvInstallDir,
|
|
59
61
|
localUvExecutable: process.platform === "win32"
|
|
60
62
|
? path.join(localUvInstallDir, "uv.exe")
|
|
61
63
|
: path.join(localUvInstallDir, "uv"),
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
|
-
function
|
|
66
|
+
function buildProjectLocalUvInstallerEnv(baseEnv, projectRoot) {
|
|
65
67
|
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
66
68
|
const env = {
|
|
67
69
|
...baseEnv,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
UV_TOOL_DIR: uvPaths.toolDir,
|
|
71
|
-
UV_TOOL_BIN_DIR: uvPaths.toolBinDir,
|
|
72
|
-
UV_PYTHON_PREFERENCE: "only-managed",
|
|
70
|
+
UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
|
|
71
|
+
UV_NO_MODIFY_PATH: "1",
|
|
73
72
|
};
|
|
73
|
+
// Windows PowerShell must discover its own modules, not inherit PowerShell 7's.
|
|
74
74
|
if (process.platform === "win32") {
|
|
75
|
-
env
|
|
75
|
+
for (const key of Object.keys(env)) {
|
|
76
|
+
if (key.toLowerCase() === "psmodulepath")
|
|
77
|
+
delete env[key];
|
|
78
|
+
}
|
|
76
79
|
}
|
|
77
80
|
return env;
|
|
78
81
|
}
|
|
79
|
-
function buildProjectLocalUvInstallerEnv(baseEnv, projectRoot) {
|
|
80
|
-
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
81
|
-
return {
|
|
82
|
-
...buildProjectLocalUvEnv(baseEnv, projectRoot),
|
|
83
|
-
UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
|
|
84
|
-
UV_NO_MODIFY_PATH: "1",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
82
|
function getProjectLocalUvInstallerCommand() {
|
|
88
83
|
if (process.platform === "win32") {
|
|
89
84
|
return {
|
|
@@ -102,10 +97,41 @@ function getProjectLocalUvInstallerCommand() {
|
|
|
102
97
|
args: ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"],
|
|
103
98
|
};
|
|
104
99
|
}
|
|
105
|
-
function buildUvVenvArgs(venvDir
|
|
106
|
-
return ["venv", venvDir
|
|
100
|
+
function buildUvVenvArgs(venvDir) {
|
|
101
|
+
return ["venv", venvDir];
|
|
102
|
+
}
|
|
103
|
+
function getVirtualEnvPythonPath(venvDir) {
|
|
104
|
+
return process.platform === "win32"
|
|
105
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
106
|
+
: path.join(venvDir, "bin", "python");
|
|
107
|
+
}
|
|
108
|
+
async function ensureUvVirtualEnvironment(uvCommand, venvDir, cwd, env) {
|
|
109
|
+
const pythonExecutable = getVirtualEnvPythonPath(venvDir);
|
|
110
|
+
if (!fs.existsSync(venvDir)) {
|
|
111
|
+
const { stdout, stderr } = await execFileAsync(uvCommand, buildUvVenvArgs(venvDir), { cwd, env });
|
|
112
|
+
if (env.SWALLOWKIT_MACHINE_OUTPUT !== "1") {
|
|
113
|
+
if (stdout)
|
|
114
|
+
process.stdout.write(stdout);
|
|
115
|
+
if (stderr)
|
|
116
|
+
process.stderr.write(stderr);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await execFileAsync(pythonExecutable, ["--version"], { cwd, env });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
124
|
+
throw new Error(`Cannot execute Python at ${pythonExecutable}: ${reason}\n` +
|
|
125
|
+
"The virtual environment was preserved. Check the interpreter and OS application-control policy. " +
|
|
126
|
+
"To switch Python, explicitly recreate this environment with uv and a trusted interpreter; SwallowKit will not clear it automatically.");
|
|
127
|
+
}
|
|
128
|
+
return pythonExecutable;
|
|
107
129
|
}
|
|
108
130
|
function buildUvPipInstallArgs(pythonExecutable, requirementsPath) {
|
|
109
131
|
return ["pip", "install", "--python", pythonExecutable, "-r", requirementsPath];
|
|
110
132
|
}
|
|
133
|
+
function buildPythonCodegenInstallEnv(env) {
|
|
134
|
+
// Codegen reads JSON; PyYAML's optional accelerator must not require a C compiler.
|
|
135
|
+
return { ...env, PYYAML_FORCE_LIBYAML: env.PYYAML_FORCE_LIBYAML ?? "0" };
|
|
136
|
+
}
|
|
111
137
|
//# sourceMappingURL=python-uv.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"python-uv.js","sourceRoot":"","sources":["../../src/utils/python-uv.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,oDAEC;AAED,
|
|
1
|
+
{"version":3,"file":"python-uv.js","sourceRoot":"","sources":["../../src/utils/python-uv.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-dev.d.ts","sourceRoot":"","sources":["../../src/utils/react-dev.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,UAMvC,CAAC;AAEF,wBAAgB,+BAA+B,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAkBlD"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REACT_DEV_WATCH_DIRECTORIES = void 0;
|
|
4
|
+
exports.buildReactDevWatchIgnoredSource = buildReactDevWatchIgnoredSource;
|
|
5
|
+
exports.buildReactDevConfigSource = buildReactDevConfigSource;
|
|
6
|
+
exports.REACT_DEV_WATCH_DIRECTORIES = [
|
|
7
|
+
"functions",
|
|
8
|
+
"bff",
|
|
9
|
+
".uv",
|
|
10
|
+
".swallowkit",
|
|
11
|
+
"shared/dist",
|
|
12
|
+
];
|
|
13
|
+
function buildReactDevWatchIgnoredSource(rootExpression) {
|
|
14
|
+
return `${JSON.stringify(exports.REACT_DEV_WATCH_DIRECTORIES)}.map(directory => \`\${${rootExpression}.replace(/\\\\/g, '/').replace(/\\/$/, '')}/\${directory}/**\`)`;
|
|
15
|
+
}
|
|
16
|
+
function buildReactDevConfigSource() {
|
|
17
|
+
return `import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
|
|
18
|
+
|
|
19
|
+
export default defineConfig(async env => {
|
|
20
|
+
const loaded = await loadConfigFromFile(env, undefined, process.cwd());
|
|
21
|
+
const config = loaded?.config ?? {};
|
|
22
|
+
if (config.server?.watch === null) return config;
|
|
23
|
+
return mergeConfig(config, {
|
|
24
|
+
plugins: [{
|
|
25
|
+
name: 'swallowkit-dev-watch-boundaries',
|
|
26
|
+
configResolved(resolved) {
|
|
27
|
+
resolved.configFileDependencies.push(...(loaded?.dependencies ?? []));
|
|
28
|
+
},
|
|
29
|
+
}],
|
|
30
|
+
server: { watch: { ignored: ${buildReactDevWatchIgnoredSource("process.cwd()")} } },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=react-dev.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-dev.js","sourceRoot":"","sources":["../../src/utils/react-dev.ts"],"names":[],"mappings":";;;AAQA,0EAEC;AAED,8DAkBC;AA9BY,QAAA,2BAA2B,GAAG;IACzC,WAAW;IACX,KAAK;IACL,KAAK;IACL,aAAa;IACb,aAAa;CACd,CAAC;AAEF,SAAgB,+BAA+B,CAAC,cAAsB;IACpE,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,mCAA2B,CAAC,0BAA0B,cAAc,iEAAiE,CAAC;AACjK,CAAC;AAED,SAAgB,yBAAyB;IACvC,OAAO;;;;;;;;;;;;;kCAayB,+BAA+B,CAAC,eAAe,CAAC;;;CAGjF,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -300,6 +300,15 @@ describe("generateLoginPage", () => {
|
|
|
300
300
|
expect(code).toContain("handleSubmit");
|
|
301
301
|
expect(code).toContain("/api/auth/login");
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
it("keeps the shared form legible with either global color scheme", () => {
|
|
305
|
+
const code = generateLoginPage();
|
|
306
|
+
expect(code).toContain("bg-gray-50 dark:bg-gray-900");
|
|
307
|
+
expect(code).toContain("bg-white dark:bg-gray-800");
|
|
308
|
+
expect(code).toContain("text-gray-900 dark:text-gray-100");
|
|
309
|
+
expect(code).toContain("dark:border-gray-600");
|
|
310
|
+
expect(code).toContain("dark:text-red-300");
|
|
311
|
+
});
|
|
303
312
|
});
|
|
304
313
|
|
|
305
314
|
describe("generateAuthContext", () => {
|
|
@@ -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,50 @@
|
|
|
1
|
+
import { buildHomePage } from "../core/project/home-page";
|
|
2
|
+
import { adaptReactPage } from "../core/scaffold/react-ui-generator";
|
|
3
|
+
|
|
4
|
+
describe("shared home page presentation", () => {
|
|
5
|
+
it.each(["npm", "pnpm"] as const)(
|
|
6
|
+
"differs only in framework label and routing with %s",
|
|
7
|
+
(pm) => {
|
|
8
|
+
const next = buildHomePage("nextjs", pm);
|
|
9
|
+
const react = buildHomePage("react", pm);
|
|
10
|
+
const adaptedNext = adaptReactPage(next)
|
|
11
|
+
.replace("export const dynamic = 'force-dynamic';\n\n", "")
|
|
12
|
+
.replace("Next.js on Azure", "React on Azure");
|
|
13
|
+
|
|
14
|
+
expect(react).toBe(adaptedNext);
|
|
15
|
+
expect(react).toContain("Welcome to SwallowKit");
|
|
16
|
+
expect(react).toContain("Your Models");
|
|
17
|
+
expect(react).toContain("Get Started");
|
|
18
|
+
expect(react).toContain("Built with SwallowKit");
|
|
19
|
+
expect(react).toContain(
|
|
20
|
+
`${pm === "npm" ? "npx" : "pnpm exec"} swallowkit scaffold shared/models/your-model.ts`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it("keeps Next.js-only directives out of the SPA", () => {
|
|
26
|
+
const next = buildHomePage("nextjs", "pnpm");
|
|
27
|
+
const react = buildHomePage("react", "pnpm");
|
|
28
|
+
|
|
29
|
+
expect(next).toContain("'use client'");
|
|
30
|
+
expect(next).toContain("export const dynamic = 'force-dynamic'");
|
|
31
|
+
expect(next).toContain("href={model.path}");
|
|
32
|
+
expect(react).not.toMatch(/next\/|use client|force-dynamic/);
|
|
33
|
+
expect(react).toContain("import { Link } from 'react-router-dom'");
|
|
34
|
+
expect(react).toContain("to={model.path}");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it.each(["nextjs", "react"] as const)(
|
|
38
|
+
"preserves connection states and same-origin requests for %s",
|
|
39
|
+
(frontend) => {
|
|
40
|
+
const source = buildHomePage(frontend, "pnpm");
|
|
41
|
+
expect(source).toContain("fetch('/api/greet?name=SwallowKit')");
|
|
42
|
+
expect(source).toContain("disabled={greetingStatus === 'loading'}");
|
|
43
|
+
expect(source).toContain("setGreetingStatus('success')");
|
|
44
|
+
expect(source).toContain("setGreetingStatus('error')");
|
|
45
|
+
expect(source).toContain('role="status"');
|
|
46
|
+
expect(source).toContain('role="alert"');
|
|
47
|
+
expect(source).not.toMatch(/BACKEND_FUNCTIONS_|VITE_/);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getAzureFunctionsPipeline,
|
|
17
17
|
getGitHubFunctionsWorkflow,
|
|
18
18
|
injectSwallowKitNextConfig,
|
|
19
|
+
ensureNextTailwindPostcssConfig,
|
|
19
20
|
parseIgnoredBuilds,
|
|
20
21
|
buildCosmosDbFreeTierBicepSource,
|
|
21
22
|
buildFunctionsHostKeyBicepExpression,
|
|
@@ -109,6 +110,13 @@ export default nextConfig;
|
|
|
109
110
|
expect(source).toContain('<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />');
|
|
110
111
|
});
|
|
111
112
|
|
|
113
|
+
it("uses a current Cosmos SDK with binary query response fixes", () => {
|
|
114
|
+
const source = buildCSharpFunctionsProjectSource();
|
|
115
|
+
|
|
116
|
+
expect(source).toContain('<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />');
|
|
117
|
+
expect(source).not.toContain('Version="3.47.0"');
|
|
118
|
+
});
|
|
119
|
+
|
|
112
120
|
it("builds swallowkit.config.js without a local swallowkit package type import", () => {
|
|
113
121
|
const source = buildSwallowKitConfigSource("typescript");
|
|
114
122
|
|
|
@@ -155,9 +163,43 @@ export default nextConfig;
|
|
|
155
163
|
|
|
156
164
|
expect(devDependencies).toEqual({
|
|
157
165
|
swallowkit: expect.stringMatching(/.+/),
|
|
166
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
158
167
|
});
|
|
159
168
|
});
|
|
160
169
|
|
|
170
|
+
it("does not add the Next.js PostCSS plugin to React projects", () => {
|
|
171
|
+
expect(buildGeneratedProjectDevDependencies("react")).not.toHaveProperty("@tailwindcss/postcss");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("configures Tailwind for webpack even when create-next-app only configured Turbopack", () => {
|
|
175
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
176
|
+
try {
|
|
177
|
+
ensureNextTailwindPostcssConfig(project);
|
|
178
|
+
const config = path.join(project, "postcss.config.mjs");
|
|
179
|
+
expect(fs.readFileSync(config, "utf8")).toContain('"@tailwindcss/postcss": {}');
|
|
180
|
+
fs.writeFileSync(config, "export default { plugins: { custom: {} } };");
|
|
181
|
+
ensureNextTailwindPostcssConfig(project);
|
|
182
|
+
expect(fs.readFileSync(config, "utf8")).toBe("export default { plugins: { custom: {} } };");
|
|
183
|
+
} finally {
|
|
184
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it.each(["postcss.config.js", "postcss.config.cjs", "postcss.config.json"])(
|
|
189
|
+
"preserves an existing %s",
|
|
190
|
+
file => {
|
|
191
|
+
const project = fs.mkdtempSync(path.join(process.cwd(), ".tmp-postcss-"));
|
|
192
|
+
try {
|
|
193
|
+
fs.writeFileSync(path.join(project, file), "{}");
|
|
194
|
+
ensureNextTailwindPostcssConfig(project);
|
|
195
|
+
expect(fs.existsSync(path.join(project, "postcss.config.mjs"))).toBe(false);
|
|
196
|
+
expect(fs.readFileSync(path.join(project, file), "utf8")).toBe("{}");
|
|
197
|
+
} finally {
|
|
198
|
+
fs.rmSync(project, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
|
|
161
203
|
it("builds a project-scoped MCP config that resolves the configured swallowkit-mcp version", () => {
|
|
162
204
|
const source = buildSwallowKitMcpProjectConfigSource();
|
|
163
205
|
const parsed = JSON.parse(source) as {
|
|
@@ -335,6 +377,21 @@ describe("Infrastructure generation", () => {
|
|
|
335
377
|
});
|
|
336
378
|
|
|
337
379
|
describe("GitHub Actions workflow generation", () => {
|
|
380
|
+
it("keeps Python deployment and both CI providers on the same GA runtime", async () => {
|
|
381
|
+
expect(getGitHubFunctionsWorkflow("npm", "python")).toContain("python-version: '3.14'");
|
|
382
|
+
expect(getAzureFunctionsPipeline("npm", "python")).toContain("versionSpec: '3.14'");
|
|
383
|
+
const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-python-infra-"));
|
|
384
|
+
try {
|
|
385
|
+
await createInfrastructure(projectDir, "sample-app", {
|
|
386
|
+
cosmosDbMode: "serverless", vnetOption: "none", swaPlan: "free",
|
|
387
|
+
}, "python");
|
|
388
|
+
const source = fs.readFileSync(path.join(projectDir, "infra", "main.bicep"), "utf8");
|
|
389
|
+
expect(source).toContain("functionsRuntimeVersion: '3.14'");
|
|
390
|
+
} finally {
|
|
391
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
338
395
|
it("only enables setup-node caching when no package-manager conversion is needed", () => {
|
|
339
396
|
expect(buildGitHubSwaWorkflow("npm")).toContain("Select Node.js 22 for SWA");
|
|
340
397
|
expect(buildGitHubSwaWorkflow("npm")).toContain("app_build_command: 'npm run build'");
|
|
@@ -237,8 +237,8 @@ describe("getFunctionsStartScript", () => {
|
|
|
237
237
|
expect(getFunctionsStartScript("npm")).toContain("npm start");
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
it("uses
|
|
241
|
-
expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions &&
|
|
240
|
+
it("uses dotnet run for csharp isolated backends", () => {
|
|
241
|
+
expect(getFunctionsStartScript("pnpm", "csharp")).toBe("cd functions && dotnet run");
|
|
242
242
|
});
|
|
243
243
|
|
|
244
244
|
it("uses func start for python backends", () => {
|
|
@@ -17,6 +17,12 @@ jest.mock("child_process", () => {
|
|
|
17
17
|
const actual = jest.requireActual<typeof import("child_process")>("child_process");
|
|
18
18
|
return {
|
|
19
19
|
...actual,
|
|
20
|
+
execFile: jest.fn((
|
|
21
|
+
_command: string,
|
|
22
|
+
_args: string[],
|
|
23
|
+
_options: object,
|
|
24
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
25
|
+
) => process.nextTick(() => callback(null, "", ""))),
|
|
20
26
|
execFileSync: jest.fn(actual.execFileSync),
|
|
21
27
|
spawn: jest.fn(),
|
|
22
28
|
spawnSync: jest.fn(),
|
|
@@ -125,6 +131,7 @@ function createNativeBackendFixture(rootDir: string, backendLanguage: "csharp" |
|
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
function mockSuccessfulNativeCodegen(): () => void {
|
|
134
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
128
135
|
const spawnSyncMock = childProcess.spawnSync as unknown as any;
|
|
129
136
|
const spawnMock = childProcess.spawn as unknown as any;
|
|
130
137
|
spawnSyncMock.mockReturnValue({ status: 0 });
|
|
@@ -134,6 +141,7 @@ function mockSuccessfulNativeCodegen(): () => void {
|
|
|
134
141
|
return child;
|
|
135
142
|
});
|
|
136
143
|
return () => {
|
|
144
|
+
jest.mocked(childProcess.execFile).mockClear();
|
|
137
145
|
spawnMock.mockReset();
|
|
138
146
|
spawnSyncMock.mockReset();
|
|
139
147
|
};
|
|
@@ -525,6 +533,7 @@ describe("plan / apply / drift / verify machine commands", () => {
|
|
|
525
533
|
expect(fs.existsSync(path.join(tempDir, "functions", "openapi"))).toBe(false);
|
|
526
534
|
expect(fs.existsSync(path.join(tempDir, modelPath))).toBe(false);
|
|
527
535
|
expect(childProcess.spawn).not.toHaveBeenCalled();
|
|
536
|
+
expect(childProcess.execFile).not.toHaveBeenCalled();
|
|
528
537
|
|
|
529
538
|
const applied = await runMachine(
|
|
530
539
|
machineArgs("apply", "scaffold", "--plan", plan.response.data.planId)
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
2
4
|
import {
|
|
3
|
-
|
|
5
|
+
buildProjectLocalUvInstallerEnv,
|
|
4
6
|
buildUvPipInstallArgs,
|
|
5
7
|
buildUvVenvArgs,
|
|
6
8
|
getProjectLocalUvPaths,
|
|
7
9
|
getPythonProjectRoot,
|
|
10
|
+
ensureUvVirtualEnvironment,
|
|
11
|
+
buildPythonCodegenInstallEnv,
|
|
8
12
|
} from "../utils/python-uv";
|
|
9
13
|
|
|
10
14
|
describe("python uv helpers", () => {
|
|
11
|
-
it("keeps uv
|
|
15
|
+
it("keeps only the fallback uv binary inside the project", () => {
|
|
12
16
|
const projectRoot = "C:\\repo";
|
|
13
17
|
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
14
18
|
|
|
15
19
|
expect(uvPaths.stateDir).toBe(path.join(projectRoot, ".uv"));
|
|
16
|
-
expect(uvPaths.cacheDir).toBe(path.join(projectRoot, ".uv", "cache"));
|
|
17
|
-
expect(uvPaths.pythonInstallDir).toBe(path.join(projectRoot, ".uv", "python"));
|
|
18
20
|
expect(uvPaths.localUvExecutable.endsWith(process.platform === "win32" ? path.join(".uv", "bin", "uv.exe") : path.join(".uv", "bin", "uv"))).toBe(true);
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -22,20 +24,47 @@ describe("python uv helpers", () => {
|
|
|
22
24
|
expect(getPythonProjectRoot(path.join("C:\\repo", "functions"))).toBe("C:\\repo");
|
|
23
25
|
});
|
|
24
26
|
|
|
25
|
-
it("
|
|
26
|
-
const
|
|
27
|
+
it("leaves uv interpreter discovery, cache paths and user settings unchanged during bootstrap", () => {
|
|
28
|
+
const configured = {
|
|
29
|
+
PATH: "system-path",
|
|
30
|
+
UV_PYTHON: "3.15",
|
|
31
|
+
UV_CACHE_DIR: "user-cache",
|
|
32
|
+
UV_PYTHON_INSTALL_DIR: "user-python",
|
|
33
|
+
UV_PYTHON_PREFERENCE: "system",
|
|
34
|
+
UV_PYTHON_DOWNLOADS: "never",
|
|
35
|
+
UV_PYTHON_NO_REGISTRY: "false",
|
|
36
|
+
};
|
|
37
|
+
expect(buildProjectLocalUvInstallerEnv(configured, "C:\\repo")).toMatchObject(configured);
|
|
38
|
+
expect(buildProjectLocalUvInstallerEnv({}, "C:\\repo").UV_PYTHON_PREFERENCE).toBeUndefined();
|
|
39
|
+
expect(buildProjectLocalUvInstallerEnv({}, "C:\\repo").UV_PYTHON_NO_REGISTRY).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("limits bootstrap changes to a project-local uv binary and its child environment", () => {
|
|
43
|
+
const base = { PSModulePath: "powershell-7-modules", UV_PYTHON: "3.15" };
|
|
44
|
+
const env = buildProjectLocalUvInstallerEnv(base, "C:\\repo");
|
|
45
|
+
expect(env.UV_UNMANAGED_INSTALL).toBe(path.join("C:\\repo", ".uv", "bin"));
|
|
46
|
+
expect(env.UV_NO_MODIFY_PATH).toBe("1");
|
|
47
|
+
expect(env.UV_PYTHON).toBe("3.15");
|
|
48
|
+
expect(env.PSModulePath).toBe(process.platform === "win32" ? undefined : base.PSModulePath);
|
|
49
|
+
expect(base.PSModulePath).toBe("powershell-7-modules");
|
|
50
|
+
});
|
|
27
51
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
it("preserves an existing unusable virtual environment instead of clearing it", async () => {
|
|
53
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "swallowkit-uv-test-"));
|
|
54
|
+
const marker = path.join(root, "keep.txt");
|
|
55
|
+
fs.writeFileSync(marker, "user data");
|
|
56
|
+
try {
|
|
57
|
+
await expect(
|
|
58
|
+
ensureUvVirtualEnvironment("uv-must-not-be-called", root, root, process.env)
|
|
59
|
+
).rejects.toThrow("The virtual environment was preserved");
|
|
60
|
+
expect(fs.readFileSync(marker, "utf8")).toBe("user data");
|
|
61
|
+
} finally {
|
|
62
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
63
|
+
}
|
|
35
64
|
});
|
|
36
65
|
|
|
37
66
|
it("builds uv commands for venv creation and requirements installation", () => {
|
|
38
|
-
expect(buildUvVenvArgs(".venv")).toEqual(["venv", ".venv"
|
|
67
|
+
expect(buildUvVenvArgs(".venv")).toEqual(["venv", ".venv"]);
|
|
39
68
|
expect(buildUvPipInstallArgs(path.join("C:\\repo", "functions", ".venv", "Scripts", "python.exe"), "requirements.txt")).toEqual([
|
|
40
69
|
"pip",
|
|
41
70
|
"install",
|
|
@@ -45,4 +74,11 @@ describe("python uv helpers", () => {
|
|
|
45
74
|
"requirements.txt",
|
|
46
75
|
]);
|
|
47
76
|
});
|
|
77
|
+
|
|
78
|
+
it("does not require a C compiler for PyYAML's optional codegen accelerator", () => {
|
|
79
|
+
expect(buildPythonCodegenInstallEnv({ UV_PYTHON: "3.15" })).toEqual({
|
|
80
|
+
UV_PYTHON: "3.15", PYYAML_FORCE_LIBYAML: "0",
|
|
81
|
+
});
|
|
82
|
+
expect(buildPythonCodegenInstallEnv({ PYYAML_FORCE_LIBYAML: "1" }).PYYAML_FORCE_LIBYAML).toBe("1");
|
|
83
|
+
});
|
|
48
84
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { ensureUvVirtualEnvironment, getVirtualEnvPythonPath } from "../utils/python-uv";
|
|
5
|
+
|
|
6
|
+
let mockInterpreterError: Error | null = null;
|
|
7
|
+
jest.mock("child_process", () => ({
|
|
8
|
+
execFile: jest.fn((
|
|
9
|
+
_command: string,
|
|
10
|
+
args: string[],
|
|
11
|
+
_options: object,
|
|
12
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
13
|
+
) => callback(args[0] === "--version" ? mockInterpreterError : null, "Python 3.15.1\n", "")),
|
|
14
|
+
}));
|
|
15
|
+
jest.mock("fs", () => ({ existsSync: jest.fn() }));
|
|
16
|
+
|
|
17
|
+
describe("uv virtual environment lifecycle", () => {
|
|
18
|
+
const cwd = path.resolve("python-fixture", "functions");
|
|
19
|
+
const venv = path.join(cwd, ".venv");
|
|
20
|
+
const env = { UV_PYTHON: "3.15", UV_PYTHON_DOWNLOADS: "never" };
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
mockInterpreterError = null;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("creates with normal uv selection, then checks that Python can actually execute", async () => {
|
|
28
|
+
jest.mocked(fs.existsSync).mockReturnValue(false);
|
|
29
|
+
await expect(ensureUvVirtualEnvironment("uv", venv, cwd, env)).resolves.toBe(getVirtualEnvPythonPath(venv));
|
|
30
|
+
expect(execFile).toHaveBeenNthCalledWith(1, "uv", ["venv", venv], { cwd, env }, expect.any(Function));
|
|
31
|
+
expect(execFile).toHaveBeenNthCalledWith(2, getVirtualEnvPythonPath(venv), ["--version"], { cwd, env }, expect.any(Function));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("reuses an existing executable environment without changing its interpreter", async () => {
|
|
35
|
+
jest.mocked(fs.existsSync).mockReturnValue(true);
|
|
36
|
+
await ensureUvVirtualEnvironment("uv", venv, cwd, env);
|
|
37
|
+
expect(execFile).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(execFile).toHaveBeenCalledWith(getVirtualEnvPythonPath(venv), ["--version"], { cwd, env }, expect.any(Function));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it.each([true, false])("reports blocked Python after checking the environment (existing=%s)", async exists => {
|
|
42
|
+
jest.mocked(fs.existsSync).mockReturnValue(exists);
|
|
43
|
+
mockInterpreterError = new Error("Application control blocked this file (os error 4551)");
|
|
44
|
+
await expect(ensureUvVirtualEnvironment("uv", venv, cwd, env)).rejects.toThrow("os error 4551");
|
|
45
|
+
expect(jest.mocked(execFile).mock.calls.some(call => call[1]?.includes("--clear"))).toBe(false);
|
|
46
|
+
expect(execFile).toHaveBeenCalledTimes(exists ? 1 : 2);
|
|
47
|
+
});
|
|
48
|
+
});
|