swallowkit 1.1.2 → 2.0.0-alpha.2
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 +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +66 -21
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +108 -44
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -5
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +83 -39
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +38 -15
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +6 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +12 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +7 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +7 -0
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/react-project.d.ts +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +422 -0
- package/dist/core/project/react-project.js.map +1 -0
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/index.js +2 -2
- package/dist/machine/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +5 -2
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/agent-instructions.test.ts +1 -1
- package/src/__tests__/init.test.ts +29 -5
- package/src/__tests__/package-manager.test.ts +15 -0
- package/src/__tests__/react.test.ts +819 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/cli/commands/add-auth.ts +67 -23
- package/src/cli/commands/dev.ts +109 -46
- package/src/cli/commands/init.ts +75 -35
- package/src/cli/commands/scaffold.ts +40 -19
- package/src/cli/index.ts +6 -3
- package/src/core/config.ts +21 -1
- package/src/core/project/agent-instructions.ts +14 -1
- package/src/core/project/agent-templates/AGENTS.md +24 -9
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/capabilities.ts +14 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/react-project.ts +507 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/index.ts +3 -1
- package/src/machine/index.ts +2 -2
- package/src/types/index.ts +4 -0
- package/src/utils/package-manager.ts +6 -3
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
describe("npm release metadata", () => {
|
|
6
|
+
const repository = process.cwd();
|
|
7
|
+
const script = path.join(repository, "scripts", "release-metadata.cjs");
|
|
8
|
+
let directory: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
directory = fs.mkdtempSync(path.join(repository, ".tmp-release-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function run(
|
|
19
|
+
version: unknown,
|
|
20
|
+
options: {
|
|
21
|
+
refType?: string;
|
|
22
|
+
refName?: string;
|
|
23
|
+
lockVersion?: string;
|
|
24
|
+
rootVersion?: string;
|
|
25
|
+
noRootPackage?: boolean;
|
|
26
|
+
} = {}
|
|
27
|
+
) {
|
|
28
|
+
fs.writeFileSync(
|
|
29
|
+
path.join(directory, "package.json"),
|
|
30
|
+
JSON.stringify({ name: "swallowkit", version })
|
|
31
|
+
);
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(directory, "package-lock.json"),
|
|
34
|
+
JSON.stringify({
|
|
35
|
+
version: options.lockVersion ?? version,
|
|
36
|
+
packages: options.noRootPackage
|
|
37
|
+
? {}
|
|
38
|
+
: { "": { version: options.rootVersion ?? version } },
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
|
+
const outputPath = path.join(directory, "output.txt");
|
|
42
|
+
const result = spawnSync(process.execPath, [script], {
|
|
43
|
+
cwd: directory,
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
env: {
|
|
46
|
+
...process.env,
|
|
47
|
+
GITHUB_REF_TYPE: options.refType ?? "tag",
|
|
48
|
+
GITHUB_REF_NAME: options.refName ?? `v${version}`,
|
|
49
|
+
GITHUB_OUTPUT: outputPath,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(result.error).toBeUndefined();
|
|
53
|
+
const output = fs.existsSync(outputPath)
|
|
54
|
+
? fs.readFileSync(outputPath, "utf8")
|
|
55
|
+
: "";
|
|
56
|
+
return { ...result, output };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["2.0.0", "latest"],
|
|
61
|
+
["2.0.0-alpha.1", "alpha"],
|
|
62
|
+
["2.0.0-alpha.2", "alpha"],
|
|
63
|
+
["2.0.0-alpha", "alpha"],
|
|
64
|
+
["2.0.0-beta.1", "beta"],
|
|
65
|
+
["2.0.0-beta.12", "beta"],
|
|
66
|
+
["2.0.0-rc.1", "next"],
|
|
67
|
+
["2.0.0-next.1", "next"],
|
|
68
|
+
["2.0.0-alphabet.1", "next"],
|
|
69
|
+
["2.0.0-0", "next"],
|
|
70
|
+
["2.0.0+build.1", "latest"],
|
|
71
|
+
["2.0.0-alpha.1+build.1", "alpha"],
|
|
72
|
+
])("publishes %s on the %s channel", (version, channel) => {
|
|
73
|
+
const result = run(version);
|
|
74
|
+
expect(result.status).toBe(0);
|
|
75
|
+
expect(result.stderr).toBe("");
|
|
76
|
+
expect(result.output).toBe(`version=${version}\ndist_tag=${channel}\n`);
|
|
77
|
+
expect(result.stdout).toBe(result.output);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("supports manual branch dry runs without requiring a release tag", () => {
|
|
81
|
+
const result = run("2.0.0-alpha.1", {
|
|
82
|
+
refType: "branch",
|
|
83
|
+
refName: "main",
|
|
84
|
+
});
|
|
85
|
+
expect(result.status).toBe(0);
|
|
86
|
+
expect(result.output).toContain("dist_tag=alpha\n");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
{ refName: "v2.0.0" },
|
|
91
|
+
{ refName: "2.0.0-alpha.1" },
|
|
92
|
+
{ lockVersion: "1.1.2" },
|
|
93
|
+
{ rootVersion: "1.1.2" },
|
|
94
|
+
{ noRootPackage: true },
|
|
95
|
+
])("rejects inconsistent release metadata: %j", (options) => {
|
|
96
|
+
const result = run("2.0.0-alpha.1", options);
|
|
97
|
+
expect(result.status).not.toBe(0);
|
|
98
|
+
expect(result.stderr).toContain("must match");
|
|
99
|
+
expect(result.output).toBe("");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it.each([
|
|
103
|
+
undefined,
|
|
104
|
+
2,
|
|
105
|
+
"",
|
|
106
|
+
"v2.0.0-alpha.1",
|
|
107
|
+
"2.0",
|
|
108
|
+
"02.0.0",
|
|
109
|
+
"2.0.0-",
|
|
110
|
+
"2.0.0-alpha.01",
|
|
111
|
+
"2.0.0-alpha..1",
|
|
112
|
+
"2.0.0-alpha_1",
|
|
113
|
+
"2.0.0\n",
|
|
114
|
+
"2.0.0-alpha.1\ndist_tag=latest",
|
|
115
|
+
])("rejects invalid versions before emitting Actions outputs: %j", (version) => {
|
|
116
|
+
const result = run(version);
|
|
117
|
+
expect(result.status).not.toBe(0);
|
|
118
|
+
expect(result.stderr).toContain("must be a valid SemVer version");
|
|
119
|
+
expect(result.output).toBe("");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
|
-
import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
|
|
9
|
-
import {
|
|
8
|
+
import { ensureSwallowKitProject, getBackendLanguage, getFrontendFramework, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
|
|
9
|
+
import { generateManagedFunctionConfig, generateReactAuthBff, generateReactBffHelper } from "../../core/scaffold/react-bff-generator";
|
|
10
|
+
import { adaptReactPage } from "../../core/scaffold/react-ui-generator";
|
|
11
|
+
import { AuthConfig, AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
|
|
10
12
|
import {
|
|
11
13
|
generateAuthModels,
|
|
12
14
|
generateAuthFunctionsTS,
|
|
@@ -98,6 +100,32 @@ function defaultCustomJwtConfig(): CustomJwtConfig {
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
function writeCallFunction(cwd: string, session: FileOperationSession, nextSource: string, auth: AuthConfig): void {
|
|
104
|
+
if (getFrontendFramework() === "react") {
|
|
105
|
+
const name = JSON.parse(session.readFile(path.join(cwd, "package.json"))).name;
|
|
106
|
+
session.writeFile(path.join(cwd, "bff", "src", "lib", "call-function.ts"), generateReactBffHelper(name, auth), managedMeta);
|
|
107
|
+
} else {
|
|
108
|
+
session.writeFile(path.join(cwd, "lib", "api", "call-function.ts"), nextSource, managedMeta);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeLoginPage(cwd: string, session: FileOperationSession, source: string): void {
|
|
113
|
+
const react = getFrontendFramework() === "react";
|
|
114
|
+
const destination = react ? path.join(cwd, "src", "pages", "login.tsx") : path.join(cwd, "app", "login", "page.tsx");
|
|
115
|
+
session.writeFile(destination, react ? adaptReactPage(source) : source, managedMeta);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeAuthContext(cwd: string, session: FileOperationSession, source: string): void {
|
|
119
|
+
session.writeFile(path.join(cwd, "lib", "auth", "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(source) : source, managedMeta);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeManagedAuthRoute(cwd: string, session: FileOperationSession, customJwt: boolean): void {
|
|
123
|
+
const name = JSON.parse(session.readFile(path.join(cwd, "package.json"))).name;
|
|
124
|
+
const sharedName = JSON.parse(session.readFile(path.join(cwd, "shared", "package.json"))).name;
|
|
125
|
+
session.writeFile(path.join(cwd, "bff", "src", "auth", "index.ts"), generateReactAuthBff(name, sharedName, customJwt), managedMeta);
|
|
126
|
+
session.writeFile(path.join(cwd, "bff", "auth", "function.json"), generateManagedFunctionConfig("auth/{action}", ["GET", "POST"], "auth"), managedMeta);
|
|
127
|
+
}
|
|
128
|
+
|
|
101
129
|
function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage, session: FileOperationSession, allowedProviders?: string[]): void {
|
|
102
130
|
const providers = normalizeSwaProviders(allowedProviders);
|
|
103
131
|
const helperPath = backendLanguage === "typescript"
|
|
@@ -112,14 +140,12 @@ function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage, session: Fi
|
|
|
112
140
|
: generateSwaAuthHelperPython();
|
|
113
141
|
session.writeFile(helperPath, helper, managedMeta);
|
|
114
142
|
|
|
115
|
-
|
|
116
|
-
session.writeFile(callFnPath, generateBFFCallFunctionWithSwaAuth(), managedMeta);
|
|
143
|
+
writeCallFunction(cwd, session, generateBFFCallFunctionWithSwaAuth(), { provider: "swa" });
|
|
117
144
|
|
|
118
145
|
const authDir = path.join(cwd, "lib", "auth");
|
|
119
146
|
session.writeFile(path.join(authDir, "auth-context.tsx"), generateSwaAuthContext(providers), managedMeta);
|
|
120
147
|
updateRootLayoutWithAuthProvider(cwd, session);
|
|
121
|
-
|
|
122
|
-
session.writeFile(path.join(loginDir, "page.tsx"), generateSwaLoginPage(providers), managedMeta);
|
|
148
|
+
writeLoginPage(cwd, session, generateSwaLoginPage(providers));
|
|
123
149
|
updateSwaRouteConfig(cwd, session, providers);
|
|
124
150
|
}
|
|
125
151
|
|
|
@@ -127,10 +153,10 @@ function setupExternalTokenAuth(cwd: string, backendLanguage: BackendLanguage, s
|
|
|
127
153
|
const authDir = path.join(cwd, "lib", "auth");
|
|
128
154
|
writeIfMissing(session, path.join(authDir, "external-token-adapter.ts"), generateExternalTokenAdapter());
|
|
129
155
|
session.writeFile(path.join(authDir, "authenticated-fetch.ts"), generateAuthenticatedFetch(), managedMeta);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
session.writeFile(path.join(
|
|
156
|
+
writeAuthContext(cwd, session, generateExternalTokenAuthContext());
|
|
157
|
+
writeCallFunction(cwd, session, generateBFFCallFunctionWithAuth(), { provider: "external-token" });
|
|
158
|
+
if (getFrontendFramework() === "react") writeManagedAuthRoute(cwd, session, false);
|
|
159
|
+
else session.writeFile(path.join(cwd, "app", "api", "auth", "me", "route.ts"), generateExternalTokenBFFMeRoute(), managedMeta);
|
|
134
160
|
updateRootLayoutWithAuthProvider(cwd, session);
|
|
135
161
|
|
|
136
162
|
if (backendLanguage === "typescript") {
|
|
@@ -240,6 +266,14 @@ export function addAuthProviderToRootLayout(layout: string): string {
|
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
function updateRootLayoutWithAuthProvider(cwd: string, session: FileOperationSession): void {
|
|
269
|
+
if (getFrontendFramework() === "react") {
|
|
270
|
+
const mainPath = path.join(cwd, "src", "main.tsx");
|
|
271
|
+
const main = session.readFile(mainPath);
|
|
272
|
+
if (main.includes("<AuthProvider>")) return;
|
|
273
|
+
if (!main.includes("<App />")) throw new Error("Could not find <App /> in src/main.tsx; cannot install AuthProvider");
|
|
274
|
+
session.writeFile(mainPath, `import { AuthProvider } from '@/lib/auth/auth-context';\n${main.replace("<App />", "<AuthProvider><App /></AuthProvider>")}`, extensionMeta);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
243
277
|
const layoutPath = path.join(cwd, "app", "layout.tsx");
|
|
244
278
|
if (!session.fileExists(layoutPath)) {
|
|
245
279
|
throw new Error("app/layout.tsx was not found; cannot install AuthProvider");
|
|
@@ -364,27 +398,25 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
364
398
|
|
|
365
399
|
// 4. Generate proxy
|
|
366
400
|
console.log("\n Generating proxy...");
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
401
|
+
if (getFrontendFramework() === "nextjs") {
|
|
402
|
+
const proxyPath = path.join(cwd, "proxy.ts");
|
|
403
|
+
session.writeFile(proxyPath, generateProxy(projectName), managedMeta);
|
|
404
|
+
console.log(` Created: proxy.ts`);
|
|
405
|
+
}
|
|
370
406
|
|
|
371
407
|
// 5. Generate login page
|
|
372
408
|
console.log("\n Generating login page...");
|
|
373
|
-
|
|
374
|
-
session.writeFile(path.join(loginDir, "page.tsx"), generateLoginPage(), managedMeta);
|
|
375
|
-
console.log(` Created: app/login/page.tsx`);
|
|
409
|
+
writeLoginPage(cwd, session, generateLoginPage());
|
|
376
410
|
|
|
377
411
|
// 6. Generate auth context
|
|
378
412
|
console.log("\n Generating auth context...");
|
|
379
|
-
|
|
380
|
-
session.writeFile(path.join(authLibDir, "auth-context.tsx"), generateAuthContext(), managedMeta);
|
|
413
|
+
writeAuthContext(cwd, session, generateAuthContext());
|
|
381
414
|
console.log(`✅ Created: lib/auth/auth-context.tsx`);
|
|
382
415
|
|
|
383
416
|
// 7. Update callFunction with auth support
|
|
384
417
|
console.log("\n Updating callFunction with auth support...");
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
console.log(` Updated: lib/api/call-function.ts`);
|
|
418
|
+
writeCallFunction(cwd, session, generateBFFCallFunctionWithAuth(), { provider: "custom-jwt" });
|
|
419
|
+
console.log(` Updated: ${getFrontendFramework() === "react" ? "bff/src/lib" : "lib/api"}/call-function.ts`);
|
|
388
420
|
|
|
389
421
|
// 8. Update swallowkit.config.js
|
|
390
422
|
console.log("\n Updating configuration...");
|
|
@@ -498,11 +530,19 @@ function addNamedScheme(cwd: string, scheme: string, provider: AuthProvider, lan
|
|
|
498
530
|
if (provider === "external-token") {
|
|
499
531
|
writeIfMissing(session, path.join(clientDir, "token-adapter.ts"), generateExternalTokenAdapter());
|
|
500
532
|
writeIfMissing(session, path.join(clientDir, "authenticated-fetch.ts"), generateAuthenticatedFetch().replace("'./external-token-adapter'", "'./token-adapter'"));
|
|
501
|
-
|
|
533
|
+
const source = generateExternalTokenAuthContext().replace(/external-token-adapter/g, "token-adapter");
|
|
534
|
+
writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(source) : source);
|
|
502
535
|
} else if (provider === "swa") {
|
|
503
536
|
writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), generateSwaAuthContext(allowedProviders));
|
|
504
537
|
} else if (provider === "custom-jwt") {
|
|
505
|
-
writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), generateAuthContext());
|
|
538
|
+
writeIfMissing(session, path.join(clientDir, "auth-context.tsx"), getFrontendFramework() === "react" ? adaptReactPage(generateAuthContext()) : generateAuthContext());
|
|
539
|
+
}
|
|
540
|
+
if (getFrontendFramework() === "react") {
|
|
541
|
+
const existing = getValidatedFullConfig().auth;
|
|
542
|
+
writeCallFunction(cwd, session, "", {
|
|
543
|
+
...existing, schemes: { ...existing?.schemes, [scheme]: { provider } },
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
506
546
|
}
|
|
507
547
|
const callFunctionPath = path.join(cwd, "lib", "api", "call-function.ts");
|
|
508
548
|
if (!session.fileExists(callFunctionPath)) writeIfMissing(session, callFunctionPath, generateBFFCallFunctionWithMultipleAuth());
|
|
@@ -667,6 +707,10 @@ function generateFunctionsAuth(
|
|
|
667
707
|
}
|
|
668
708
|
|
|
669
709
|
function generateBFFAuth(cwd: string, projectName: string, sharedPackageName: string, session: FileOperationSession): void {
|
|
710
|
+
if (getFrontendFramework() === "react") {
|
|
711
|
+
writeManagedAuthRoute(cwd, session, true);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
670
714
|
const authApiDir = path.join(cwd, "app", "api", "auth");
|
|
671
715
|
|
|
672
716
|
// Login route
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { spawn, spawnSync, ChildProcess } from 'child_process';
|
|
3
3
|
import * as http from 'http';
|
|
4
4
|
import * as https from 'https';
|
|
5
5
|
import * as path from 'path';
|
|
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import * as net from 'net';
|
|
9
9
|
import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
|
|
10
|
-
import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
|
|
10
|
+
import { ensureSwallowKitProject, getBackendLanguage, getFrontendFramework, getAuthConfig, normalizeAuthConfig } from '../../core/config';
|
|
11
11
|
import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
12
12
|
import {
|
|
13
13
|
applyDevSeedEnvironment,
|
|
@@ -32,6 +32,7 @@ import { ConnectorMockServer } from '../../core/mock/connector-mock-server';
|
|
|
32
32
|
export interface DevOptions {
|
|
33
33
|
port?: string;
|
|
34
34
|
functionsPort?: string;
|
|
35
|
+
bffPort?: string;
|
|
35
36
|
host?: string;
|
|
36
37
|
open?: boolean;
|
|
37
38
|
verbose?: boolean;
|
|
@@ -125,7 +126,7 @@ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: s
|
|
|
125
126
|
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
|
|
129
|
+
export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string, bffPort?: string): string[] {
|
|
129
130
|
return [
|
|
130
131
|
'start',
|
|
131
132
|
`http://${host || 'localhost'}:${nextPort}`,
|
|
@@ -133,9 +134,15 @@ export function buildSwaStartArgs(host: string | undefined, nextPort: string, sw
|
|
|
133
134
|
'.',
|
|
134
135
|
'--port',
|
|
135
136
|
swaPort,
|
|
137
|
+
...(bffPort ? ['--api-devserver-url', buildFunctionsBaseUrl(host, bffPort)] : []),
|
|
136
138
|
];
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
export function buildReactDevArgs(pm: string, port: string, host = 'localhost'): string[] {
|
|
142
|
+
const args = ['vite', '--port', port, '--host', host, '--strictPort'];
|
|
143
|
+
return pm === 'pnpm' ? ['exec', ...args] : args;
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
export function getSwaCliInstallCommand(pm: string): string {
|
|
140
147
|
return pm === 'pnpm'
|
|
141
148
|
? 'pnpm add -Dw @azure/static-web-apps-cli'
|
|
@@ -522,23 +529,48 @@ async function checkCosmosDBEmulator(): Promise<boolean> {
|
|
|
522
529
|
});
|
|
523
530
|
}
|
|
524
531
|
|
|
532
|
+
export async function assertDevPortAvailable(port: number, host: string): Promise<void> {
|
|
533
|
+
await new Promise<void>((resolve, reject) => {
|
|
534
|
+
const server = net.createServer();
|
|
535
|
+
server.once('error', error => reject(new Error(`Development port ${port} on ${host} is unavailable: ${error.message}`)));
|
|
536
|
+
server.listen({ port, host, exclusive: true }, () => server.close(error => error ? reject(error) : resolve()));
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function stopDevProcesses(processes: ChildProcess[]): void {
|
|
541
|
+
const active = processes.filter(child => child.pid && !child.killed);
|
|
542
|
+
if (process.platform === 'win32' && active.length) {
|
|
543
|
+
// Windows shell termination alone leaves Vite, Core Tools, and SWA running.
|
|
544
|
+
const ids = active.map(child => child.pid).join(',');
|
|
545
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
546
|
+
`$all = @(Get-CimInstance Win32_Process); function Stop-OwnedTree([int]$id) { foreach ($child in ($all | Where-Object ParentProcessId -eq $id)) { Stop-OwnedTree $child.ProcessId }; Stop-Process -Id $id -Force -ErrorAction SilentlyContinue }; foreach ($id in @(${ids})) { Stop-OwnedTree $id }`,
|
|
547
|
+
], { windowsHide: true, encoding: 'utf8' });
|
|
548
|
+
if (result.error || result.status !== 0) {
|
|
549
|
+
console.error('❌ Could not stop all development subprocesses:', result.error?.message || result.stderr);
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
for (const child of active) child.kill();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
525
556
|
export function buildDevCommand(
|
|
526
557
|
runDevEnvironment: (options: DevOptions) => Promise<void> = startDevEnvironment,
|
|
527
558
|
verifyProject: (commandName: string, projectRoot?: string) => void = ensureSwallowKitProject
|
|
528
559
|
): Command {
|
|
529
560
|
return new Command()
|
|
530
561
|
.name('dev')
|
|
531
|
-
.description('Start SwallowKit
|
|
532
|
-
.option('-p, --port <port>', '
|
|
562
|
+
.description('Start SwallowKit frontend, BFF, and standalone Azure Functions')
|
|
563
|
+
.option('-p, --port <port>', 'Frontend dev server port', '3000')
|
|
533
564
|
.option('-f, --functions-port <port>', 'Azure Functions port', '7071')
|
|
565
|
+
.option('--bff-port <port>', 'React managed BFF Functions port', '7073')
|
|
534
566
|
.option('--host <host>', 'Host name', 'localhost')
|
|
535
567
|
.option('--open', 'Open browser automatically', false)
|
|
536
568
|
.option('--verbose', 'Show verbose logs', false)
|
|
537
569
|
.option('--no-functions', 'Skip Azure Functions startup')
|
|
538
570
|
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
539
571
|
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
540
|
-
.option('--swa-port <port>', 'SWA authentication emulator port', '4280')
|
|
541
|
-
.option('--no-swa', 'Skip
|
|
572
|
+
.option('--swa-port <port>', 'SWA routing/authentication emulator port', '4280')
|
|
573
|
+
.option('--no-swa', 'Skip SWA routing/authentication emulation (React direct access has no same-origin API)')
|
|
542
574
|
.action(async (options: ParsedDevActionOptions) => {
|
|
543
575
|
const normalizedOptions = normalizeParsedDevOptions(options);
|
|
544
576
|
|
|
@@ -656,19 +688,24 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
656
688
|
const port = options.port || '3000';
|
|
657
689
|
const functionsPort = options.functionsPort || '7071';
|
|
658
690
|
const swaPort = options.swaPort || '4280';
|
|
691
|
+
const bffPort = options.bffPort || '7073';
|
|
659
692
|
|
|
660
693
|
// Detect package manager from project lockfile
|
|
661
694
|
const pm = detectFromProject();
|
|
662
695
|
const pmCmd = getCommands(pm);
|
|
663
696
|
const backendLanguage = getBackendLanguage();
|
|
697
|
+
const react = getFrontendFramework() === 'react';
|
|
698
|
+
const frontendLabel = react ? 'React/Vite' : 'Next.js';
|
|
664
699
|
const authConfig = getAuthConfig();
|
|
665
700
|
const normalizedAuth = normalizeAuthConfig(authConfig);
|
|
666
|
-
const
|
|
701
|
+
const authSchemes = Object.values(normalizedAuth?.schemes ?? {});
|
|
702
|
+
const swaScheme = authSchemes.find(scheme => scheme.provider === 'swa');
|
|
703
|
+
const useSwaEmulator = (react || Boolean(swaScheme)) && !options.noSwa;
|
|
667
704
|
const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
|
|
668
705
|
|
|
669
706
|
if (useSwaEmulator && !swaCommand) {
|
|
670
707
|
const installCommand = getSwaCliInstallCommand(pm);
|
|
671
|
-
console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
|
|
708
|
+
console.error('❌ SWA routing/authentication requires Azure Static Web Apps CLI for local development.');
|
|
672
709
|
console.error(' Install it in this project and retry:');
|
|
673
710
|
console.error(`\n ${installCommand}\n`);
|
|
674
711
|
process.exitCode = 1;
|
|
@@ -707,15 +744,22 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
707
744
|
if (mockServer) {
|
|
708
745
|
await mockServer.stop();
|
|
709
746
|
}
|
|
710
|
-
processes
|
|
711
|
-
if (proc && !proc.killed) {
|
|
712
|
-
proc.kill();
|
|
713
|
-
}
|
|
714
|
-
});
|
|
747
|
+
stopDevProcesses(processes);
|
|
715
748
|
process.exit(0);
|
|
716
749
|
});
|
|
717
750
|
|
|
718
751
|
try {
|
|
752
|
+
if (react) {
|
|
753
|
+
const ports = [port, bffPort, ...(!options.noFunctions ? [functionsPort] : []), ...(useSwaEmulator ? [swaPort] : []), ...(options.mockConnectors ? [String(Number(functionsPort) + 1)] : [])];
|
|
754
|
+
if (ports.some(value => !/^\d+$/.test(value) || Number(value) < 1 || Number(value) > 65535) || new Set(ports).size !== ports.length) {
|
|
755
|
+
throw new Error('Frontend, BFF, standalone Functions, mock, and SWA ports must be valid and distinct');
|
|
756
|
+
}
|
|
757
|
+
for (const value of ports) await assertDevPortAvailable(Number(value), options.host || 'localhost');
|
|
758
|
+
const coreToolsVersion = await resolveInstalledCoreToolsVersion();
|
|
759
|
+
if (!coreToolsVersion || compareVersionNumbers(coreToolsVersion, '4.0.0') < 0) {
|
|
760
|
+
throw new Error('React BFF requires Azure Functions Core Tools v4. Install it explicitly before running dev.');
|
|
761
|
+
}
|
|
762
|
+
}
|
|
719
763
|
// 1. Check for Next.js project
|
|
720
764
|
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
721
765
|
const nextConfigPathJs = path.join(process.cwd(), 'next.config.js');
|
|
@@ -724,11 +768,11 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
724
768
|
|
|
725
769
|
if (!fs.existsSync(packageJsonPath)) {
|
|
726
770
|
console.log('❌ package.json not found.');
|
|
727
|
-
console.log('💡 Please run this command in the root directory of a
|
|
771
|
+
console.log('💡 Please run this command in the root directory of a SwallowKit project.');
|
|
728
772
|
process.exit(1);
|
|
729
773
|
}
|
|
730
774
|
|
|
731
|
-
if (!fs.existsSync(nextConfigPathJs) && !fs.existsSync(nextConfigPathTs) && !fs.existsSync(nextConfigPathMjs)) {
|
|
775
|
+
if (!react && !fs.existsSync(nextConfigPathJs) && !fs.existsSync(nextConfigPathTs) && !fs.existsSync(nextConfigPathMjs)) {
|
|
732
776
|
console.log('⚠️ next.config file not found. Is this a Next.js project?');
|
|
733
777
|
}
|
|
734
778
|
|
|
@@ -963,14 +1007,14 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
963
1007
|
);
|
|
964
1008
|
} else if (!hasFunctions) {
|
|
965
1009
|
console.log('');
|
|
966
|
-
console.log(
|
|
1010
|
+
console.log(`ℹ️ functions/ directory not found. Starting ${frontendLabel}${react ? ' and managed BFF' : ''} only.`);
|
|
967
1011
|
} else if (options.noFunctions) {
|
|
968
1012
|
console.log('');
|
|
969
1013
|
console.log('ℹ️ --no-functions specified. Skipping Azure Functions.');
|
|
970
1014
|
}
|
|
971
1015
|
|
|
972
1016
|
console.log('');
|
|
973
|
-
console.log(
|
|
1017
|
+
console.log(`🚀 Starting ${frontendLabel} development server...`);
|
|
974
1018
|
|
|
975
1019
|
// Mock connector server — start proxy if --mock-connectors is enabled
|
|
976
1020
|
let bffTargetPort = functionsPort;
|
|
@@ -1039,7 +1083,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1039
1083
|
}
|
|
1040
1084
|
|
|
1041
1085
|
// 5. Start Next.js development server
|
|
1042
|
-
const nextArgs = buildNextDevArgs(pm, port);
|
|
1086
|
+
const nextArgs = react ? buildReactDevArgs(pm, port, options.host) : buildNextDevArgs(pm, port);
|
|
1043
1087
|
|
|
1044
1088
|
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1045
1089
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
@@ -1067,6 +1111,25 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1067
1111
|
FUNCTIONS_BASE_URL: bffTargetUrl,
|
|
1068
1112
|
};
|
|
1069
1113
|
|
|
1114
|
+
if (react) {
|
|
1115
|
+
await runCommand(pm, ['run', 'build'], path.join(process.cwd(), 'shared'), 'Shared schema build');
|
|
1116
|
+
const bffDir = path.join(process.cwd(), 'bff');
|
|
1117
|
+
await runCommand(pm, ['run', 'build'], bffDir, 'Managed BFF build');
|
|
1118
|
+
const bffProcess = spawn('func', buildFunctionsStartArgs(bffPort), {
|
|
1119
|
+
cwd: bffDir, shell: true, stdio: 'inherit', env: nextEnv,
|
|
1120
|
+
});
|
|
1121
|
+
processes.push(bffProcess);
|
|
1122
|
+
bffProcess.on('error', error => console.error('❌ Managed BFF startup error:', error.message));
|
|
1123
|
+
bffProcess.on('close', code => {
|
|
1124
|
+
console.error(`⏹️ Managed BFF exited (exit code: ${code})`);
|
|
1125
|
+
stopDevProcesses(processes);
|
|
1126
|
+
process.exit(code || 1);
|
|
1127
|
+
});
|
|
1128
|
+
if (!await waitForHttpServerReady(buildFunctionsBaseUrl(options.host, bffPort))) {
|
|
1129
|
+
throw new Error(`Managed BFF did not become ready on port ${bffPort}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1070
1133
|
const nextProcess = spawn(pm === 'pnpm' ? 'pnpm' : 'npx', nextArgs, {
|
|
1071
1134
|
cwd: process.cwd(),
|
|
1072
1135
|
shell: true,
|
|
@@ -1077,23 +1140,19 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1077
1140
|
processes.push(nextProcess);
|
|
1078
1141
|
|
|
1079
1142
|
nextProcess.on('error', (error) => {
|
|
1080
|
-
console.error(
|
|
1143
|
+
console.error(`❌ ${frontendLabel} startup error:`, error.message);
|
|
1081
1144
|
process.exit(1);
|
|
1082
1145
|
});
|
|
1083
1146
|
|
|
1084
1147
|
nextProcess.on('close', (code) => {
|
|
1085
1148
|
if (code !== 0) {
|
|
1086
|
-
console.log(`\n⏹️
|
|
1149
|
+
console.log(`\n⏹️ ${frontendLabel} exited (exit code: ${code})`);
|
|
1087
1150
|
}
|
|
1088
|
-
// Exit all processes when
|
|
1151
|
+
// Exit all owned processes when the frontend exits.
|
|
1089
1152
|
if (mockServer) {
|
|
1090
1153
|
mockServer.stop().catch(() => {});
|
|
1091
1154
|
}
|
|
1092
|
-
processes
|
|
1093
|
-
if (proc && !proc.killed) {
|
|
1094
|
-
proc.kill();
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1155
|
+
stopDevProcesses(processes);
|
|
1097
1156
|
process.exit(code || 0);
|
|
1098
1157
|
});
|
|
1099
1158
|
|
|
@@ -1110,14 +1169,14 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1110
1169
|
let swaReady = false;
|
|
1111
1170
|
if (useSwaEmulator && swaCommand) {
|
|
1112
1171
|
const nextUrl = `http://${options.host || 'localhost'}:${port}`;
|
|
1113
|
-
console.log(`⏳ Waiting for
|
|
1172
|
+
console.log(`⏳ Waiting for ${frontendLabel} before starting the SWA emulator at ${nextUrl}...`);
|
|
1114
1173
|
const nextReady = await waitForHttpServerReady(nextUrl);
|
|
1115
1174
|
if (!nextReady) {
|
|
1116
|
-
throw new Error(
|
|
1175
|
+
throw new Error(`${frontendLabel} did not become ready at ${nextUrl}`);
|
|
1117
1176
|
}
|
|
1118
1177
|
|
|
1119
|
-
console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
|
|
1120
|
-
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
|
|
1178
|
+
console.log(`🔐 Starting SWA routing/authentication emulator (port: ${swaPort})...`);
|
|
1179
|
+
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort, react ? bffPort : undefined), {
|
|
1121
1180
|
cwd: process.cwd(),
|
|
1122
1181
|
shell: true,
|
|
1123
1182
|
stdio: 'inherit',
|
|
@@ -1131,7 +1190,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1131
1190
|
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1132
1191
|
swaReady = await waitForHttpServerReady(swaUrl);
|
|
1133
1192
|
if (!swaReady) {
|
|
1134
|
-
throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
|
|
1193
|
+
throw new Error(`SWA routing/authentication emulator did not become ready at ${swaUrl}`);
|
|
1135
1194
|
}
|
|
1136
1195
|
}
|
|
1137
1196
|
|
|
@@ -1140,18 +1199,25 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1140
1199
|
console.log('');
|
|
1141
1200
|
if (useSwaEmulator) {
|
|
1142
1201
|
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1143
|
-
console.log('🔐 SWA
|
|
1202
|
+
console.log('🔐 SWA app (same-origin frontend and BFF):');
|
|
1144
1203
|
console.log(` ${swaUrl}`);
|
|
1145
1204
|
console.log('');
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1205
|
+
if (swaScheme) {
|
|
1206
|
+
const provider = swaScheme.swa?.allowedProviders?.[0] || 'aad';
|
|
1207
|
+
console.log('🔑 Local SWA sign-in:');
|
|
1208
|
+
console.log(` ${swaUrl}/.auth/login/${provider}`);
|
|
1209
|
+
console.log(' Enter any username and select Login.');
|
|
1210
|
+
console.log(' The "authenticated" role is added automatically.');
|
|
1211
|
+
console.log('');
|
|
1212
|
+
}
|
|
1213
|
+
if (authSchemes.some(scheme => scheme.provider === 'custom-jwt')) {
|
|
1214
|
+
console.log(`🔑 Application sign-in: ${swaUrl}/login`);
|
|
1215
|
+
console.log('');
|
|
1216
|
+
}
|
|
1217
|
+
console.log(`📱 ${frontendLabel} direct access (SWA routing/authentication is not emulated):`);
|
|
1152
1218
|
console.log(` http://${options.host || 'localhost'}:${port}`);
|
|
1153
1219
|
} else {
|
|
1154
|
-
console.log(`📱
|
|
1220
|
+
console.log(`📱 ${frontendLabel}: http://${options.host || 'localhost'}:${port}`);
|
|
1155
1221
|
}
|
|
1156
1222
|
if (hasFunctions && !options.noFunctions) {
|
|
1157
1223
|
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
@@ -1161,7 +1227,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1161
1227
|
}
|
|
1162
1228
|
console.log('');
|
|
1163
1229
|
if (hasFunctions && !options.noFunctions && functionsReady) {
|
|
1164
|
-
console.log(
|
|
1230
|
+
console.log(`💡 Azure Functions and ${react ? 'managed Functions' : 'Next.js'} BFF are connected`);
|
|
1165
1231
|
} else if (hasFunctions && !options.noFunctions) {
|
|
1166
1232
|
console.log('💡 Azure Functions is still warming up; BFF routes can fail until the backend responds.');
|
|
1167
1233
|
}
|
|
@@ -1171,6 +1237,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1171
1237
|
if (swaReady) {
|
|
1172
1238
|
console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
|
|
1173
1239
|
}
|
|
1240
|
+
if (react) console.log(`🔀 Managed BFF: ${buildFunctionsBaseUrl(options.host, bffPort)} (not the standalone Functions endpoint)`);
|
|
1174
1241
|
console.log('');
|
|
1175
1242
|
|
|
1176
1243
|
if (options.open) {
|
|
@@ -1189,11 +1256,7 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1189
1256
|
|
|
1190
1257
|
} catch (error) {
|
|
1191
1258
|
console.error('❌ Failed to start development environment:', error instanceof Error ? error.message : error);
|
|
1192
|
-
processes
|
|
1193
|
-
if (proc && !proc.killed) {
|
|
1194
|
-
proc.kill();
|
|
1195
|
-
}
|
|
1196
|
-
});
|
|
1259
|
+
stopDevProcesses(processes);
|
|
1197
1260
|
process.exit(1);
|
|
1198
1261
|
}
|
|
1199
1262
|
}
|