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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import {
|
|
3
|
+
buildReactDevConfigSource,
|
|
4
|
+
buildReactDevWatchIgnoredSource,
|
|
5
|
+
} from "../utils/react-dev";
|
|
6
|
+
import { buildReactDevArgs } from "../cli/commands/dev";
|
|
7
|
+
|
|
8
|
+
describe("React dev watch boundaries", () => {
|
|
9
|
+
it.each(["C:\\repo\\", "C:\\repo", "/repo/"])(
|
|
10
|
+
"ignores backend artifacts without excluding frontend or shared sources under %s",
|
|
11
|
+
root => {
|
|
12
|
+
const ignored: string[] = new Function("root", `return ${buildReactDevWatchIgnoredSource("root")}`)(root);
|
|
13
|
+
const normalized = root.replace(/\\/g, "/").replace(/\/$/, "");
|
|
14
|
+
expect(ignored).toEqual([
|
|
15
|
+
`${normalized}/functions/**`,
|
|
16
|
+
`${normalized}/bff/**`,
|
|
17
|
+
`${normalized}/.uv/**`,
|
|
18
|
+
`${normalized}/.swallowkit/**`,
|
|
19
|
+
`${normalized}/shared/dist/**`,
|
|
20
|
+
]);
|
|
21
|
+
expect(ignored.some(value => value.includes("/src/") || value.includes("/shared/models/"))).toBe(false);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function loadWrapper(config: object) {
|
|
26
|
+
const loadConfigFromFile = jest.fn(async () => ({
|
|
27
|
+
config,
|
|
28
|
+
dependencies: ["/repo/vite.config.mts", "/repo/vite-settings.ts"],
|
|
29
|
+
}));
|
|
30
|
+
const mergeConfig = jest.fn((base: object, overrides: object) => ({ base, overrides }));
|
|
31
|
+
const result = { exports: {} as { default: (env: object) => Promise<unknown> } };
|
|
32
|
+
const source = ts.transpileModule(buildReactDevConfigSource(), {
|
|
33
|
+
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
|
|
34
|
+
}).outputText;
|
|
35
|
+
new Function("require", "exports", source)(
|
|
36
|
+
() => ({ defineConfig: (value: unknown) => value, loadConfigFromFile, mergeConfig }),
|
|
37
|
+
result.exports
|
|
38
|
+
);
|
|
39
|
+
return { load: result.exports.default, loadConfigFromFile, mergeConfig };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it("loads the user's Vite config and merges rather than replacing their watch options", async () => {
|
|
43
|
+
const config = { plugins: ["user-plugin"], server: { watch: { ignored: ["**/custom/**"], usePolling: true } } };
|
|
44
|
+
const wrapper = loadWrapper(config);
|
|
45
|
+
const env = { command: "serve", mode: "development" };
|
|
46
|
+
await wrapper.load(env);
|
|
47
|
+
expect(wrapper.loadConfigFromFile).toHaveBeenCalledWith(env, undefined, process.cwd());
|
|
48
|
+
expect(wrapper.mergeConfig).toHaveBeenCalledWith(config, {
|
|
49
|
+
plugins: [expect.objectContaining({ name: "swallowkit-dev-watch-boundaries" })],
|
|
50
|
+
server: { watch: { ignored: expect.arrayContaining([expect.stringContaining("/functions/**")]) } },
|
|
51
|
+
});
|
|
52
|
+
const plugin = wrapper.mergeConfig.mock.calls[0][1] as {
|
|
53
|
+
plugins: Array<{ configResolved: (config: { configFileDependencies: string[] }) => void }>;
|
|
54
|
+
};
|
|
55
|
+
const resolved = { configFileDependencies: ["/repo/wrapper.mjs"] };
|
|
56
|
+
plugin.plugins[0].configResolved(resolved);
|
|
57
|
+
expect(resolved.configFileDependencies).toContain("/repo/vite.config.mts");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("respects an explicitly disabled watcher", async () => {
|
|
61
|
+
const config = { server: { watch: null } };
|
|
62
|
+
const wrapper = loadWrapper(config);
|
|
63
|
+
await expect(wrapper.load({})).resolves.toBe(config);
|
|
64
|
+
expect(wrapper.mergeConfig).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("passes the override config to existing projects without editing their Vite config", () => {
|
|
68
|
+
expect(buildReactDevArgs("pnpm", "3100", "127.0.0.1", ".swallowkit/config.mjs")).toEqual([
|
|
69
|
+
"exec", "vite", "--port", "3100", "--host", "127.0.0.1", "--strictPort", "--config", ".swallowkit/config.mjs",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
createReactBffProject,
|
|
15
15
|
buildReactSwaConfig,
|
|
16
16
|
} from "../core/project/react-project";
|
|
17
|
+
import { buildHomePage } from "../core/project/home-page";
|
|
17
18
|
import {
|
|
18
19
|
generateManagedFunctionConfig,
|
|
19
20
|
generateReactBffHelper,
|
|
@@ -505,6 +506,24 @@ describe("React project integration", () => {
|
|
|
505
506
|
).toBe(false);
|
|
506
507
|
});
|
|
507
508
|
|
|
509
|
+
it("generates the shared home page with the selected package manager and Tailwind defaults", () => {
|
|
510
|
+
createReactProject(project, "sample", "npm");
|
|
511
|
+
expect(fs.readFileSync(path.join(project, "src", "Home.tsx"), "utf8"))
|
|
512
|
+
.toBe(buildHomePage("react", "npm"));
|
|
513
|
+
|
|
514
|
+
const css = fs.readFileSync(path.join(project, "src", "style.css"), "utf8");
|
|
515
|
+
expect(css).toContain('@import "tailwindcss"');
|
|
516
|
+
expect(css).toContain("@media (prefers-color-scheme: dark)");
|
|
517
|
+
expect(css).toContain("--background: #ffffff");
|
|
518
|
+
expect(css).toContain("--background: #0a0a0a");
|
|
519
|
+
expect(css).toContain("--color-background: var(--background)");
|
|
520
|
+
expect(css).toContain("font-family: Arial, Helvetica, sans-serif");
|
|
521
|
+
expect(fs.readFileSync(path.join(project, "index.html"), "utf8"))
|
|
522
|
+
.toContain('class="h-full antialiased"');
|
|
523
|
+
expect(fs.readFileSync(path.join(project, "vite.config.mts"), "utf8"))
|
|
524
|
+
.toContain("plugins: [react(), tailwindcss()]");
|
|
525
|
+
});
|
|
526
|
+
|
|
508
527
|
it("plans and applies BFF bindings, React pages and metadata through the common path", async () => {
|
|
509
528
|
const plan = await planScaffoldOperation({ model: "todo" });
|
|
510
529
|
expect(plan.requiresApproval).toBe(false);
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -19,15 +19,15 @@ import {
|
|
|
19
19
|
import { BackendLanguage } from '../../types';
|
|
20
20
|
import { detectFromProject, getCommands } from '../../utils/package-manager';
|
|
21
21
|
import {
|
|
22
|
-
buildProjectLocalUvEnv,
|
|
23
22
|
buildProjectLocalUvInstallerEnv,
|
|
24
23
|
buildUvPipInstallArgs,
|
|
25
|
-
|
|
24
|
+
ensureUvVirtualEnvironment,
|
|
26
25
|
getProjectLocalUvInstallerCommand,
|
|
27
26
|
getProjectLocalUvPaths,
|
|
28
27
|
getPythonProjectRoot,
|
|
29
28
|
} from '../../utils/python-uv';
|
|
30
29
|
import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
|
|
30
|
+
import { buildReactDevConfigSource } from '../../utils/react-dev';
|
|
31
31
|
|
|
32
32
|
export interface DevOptions {
|
|
33
33
|
port?: string;
|
|
@@ -65,8 +65,8 @@ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions
|
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export function buildFunctionsStartArgs(functionsPort: string): string[] {
|
|
69
|
-
return ['start', '--port', functionsPort];
|
|
68
|
+
export function buildFunctionsStartArgs(functionsPort: string, backendLanguage: BackendLanguage = 'typescript'): string[] {
|
|
69
|
+
return [...(backendLanguage === 'csharp' ? [] : ['start']), '--port', functionsPort];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
export function parseCoreToolsVersion(output: string): string | null {
|
|
@@ -105,15 +105,15 @@ export function buildFunctionsCoreToolsCommand(
|
|
|
105
105
|
|
|
106
106
|
return {
|
|
107
107
|
command: 'npm',
|
|
108
|
-
argsPrefix: ['exec', '--yes', NPM_CORE_TOOLS_PACKAGE, '--'],
|
|
109
|
-
label: `npm
|
|
108
|
+
argsPrefix: ['exec', '--yes', '--package', NPM_CORE_TOOLS_PACKAGE, '--', 'dotnet', 'run', '--no-launch-profile', '--'],
|
|
109
|
+
label: `dotnet run with npm ${NPM_CORE_TOOLS_PACKAGE} (${reason})`,
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
return {
|
|
114
|
-
command: 'func',
|
|
115
|
-
argsPrefix: [],
|
|
116
|
-
label: installedVersion ? `func ${installedVersion}` : 'func',
|
|
114
|
+
command: backendLanguage === 'csharp' ? 'dotnet' : 'func',
|
|
115
|
+
argsPrefix: backendLanguage === 'csharp' ? ['run', '--no-launch-profile', '--'] : [],
|
|
116
|
+
label: backendLanguage === 'csharp' ? 'dotnet run' : installedVersion ? `func ${installedVersion}` : 'func',
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -138,8 +138,8 @@ export function buildSwaStartArgs(host: string | undefined, nextPort: string, sw
|
|
|
138
138
|
];
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export function buildReactDevArgs(pm: string, port: string, host = 'localhost'): string[] {
|
|
142
|
-
const args = ['vite', '--port', port, '--host', host, '--strictPort'];
|
|
141
|
+
export function buildReactDevArgs(pm: string, port: string, host = 'localhost', configPath?: string): string[] {
|
|
142
|
+
const args = ['vite', '--port', port, '--host', host, '--strictPort', ...(configPath ? ['--config', configPath] : [])];
|
|
143
143
|
return pm === 'pnpm' ? ['exec', ...args] : args;
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -270,7 +270,7 @@ async function checkCommand(
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
async function resolveProjectLocalUvCommand(projectRoot: string): Promise<{ command: string; env: NodeJS.ProcessEnv }> {
|
|
273
|
-
const uvEnv =
|
|
273
|
+
const uvEnv = { ...process.env };
|
|
274
274
|
const { localUvExecutable } = getProjectLocalUvPaths(projectRoot);
|
|
275
275
|
|
|
276
276
|
if (await checkCommand('uv', ['--version'], { shell: false })) {
|
|
@@ -471,22 +471,8 @@ async function bridgePythonCoreToolsForWindowsArm64(
|
|
|
471
471
|
async function preparePythonFunctionsEnvironment(functionsDir: string): Promise<NodeJS.ProcessEnv> {
|
|
472
472
|
const projectRoot = getPythonProjectRoot(functionsDir);
|
|
473
473
|
const { command: uvCommand, env: uvEnv } = await resolveProjectLocalUvCommand(projectRoot);
|
|
474
|
-
const { venvDir
|
|
475
|
-
const
|
|
476
|
-
cwd: functionsDir,
|
|
477
|
-
env: uvEnv,
|
|
478
|
-
shell: false,
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
if (!hasUsableVirtualEnv) {
|
|
482
|
-
const venvArgs = buildUvVenvArgs('.venv');
|
|
483
|
-
if (fs.existsSync(venvDir)) {
|
|
484
|
-
venvArgs.push('--clear');
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
console.log('📦 Creating Python virtual environment with uv...');
|
|
488
|
-
await runCommand(uvCommand, venvArgs, functionsDir, 'python virtual environment setup', uvEnv, false);
|
|
489
|
-
}
|
|
474
|
+
const { venvDir } = getPythonVirtualEnvPaths(functionsDir);
|
|
475
|
+
const pythonExecutable = await ensureUvVirtualEnvironment(uvCommand, venvDir, functionsDir, uvEnv);
|
|
490
476
|
|
|
491
477
|
console.log('📦 Installing Python Azure Functions dependencies with uv...');
|
|
492
478
|
await runCommand(
|
|
@@ -538,7 +524,7 @@ export async function assertDevPortAvailable(port: number, host: string): Promis
|
|
|
538
524
|
}
|
|
539
525
|
|
|
540
526
|
function stopDevProcesses(processes: ChildProcess[]): void {
|
|
541
|
-
const active = processes.filter(child => child.pid && !child.killed);
|
|
527
|
+
const active = processes.filter(child => child.pid && !child.killed && child.exitCode === null && child.signalCode === null);
|
|
542
528
|
if (process.platform === 'win32' && active.length) {
|
|
543
529
|
// Windows shell termination alone leaves Vite, Core Tools, and SWA running.
|
|
544
530
|
const ids = active.map(child => child.pid).join(',');
|
|
@@ -721,6 +707,14 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
721
707
|
const functionsBaseUrl = buildFunctionsBaseUrl(options.host, functionsPort);
|
|
722
708
|
let functionsReadinessPromise: Promise<boolean> | null = null;
|
|
723
709
|
let functionsReady = !!options.noFunctions;
|
|
710
|
+
let reactDevConfigDir: string | undefined;
|
|
711
|
+
const cleanupReactDevConfig = () => {
|
|
712
|
+
if (reactDevConfigDir) {
|
|
713
|
+
fs.rmSync(reactDevConfigDir, { recursive: true, force: true });
|
|
714
|
+
reactDevConfigDir = undefined;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
process.once('exit', cleanupReactDevConfig);
|
|
724
718
|
|
|
725
719
|
// Cleanup processes on Ctrl+C
|
|
726
720
|
process.on('SIGINT', async () => {
|
|
@@ -939,7 +933,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
939
933
|
|
|
940
934
|
// Azure Functions を起動
|
|
941
935
|
const functionsCommand = functionsCoreToolsCommand ?? buildFunctionsCoreToolsCommand(backendLanguage, installedCoreToolsVersion);
|
|
942
|
-
const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort)], {
|
|
936
|
+
const funcProcess = spawn(functionsCommand.command, [...functionsCommand.argsPrefix, ...buildFunctionsStartArgs(functionsPort, backendLanguage)], {
|
|
943
937
|
cwd: functionsDir,
|
|
944
938
|
shell: true,
|
|
945
939
|
stdio: 'pipe', // Always pipe to capture output
|
|
@@ -987,6 +981,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
987
981
|
console.error('⚠️ Azure Functions startup error:', error.message);
|
|
988
982
|
console.log('💡 Please ensure Azure Functions Core Tools is installed');
|
|
989
983
|
console.log(` ${pmCmd.addGlobal} azure-functions-core-tools@4`);
|
|
984
|
+
stopDevProcesses(processes);
|
|
985
|
+
process.exit(1);
|
|
990
986
|
});
|
|
991
987
|
|
|
992
988
|
funcProcess.on('close', (code) => {
|
|
@@ -997,6 +993,8 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
997
993
|
console.log(' Reinstall a matching Core Tools v4 package (Windows users should prefer the official x64/x86 MSI for their machine).');
|
|
998
994
|
console.log(' SwallowKit local Python dev uses functions/.venv and requirements.txt, but Core Tools still needs its own bundled Python worker.');
|
|
999
995
|
}
|
|
996
|
+
stopDevProcesses(processes);
|
|
997
|
+
process.exit(code || 1);
|
|
1000
998
|
}
|
|
1001
999
|
});
|
|
1002
1000
|
|
|
@@ -1083,7 +1081,17 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1083
1081
|
}
|
|
1084
1082
|
|
|
1085
1083
|
// 5. Start Next.js development server
|
|
1086
|
-
|
|
1084
|
+
let reactDevConfigPath: string | undefined;
|
|
1085
|
+
if (react) {
|
|
1086
|
+
const stateDir = path.join(process.cwd(), '.swallowkit');
|
|
1087
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1088
|
+
reactDevConfigDir = fs.mkdtempSync(path.join(stateDir, 'vite-dev-'));
|
|
1089
|
+
reactDevConfigPath = path.join(reactDevConfigDir, 'config.mjs');
|
|
1090
|
+
fs.writeFileSync(reactDevConfigPath, buildReactDevConfigSource());
|
|
1091
|
+
}
|
|
1092
|
+
const nextArgs = react
|
|
1093
|
+
? buildReactDevArgs(pm, port, options.host, reactDevConfigPath && path.relative(process.cwd(), reactDevConfigPath))
|
|
1094
|
+
: buildNextDevArgs(pm, port);
|
|
1087
1095
|
|
|
1088
1096
|
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1089
1097
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
@@ -1141,6 +1149,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1141
1149
|
|
|
1142
1150
|
nextProcess.on('error', (error) => {
|
|
1143
1151
|
console.error(`❌ ${frontendLabel} startup error:`, error.message);
|
|
1152
|
+
stopDevProcesses(processes);
|
|
1144
1153
|
process.exit(1);
|
|
1145
1154
|
});
|
|
1146
1155
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
buildCSharpCodegenToolManifestSource,
|
|
26
26
|
buildPythonCodegenRequirementsSource,
|
|
27
27
|
} from "../../core/scaffold/native-schema-generator";
|
|
28
|
-
import {
|
|
28
|
+
import { DEFAULT_AZURE_PYTHON_VERSION } from "../../utils/python-uv";
|
|
29
|
+
import { buildHomePage } from "../../core/project/home-page";
|
|
29
30
|
|
|
30
31
|
interface InitOptions {
|
|
31
32
|
name: string;
|
|
@@ -144,7 +145,7 @@ function getFunctionsRuntimeConfig(backendLanguage: BackendLanguage): { name: st
|
|
|
144
145
|
return { name: "dotnet-isolated", version: "10.0" };
|
|
145
146
|
}
|
|
146
147
|
if (backendLanguage === "python") {
|
|
147
|
-
return { name: "python", version:
|
|
148
|
+
return { name: "python", version: DEFAULT_AZURE_PYTHON_VERSION };
|
|
148
149
|
}
|
|
149
150
|
return { name: "node", version: "22" };
|
|
150
151
|
}
|
|
@@ -344,7 +345,7 @@ export async function initCommand(options: InitOptions) {
|
|
|
344
345
|
const azureConfig = await promptAzureConfig(options);
|
|
345
346
|
|
|
346
347
|
if (frontend === "react") {
|
|
347
|
-
createReactProject(projectDir, options.name);
|
|
348
|
+
createReactProject(projectDir, options.name, pm);
|
|
348
349
|
} else {
|
|
349
350
|
await createNextJsProject(options.name, pm);
|
|
350
351
|
await upgradeNextJs(projectDir, options.nextVersion || 'latest', pm);
|
|
@@ -711,7 +712,7 @@ export function buildCSharpFunctionsProjectSource(): string {
|
|
|
711
712
|
<None Remove="generated\\**\\bin\\**;generated\\**\\obj\\**" />
|
|
712
713
|
</ItemGroup>
|
|
713
714
|
<ItemGroup>
|
|
714
|
-
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.
|
|
715
|
+
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.63.2" />
|
|
715
716
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
716
717
|
<PackageReference Include="Azure.Identity" Version="1.17.0" />
|
|
717
718
|
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.52.0" />
|
|
@@ -778,13 +779,29 @@ function getSwallowKitPackageMetadata(): { name: string; version: string } {
|
|
|
778
779
|
};
|
|
779
780
|
}
|
|
780
781
|
|
|
781
|
-
export function buildGeneratedProjectDevDependencies(): Record<string, string> {
|
|
782
|
+
export function buildGeneratedProjectDevDependencies(frontend: FrontendFramework = "nextjs"): Record<string, string> {
|
|
782
783
|
const { name, version } = getSwallowKitPackageMetadata();
|
|
783
784
|
return {
|
|
784
785
|
[name]: version,
|
|
786
|
+
...(frontend === "nextjs" ? { "@tailwindcss/postcss": "^4.0.0" } : {}),
|
|
785
787
|
};
|
|
786
788
|
}
|
|
787
789
|
|
|
790
|
+
export function ensureNextTailwindPostcssConfig(projectDir: string): void {
|
|
791
|
+
if (["postcss.config.mjs", "postcss.config.js", "postcss.config.cjs", "postcss.config.json"]
|
|
792
|
+
.some(file => fs.existsSync(path.join(projectDir, file)))) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// New create-next-app templates can configure Tailwind for Turbopack only.
|
|
797
|
+
fs.writeFileSync(path.join(projectDir, "postcss.config.mjs"), `export default {
|
|
798
|
+
plugins: {
|
|
799
|
+
"@tailwindcss/postcss": {},
|
|
800
|
+
},
|
|
801
|
+
};
|
|
802
|
+
`);
|
|
803
|
+
}
|
|
804
|
+
|
|
788
805
|
export function buildSwallowKitMcpProjectConfigSource(pm: PackageManager = 'pnpm'): string {
|
|
789
806
|
const { name } = getSwallowKitPackageMetadata();
|
|
790
807
|
return JSON.stringify(
|
|
@@ -831,7 +848,7 @@ async function addSwallowKitFiles(
|
|
|
831
848
|
};
|
|
832
849
|
packageJson.devDependencies = {
|
|
833
850
|
...packageJson.devDependencies,
|
|
834
|
-
...buildGeneratedProjectDevDependencies(),
|
|
851
|
+
...buildGeneratedProjectDevDependencies(frontend),
|
|
835
852
|
};
|
|
836
853
|
|
|
837
854
|
packageJson.scripts = {
|
|
@@ -865,6 +882,8 @@ async function addSwallowKitFiles(
|
|
|
865
882
|
|
|
866
883
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
867
884
|
|
|
885
|
+
if (frontend === "nextjs") ensureNextTailwindPostcssConfig(projectDir);
|
|
886
|
+
|
|
868
887
|
// Create pnpm-workspace.yaml for local pnpm development when needed.
|
|
869
888
|
if (wsConfig.type === 'file') {
|
|
870
889
|
fs.writeFileSync(path.join(projectDir, wsConfig.filename), wsConfig.content + (frontend === "react" ? " esbuild: true\n" : ""));
|
|
@@ -1492,7 +1511,6 @@ public sealed class GreetFunction
|
|
|
1492
1511
|
}
|
|
1493
1512
|
|
|
1494
1513
|
function createPythonFunctionsProject(projectDir: string, functionsDir: string): void {
|
|
1495
|
-
fs.writeFileSync(path.join(projectDir, '.python-version'), '3.11\n');
|
|
1496
1514
|
ensureProjectGitignoreEntry(projectDir, '.uv');
|
|
1497
1515
|
|
|
1498
1516
|
fs.writeFileSync(path.join(functionsDir, 'requirements.txt'), `azure-functions>=1.20.0
|
|
@@ -1632,117 +1650,8 @@ export async function POST(request: NextRequest) {
|
|
|
1632
1650
|
|
|
1633
1651
|
async function createHomePage(projectDir: string, pm: PackageManager = 'pnpm') {
|
|
1634
1652
|
console.log('📦 Creating home page...\n');
|
|
1635
|
-
|
|
1636
|
-
const pmCmd = getCommands(pm);
|
|
1637
|
-
|
|
1638
|
-
const pageContent = `'use client'
|
|
1639
|
-
|
|
1640
|
-
export const dynamic = 'force-dynamic';
|
|
1641
1653
|
|
|
1642
|
-
|
|
1643
|
-
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
1644
|
-
|
|
1645
|
-
export default function Home() {
|
|
1646
|
-
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
1647
|
-
const [message, setMessage] = useState('');
|
|
1648
|
-
|
|
1649
|
-
const testConnection = async () => {
|
|
1650
|
-
setGreetingStatus('loading');
|
|
1651
|
-
try {
|
|
1652
|
-
const response = await fetch('/api/greet?name=SwallowKit');
|
|
1653
|
-
const data = await response.json();
|
|
1654
|
-
if (!response.ok) {
|
|
1655
|
-
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
1656
|
-
}
|
|
1657
|
-
setMessage(data.message);
|
|
1658
|
-
setGreetingStatus('success');
|
|
1659
|
-
} catch (error) {
|
|
1660
|
-
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
1661
|
-
setGreetingStatus('error');
|
|
1662
|
-
}
|
|
1663
|
-
};
|
|
1664
|
-
|
|
1665
|
-
return (
|
|
1666
|
-
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
1667
|
-
<div className="container mx-auto px-4 py-12">
|
|
1668
|
-
<header className="text-center mb-16">
|
|
1669
|
-
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
1670
|
-
Welcome to SwallowKit
|
|
1671
|
-
</h1>
|
|
1672
|
-
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
1673
|
-
Next.js on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
1674
|
-
</p>
|
|
1675
|
-
</header>
|
|
1676
|
-
|
|
1677
|
-
{/* Connection Test */}
|
|
1678
|
-
<section className="max-w-2xl mx-auto mb-12">
|
|
1679
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
1680
|
-
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
1681
|
-
Test BFF → Functions Connection
|
|
1682
|
-
</h2>
|
|
1683
|
-
<button
|
|
1684
|
-
onClick={testConnection}
|
|
1685
|
-
disabled={greetingStatus === 'loading'}
|
|
1686
|
-
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
1687
|
-
>
|
|
1688
|
-
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
1689
|
-
</button>
|
|
1690
|
-
{greetingStatus === 'success' && (
|
|
1691
|
-
<div className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
1692
|
-
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
1693
|
-
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
1694
|
-
</div>
|
|
1695
|
-
)}
|
|
1696
|
-
{greetingStatus === 'error' && (
|
|
1697
|
-
<div className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
1698
|
-
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
1699
|
-
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
1700
|
-
</div>
|
|
1701
|
-
)}
|
|
1702
|
-
</div>
|
|
1703
|
-
</section>
|
|
1704
|
-
|
|
1705
|
-
{/* Scaffolded Models Menu */}
|
|
1706
|
-
{scaffoldConfig.models.length > 0 ? (
|
|
1707
|
-
<section className="max-w-6xl mx-auto">
|
|
1708
|
-
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
1709
|
-
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
1710
|
-
{scaffoldConfig.models.map((model) => (
|
|
1711
|
-
<a
|
|
1712
|
-
key={model.name}
|
|
1713
|
-
href={model.path}
|
|
1714
|
-
className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
|
|
1715
|
-
>
|
|
1716
|
-
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
1717
|
-
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
1718
|
-
</a>
|
|
1719
|
-
))}
|
|
1720
|
-
</div>
|
|
1721
|
-
</section>
|
|
1722
|
-
) : (
|
|
1723
|
-
<section className="max-w-2xl mx-auto text-center">
|
|
1724
|
-
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
1725
|
-
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
1726
|
-
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
1727
|
-
Create your first model with Zod and generate CRUD operations automatically.
|
|
1728
|
-
</p>
|
|
1729
|
-
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1730
|
-
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
1731
|
-
</code>
|
|
1732
|
-
</div>
|
|
1733
|
-
</section>
|
|
1734
|
-
)}
|
|
1735
|
-
|
|
1736
|
-
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
1737
|
-
<p>Built with SwallowKit</p>
|
|
1738
|
-
</footer>
|
|
1739
|
-
</div>
|
|
1740
|
-
</div>
|
|
1741
|
-
);
|
|
1742
|
-
}
|
|
1743
|
-
`;
|
|
1744
|
-
|
|
1745
|
-
fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), pageContent);
|
|
1654
|
+
fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), buildHomePage("nextjs", pm));
|
|
1746
1655
|
|
|
1747
1656
|
console.log('✅ Home page created\n');
|
|
1748
1657
|
|
|
@@ -1796,9 +1705,8 @@ function createReadme(
|
|
|
1796
1705
|
const backendScaffoldNote = backendLanguage === 'typescript'
|
|
1797
1706
|
? '- Azure Functions CRUD endpoints'
|
|
1798
1707
|
: `- Azure Functions ${backendLanguageLabel} CRUD handlers\n- OpenAPI export + native-generated ${backendLanguageLabel} schema assets`;
|
|
1799
|
-
const pythonUvPaths = getProjectLocalUvPaths(projectDir);
|
|
1800
1708
|
const pythonLocalDevNote = backendLanguage === 'python'
|
|
1801
|
-
? `\n**Python local dev note**: SwallowKit uses \`uv\`
|
|
1709
|
+
? `\n**Python local dev note**: SwallowKit uses normal \`uv\` interpreter discovery and settings, including installed Python, \`.python-version\`, and \`UV_PYTHON\`. It does not pin local Python to the Azure deployment version (${DEFAULT_AZURE_PYTHON_VERSION}). Local Functions uses \`functions/.venv\` and schema generation uses \`functions/.codegen-venv\`. Existing environments are preserved; explicitly recreate them with uv when switching interpreters. A missing uv binary is bootstrapped under \`.uv/bin\` without changing PATH. Keep \`functions/requirements.txt\` and \`functions/requirements.codegen.txt\` as the dependency sources of truth.\n`
|
|
1802
1710
|
: '';
|
|
1803
1711
|
|
|
1804
1712
|
const readme = `# ${projectName}
|
|
@@ -2949,7 +2857,7 @@ jobs:
|
|
|
2949
2857
|
${commonSetup} - name: Setup Python
|
|
2950
2858
|
uses: actions/setup-python@v5
|
|
2951
2859
|
with:
|
|
2952
|
-
python-version: '
|
|
2860
|
+
python-version: '${DEFAULT_AZURE_PYTHON_VERSION}'
|
|
2953
2861
|
|
|
2954
2862
|
- name: Install Functions dependencies
|
|
2955
2863
|
run: |
|
|
@@ -3138,7 +3046,7 @@ variables:
|
|
|
3138
3046
|
steps:
|
|
3139
3047
|
${commonSetup} - task: UsePythonVersion@0
|
|
3140
3048
|
inputs:
|
|
3141
|
-
versionSpec: '
|
|
3049
|
+
versionSpec: '${DEFAULT_AZURE_PYTHON_VERSION}'
|
|
3142
3050
|
displayName: 'Install Python'
|
|
3143
3051
|
|
|
3144
3052
|
- script: |
|
|
@@ -71,6 +71,9 @@ function getBackendRules(backendLanguage: BackendLanguage): string {
|
|
|
71
71
|
"- All backend business logic and data access live in `functions/`.",
|
|
72
72
|
`- SwallowKit derives OpenAPI and native ${BACKEND_LANGUAGE_LABELS[backendLanguage]} schema assets from the Zod source of truth.`,
|
|
73
73
|
"- Keep custom business logic in ai-authored locations identified by the current boundary contract.",
|
|
74
|
+
backendLanguage === "python"
|
|
75
|
+
? "- Use uv with the user's interpreter/version settings. Local Python is not pinned to the Azure deployment runtime. Preserve functions/.venv and functions/.codegen-venv; do not clear them or weaken OS application-control policy to fix interpreter errors."
|
|
76
|
+
: "- Start the isolated Functions backend with dotnet run, not func start from the source directory, so the SDK loads extensions from the build output.",
|
|
74
77
|
].join("\n");
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -91,7 +94,7 @@ function interpolateTemplate(template: string, context: AgentInstructionContext)
|
|
|
91
94
|
? "├── src/ # React SPA (Vite + React Router)\n│ └── pages/ # generated/custom UI and route registrations\n├── bff/ # SWA managed HTTP-only Functions\n│ ├── src/ # BFF handlers and request-local helpers\n│ └── {function}/ # function.json registrations"
|
|
92
95
|
: "├── app/ # Next.js App Router\n│ ├── api/ # BFF routes\n│ └── {model}/ # generated/custom UI",
|
|
93
96
|
devGuidance: react
|
|
94
|
-
? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately."
|
|
97
|
+
? "Use http://localhost:4280 for the app. `swallowkit dev` runs Vite (3000), the managed BFF (7073), standalone Functions (7071), and SWA CLI (4280). SWA forwards /api only to the managed BFF, never directly to standalone Functions. `--no-functions` leaves the BFF running; `--no-swa` removes same-origin routing/auth emulation. BFF local settings belong in bff/local.settings.json. Build bundles BFF dependencies and copies staticwebapp.config.json into dist/. Deploy dist/ + bff/ together, and functions/ separately. Generated home, CRUD, and login pages share the Next.js presentation templates. Preserve their Tailwind styles and the light/dark defaults in src/style.css when editing them."
|
|
95
98
|
: "Use http://localhost:3000 for the app, or the SWA emulator port (4280) when testing SWA authentication. `swallowkit dev` runs Next.js and standalone Functions (7071). The BFF remains Next.js API routes.",
|
|
96
99
|
pm: context.packageManager,
|
|
97
100
|
runCmd,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FrontendFramework } from "../../types";
|
|
2
|
+
import { PackageManager, getCommands } from "../../utils/package-manager";
|
|
3
|
+
import { adaptReactPage } from "../scaffold/react-ui-generator";
|
|
4
|
+
|
|
5
|
+
export function buildHomePage(
|
|
6
|
+
frontend: FrontendFramework,
|
|
7
|
+
pm: PackageManager
|
|
8
|
+
): string {
|
|
9
|
+
const framework = frontend === "react" ? "React" : "Next.js";
|
|
10
|
+
const page = `import { useState } from 'react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { scaffoldConfig } from '@/lib/scaffold-config';
|
|
13
|
+
|
|
14
|
+
export default function Home() {
|
|
15
|
+
const [greetingStatus, setGreetingStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
|
16
|
+
const [message, setMessage] = useState('');
|
|
17
|
+
|
|
18
|
+
const testConnection = async () => {
|
|
19
|
+
setGreetingStatus('loading');
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch('/api/greet?name=SwallowKit');
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(data.error || \`Server error: \${response.status}\`);
|
|
25
|
+
}
|
|
26
|
+
setMessage(data.message);
|
|
27
|
+
setGreetingStatus('success');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
setMessage(error instanceof Error ? error.message : 'Failed to connect to Azure Functions');
|
|
30
|
+
setGreetingStatus('error');
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 dark:from-gray-900 dark:to-gray-800">
|
|
36
|
+
<div className="container mx-auto px-4 py-12">
|
|
37
|
+
<header className="text-center mb-16">
|
|
38
|
+
<h1 className="text-5xl font-bold text-gray-800 dark:text-white mb-4">
|
|
39
|
+
Welcome to SwallowKit
|
|
40
|
+
</h1>
|
|
41
|
+
<p className="text-xl text-gray-600 dark:text-gray-400">
|
|
42
|
+
${framework} on Azure Static Web Apps + Functions + Cosmos DB — Zod schema sharing
|
|
43
|
+
</p>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<section className="max-w-2xl mx-auto mb-12">
|
|
47
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 border border-gray-200 dark:border-gray-700">
|
|
48
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">
|
|
49
|
+
Test BFF → Functions Connection
|
|
50
|
+
</h2>
|
|
51
|
+
<button
|
|
52
|
+
onClick={testConnection}
|
|
53
|
+
disabled={greetingStatus === 'loading'}
|
|
54
|
+
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors"
|
|
55
|
+
>
|
|
56
|
+
{greetingStatus === 'loading' ? 'Testing...' : 'Test Connection'}
|
|
57
|
+
</button>
|
|
58
|
+
{greetingStatus === 'success' && (
|
|
59
|
+
<div role="status" className="mt-4 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
|
60
|
+
<p className="text-green-800 dark:text-green-200 font-medium">✅ Connection successful!</p>
|
|
61
|
+
<p className="text-green-700 dark:text-green-300 text-sm mt-1">{message}</p>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{greetingStatus === 'error' && (
|
|
65
|
+
<div role="alert" className="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
66
|
+
<p className="text-red-800 dark:text-red-200 font-medium">❌ Connection failed</p>
|
|
67
|
+
<p className="text-red-700 dark:text-red-300 text-sm mt-1">{message}</p>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</section>
|
|
72
|
+
|
|
73
|
+
{scaffoldConfig.models.length > 0 ? (
|
|
74
|
+
<section className="max-w-6xl mx-auto">
|
|
75
|
+
<h2 className="text-3xl font-bold mb-8 text-gray-900 dark:text-gray-100">Your Models</h2>
|
|
76
|
+
<div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
77
|
+
{scaffoldConfig.models.map((model) => (
|
|
78
|
+
<Link
|
|
79
|
+
key={model.name}
|
|
80
|
+
href={model.path}
|
|
81
|
+
className="block p-8 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl hover:shadow-lg hover:border-blue-400 dark:hover:border-blue-600 transition-all"
|
|
82
|
+
>
|
|
83
|
+
<h3 className="text-2xl font-semibold mb-2 text-gray-900 dark:text-gray-100">{model.label}</h3>
|
|
84
|
+
<p className="text-gray-600 dark:text-gray-400">Manage {model.label.toLowerCase()}</p>
|
|
85
|
+
</Link>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</section>
|
|
89
|
+
) : (
|
|
90
|
+
<section className="max-w-2xl mx-auto text-center">
|
|
91
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl p-12 border border-gray-200 dark:border-gray-700">
|
|
92
|
+
<h2 className="text-2xl font-semibold mb-4 text-gray-900 dark:text-gray-100">Get Started</h2>
|
|
93
|
+
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
94
|
+
Create your first model with Zod and generate CRUD operations automatically.
|
|
95
|
+
</p>
|
|
96
|
+
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
97
|
+
${getCommands(pm).exec} swallowkit scaffold shared/models/your-model.ts
|
|
98
|
+
</code>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<footer className="mt-16 text-center text-gray-600 dark:text-gray-400 text-sm">
|
|
104
|
+
<p>Built with SwallowKit</p>
|
|
105
|
+
</footer>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
return frontend === "react"
|
|
112
|
+
? adaptReactPage(page)
|
|
113
|
+
: `'use client'\n\nexport const dynamic = 'force-dynamic';\n\n${page}`;
|
|
114
|
+
}
|