swallowkit 1.0.0-beta.60 → 1.0.0-beta.61
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 +3 -0
- package/README.md +3 -0
- package/dist/cli/commands/add-connector.d.ts.map +1 -1
- package/dist/cli/commands/add-connector.js +2 -1
- package/dist/cli/commands/add-connector.js.map +1 -1
- package/dist/cli/commands/create-model.js +1 -1
- package/dist/cli/commands/create-model.js.map +1 -1
- package/dist/cli/commands/init.d.ts +1 -5
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +45 -154
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.js +1 -1
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +2 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +43 -92
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/utils/package-manager.d.ts +4 -10
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +23 -15
- package/dist/utils/package-manager.js.map +1 -1
- package/package.json +8 -13
- package/src/__tests__/agent-instructions.test.ts +1 -1
- package/src/__tests__/connector-mock-server.test.ts +45 -25
- package/src/__tests__/init.test.ts +2 -50
- package/src/__tests__/model-parser.test.ts +61 -0
- package/src/__tests__/package-manager.test.ts +22 -5
- package/src/cli/commands/add-connector.ts +2 -1
- package/src/cli/commands/create-model.ts +1 -1
- package/src/cli/commands/init.ts +46 -173
- package/src/cli/commands/scaffold.ts +1 -1
- package/src/cli/index.ts +2 -0
- package/src/core/project/agent-instructions.ts +2 -2
- package/src/core/project/agent-templates/AGENTS.md +2 -0
- package/src/core/scaffold/model-parser.ts +40 -113
- package/src/utils/package-manager.ts +22 -17
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
getGitHubFunctionsWorkflow,
|
|
17
17
|
injectSwallowKitNextConfig,
|
|
18
18
|
parseIgnoredBuilds,
|
|
19
|
-
approveBuildsViaWorkspaceYaml,
|
|
20
19
|
buildCosmosDbFreeTierBicepSource,
|
|
21
20
|
buildFunctionsHostKeyBicepExpression,
|
|
22
21
|
buildStaticWebAppConfigBicepSource,
|
|
@@ -160,7 +159,7 @@ module.exports = nextConfig;
|
|
|
160
159
|
expect(parsed.mcpServers.swallowkit.env).toEqual({ SWALLOWKIT_MCP_VERSION: "latest" });
|
|
161
160
|
});
|
|
162
161
|
|
|
163
|
-
it("builds an npm-based MCP config when
|
|
162
|
+
it("builds an npm-based MCP config resolving latest when npm is selected", () => {
|
|
164
163
|
const source = buildSwallowKitMcpProjectConfigSource("npm");
|
|
165
164
|
const parsed = JSON.parse(source);
|
|
166
165
|
|
|
@@ -171,6 +170,7 @@ module.exports = nextConfig;
|
|
|
171
170
|
"swallowkit@${SWALLOWKIT_MCP_VERSION}",
|
|
172
171
|
"swallowkit-mcp",
|
|
173
172
|
]);
|
|
173
|
+
expect(parsed.mcpServers.swallowkit.env).toEqual({ SWALLOWKIT_MCP_VERSION: "latest" });
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
it("temporarily rewrites workspace protocol dependencies for npm lockfile generation", async () => {
|
|
@@ -239,54 +239,6 @@ describe("parseIgnoredBuilds", () => {
|
|
|
239
239
|
});
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
describe("approveBuildsViaWorkspaceYaml", () => {
|
|
243
|
-
let tempDir: string;
|
|
244
|
-
|
|
245
|
-
beforeEach(() => {
|
|
246
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "sk-allowbuilds-"));
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
afterEach(() => {
|
|
250
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
const yamlPath = () => path.join(tempDir, "pnpm-workspace.yaml");
|
|
254
|
-
|
|
255
|
-
it("appends an allowBuilds map to an existing workspace file", () => {
|
|
256
|
-
fs.writeFileSync(yamlPath(), "packages:\n - shared\n - functions\n", "utf-8");
|
|
257
|
-
approveBuildsViaWorkspaceYaml(tempDir, ["esbuild", "protobufjs"]);
|
|
258
|
-
const content = fs.readFileSync(yamlPath(), "utf-8");
|
|
259
|
-
expect(content).toContain("packages:");
|
|
260
|
-
expect(content).toContain("allowBuilds:\n esbuild: true\n protobufjs: true");
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it("replaces pnpm's placeholder entries with true", () => {
|
|
264
|
-
fs.writeFileSync(
|
|
265
|
-
yamlPath(),
|
|
266
|
-
"packages:\n - shared\nallowBuilds:\n esbuild: set this to true or false\n",
|
|
267
|
-
"utf-8"
|
|
268
|
-
);
|
|
269
|
-
approveBuildsViaWorkspaceYaml(tempDir, ["esbuild", "protobufjs"]);
|
|
270
|
-
const content = fs.readFileSync(yamlPath(), "utf-8");
|
|
271
|
-
expect(content).toContain("esbuild: true");
|
|
272
|
-
expect(content).toContain("protobufjs: true");
|
|
273
|
-
expect(content).not.toContain("set this to true or false");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("quotes scoped package names and creates the file when missing", () => {
|
|
277
|
-
approveBuildsViaWorkspaceYaml(tempDir, ["@scope/pkg"]);
|
|
278
|
-
const content = fs.readFileSync(yamlPath(), "utf-8");
|
|
279
|
-
expect(content).toContain("allowBuilds:\n '@scope/pkg': true");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("keeps existing entries untouched", () => {
|
|
283
|
-
fs.writeFileSync(yamlPath(), "allowBuilds:\n core-js: false\n", "utf-8");
|
|
284
|
-
approveBuildsViaWorkspaceYaml(tempDir, ["esbuild"]);
|
|
285
|
-
const content = fs.readFileSync(yamlPath(), "utf-8");
|
|
286
|
-
expect(content).toContain("core-js: false");
|
|
287
|
-
expect(content).toContain("esbuild: true");
|
|
288
|
-
});
|
|
289
|
-
});
|
|
290
242
|
|
|
291
243
|
describe("buildCosmosDbFreeTierBicepSource", () => {
|
|
292
244
|
it("keeps database-level shared throughput at 1000 RU/s for Free Tier", () => {
|
|
@@ -3,6 +3,67 @@ import * as path from "path";
|
|
|
3
3
|
import { parseModelFile } from "../core/scaffold/model-parser";
|
|
4
4
|
|
|
5
5
|
describe("parseModelFile", () => {
|
|
6
|
+
afterEach(() => { jest.restoreAllMocks(); });
|
|
7
|
+
|
|
8
|
+
it.each(["zod", "zod/v4"])("executes TypeScript modules and nested imports with %s without fallback", async (zodImport) => {
|
|
9
|
+
const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
|
|
10
|
+
const warning = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
11
|
+
try {
|
|
12
|
+
fs.writeFileSync(path.join(tempDir, "leaf.ts"), `
|
|
13
|
+
import { z } from '${zodImport}';
|
|
14
|
+
export const displayName = 'Leaf';
|
|
15
|
+
export const leafSchema = z.object({ name: z.string() });
|
|
16
|
+
`);
|
|
17
|
+
fs.writeFileSync(path.join(tempDir, "nested.ts"), `
|
|
18
|
+
import { z } from '${zodImport}';
|
|
19
|
+
import { leafSchema } from './leaf';
|
|
20
|
+
export const displayName = 'Nested';
|
|
21
|
+
export const nestedSchema = z.object({ leaf: leafSchema });
|
|
22
|
+
`);
|
|
23
|
+
const modelPath = path.join(tempDir, "record.ts");
|
|
24
|
+
fs.writeFileSync(modelPath, `
|
|
25
|
+
import { z } from '${zodImport}';
|
|
26
|
+
import { nestedSchema } from './nested';
|
|
27
|
+
import { leafSchema as otherSchema } from './leaf';
|
|
28
|
+
interface Settings { inner: { count: number }; }
|
|
29
|
+
type Label = string;
|
|
30
|
+
const label: Label = 'https://example.com';
|
|
31
|
+
const statuses = ['draft', 'active'] as const;
|
|
32
|
+
export const recordSchema = z.object({
|
|
33
|
+
id: z.string(), title: z.string().default(label), count: z.number(), enabled: z.boolean(),
|
|
34
|
+
optional: z.string().optional(), nullable: z.number().nullable(), status: z.enum(statuses),
|
|
35
|
+
numbers: z.array(z.number()), nested: nestedSchema, others: z.array(otherSchema),
|
|
36
|
+
inline: z.object({ name: z.string() }), tenantId: z.string(),
|
|
37
|
+
});
|
|
38
|
+
export type Record = z.infer<typeof recordSchema>;
|
|
39
|
+
export const displayName = 'Record';
|
|
40
|
+
export const partitionKey = '/tenantId';
|
|
41
|
+
export const connectorConfig = { connector: 'catalog', table: 'records', operations: ['read'] } as const;
|
|
42
|
+
export const authPolicy = { read: ['reader'], write: ['editor'] } as const;
|
|
43
|
+
`);
|
|
44
|
+
const model = await parseModelFile(modelPath);
|
|
45
|
+
expect(warning).not.toHaveBeenCalled();
|
|
46
|
+
expect(model.fields).toHaveLength(12);
|
|
47
|
+
expect(model.fields).toEqual(expect.arrayContaining([
|
|
48
|
+
expect.objectContaining({ name: "count", type: "number" }),
|
|
49
|
+
expect.objectContaining({ name: "enabled", type: "boolean" }),
|
|
50
|
+
expect.objectContaining({ name: "title", isOptional: true }),
|
|
51
|
+
expect.objectContaining({ name: "optional", isOptional: true }),
|
|
52
|
+
expect.objectContaining({ name: "nullable", isNullable: true }),
|
|
53
|
+
expect.objectContaining({ name: "status", enumValues: ["draft", "active"] }),
|
|
54
|
+
expect.objectContaining({ name: "numbers", type: "number", isArray: true }),
|
|
55
|
+
expect.objectContaining({ name: "nested", type: "object" }),
|
|
56
|
+
expect.objectContaining({ name: "others", type: "object", isArray: true }),
|
|
57
|
+
]));
|
|
58
|
+
expect(model.connectorConfig).toEqual(expect.objectContaining({ connector: "catalog", table: "records", operations: ["read"] }));
|
|
59
|
+
expect(model.partitionKey).toBe("/tenantId");
|
|
60
|
+
expect(model.authPolicy).toEqual({ read: ["reader"], write: ["editor"] });
|
|
61
|
+
expect(fs.readdirSync(tempDir).some(name => name.startsWith(".swallowkit-parser-"))).toBe(false);
|
|
62
|
+
} finally {
|
|
63
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
6
67
|
it("preserves array and enum metadata for schemas with defaults", async () => {
|
|
7
68
|
const tempDir = fs.mkdtempSync(path.join(process.cwd(), ".tmp-model-parser-"));
|
|
8
69
|
const modelPath = path.join(tempDir, "product.ts");
|
|
@@ -13,6 +13,20 @@ import {
|
|
|
13
13
|
} from "../utils/package-manager";
|
|
14
14
|
|
|
15
15
|
describe("detectFromUserAgent", () => {
|
|
16
|
+
const originalAgent = process.env.npm_config_user_agent;
|
|
17
|
+
beforeEach(() => { delete process.env.npm_config_user_agent; });
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (originalAgent === undefined) delete process.env.npm_config_user_agent;
|
|
20
|
+
else process.env.npm_config_user_agent = originalAgent;
|
|
21
|
+
});
|
|
22
|
+
it("honors npm invocation even with pnpm installed", () => {
|
|
23
|
+
process.env.npm_config_user_agent = "npm/11.0.0 node/v22.14.0";
|
|
24
|
+
expect(detectFromUserAgent(() => true)).toBe("npm");
|
|
25
|
+
});
|
|
26
|
+
it("honors pnpm invocation", () => {
|
|
27
|
+
process.env.npm_config_user_agent = "pnpm/11.26.0 npm/? node/v22.14.0";
|
|
28
|
+
expect(detectFromUserAgent(() => false)).toBe("pnpm");
|
|
29
|
+
});
|
|
16
30
|
it("falls back to npm when pnpm is not installed", () => {
|
|
17
31
|
expect(detectFromUserAgent(() => false)).toBe("npm");
|
|
18
32
|
});
|
|
@@ -55,7 +69,7 @@ describe("getCommands", () => {
|
|
|
55
69
|
});
|
|
56
70
|
|
|
57
71
|
it("returns correct runFilter for workspace", () => {
|
|
58
|
-
expect(cmds.runFilter("shared")).toBe("pnpm
|
|
72
|
+
expect(cmds.runFilter("shared")).toBe("pnpm --filter shared run");
|
|
59
73
|
});
|
|
60
74
|
|
|
61
75
|
it("returns --use-pnpm flag for create-next-app", () => {
|
|
@@ -98,8 +112,8 @@ describe("getCommands", () => {
|
|
|
98
112
|
expect(cmds.runFilter("shared")).toBe("npm run --workspace=shared");
|
|
99
113
|
});
|
|
100
114
|
|
|
101
|
-
it("
|
|
102
|
-
expect(cmds.createNextAppFlag).
|
|
115
|
+
it("explicitly selects npm for create-next-app", () => {
|
|
116
|
+
expect(cmds.createNextAppFlag).toBe("--use-npm");
|
|
103
117
|
});
|
|
104
118
|
});
|
|
105
119
|
});
|
|
@@ -113,6 +127,9 @@ describe("getWorkspaceConfig", () => {
|
|
|
113
127
|
expect(config.content).toContain("packages:");
|
|
114
128
|
expect(config.content).toContain(" - packages/*");
|
|
115
129
|
expect(config.content).toContain(" - apps/*");
|
|
130
|
+
expect(config.content).toContain("protobufjs: false");
|
|
131
|
+
expect(config.content).toContain("unrs-resolver: false");
|
|
132
|
+
expect(config.content).not.toContain(": true");
|
|
116
133
|
}
|
|
117
134
|
});
|
|
118
135
|
|
|
@@ -130,7 +147,7 @@ describe("getCiSetupStep", () => {
|
|
|
130
147
|
it("returns pnpm setup step for pnpm", () => {
|
|
131
148
|
const step = getCiSetupStep("pnpm");
|
|
132
149
|
expect(step).toContain("pnpm/action-setup");
|
|
133
|
-
expect(step).toContain("version:
|
|
150
|
+
expect(step).toContain("version: 11");
|
|
134
151
|
});
|
|
135
152
|
|
|
136
153
|
it("returns empty string for npm", () => {
|
|
@@ -176,7 +193,7 @@ describe("getAzurePipelinesSetup", () => {
|
|
|
176
193
|
|
|
177
194
|
describe("getBuildScript", () => {
|
|
178
195
|
it("uses pnpm run --filter for pnpm", () => {
|
|
179
|
-
expect(getBuildScript("pnpm")).toContain("pnpm
|
|
196
|
+
expect(getBuildScript("pnpm")).toContain("pnpm --filter shared run build");
|
|
180
197
|
expect(getBuildScript("pnpm")).toContain("fs.cpSync");
|
|
181
198
|
});
|
|
182
199
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
|
+
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
7
8
|
import * as path from "path";
|
|
8
9
|
import { ensureSwallowKitProject } from "../../core/config";
|
|
9
10
|
import { ApiConnectorConfig, ConnectorDefinition } from "../../types";
|
|
@@ -40,7 +41,7 @@ export async function addConnectorCommand(options: AddConnectorOptions) {
|
|
|
40
41
|
console.log("\n📝 Next steps:");
|
|
41
42
|
console.log(` 1. Review the connector settings in ${configPath}`);
|
|
42
43
|
console.log(` 2. Set the required environment variables in functions/local.settings.json`);
|
|
43
|
-
console.log(` 3. Create models with:
|
|
44
|
+
console.log(` 3. Create models with: ${getCommands(detectFromProject()).exec} swallowkit create-model <name> --connector=${options.name}`);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function findConfigFile(): string | null {
|
|
@@ -57,6 +57,6 @@ export async function createModelCommand(options: CreateModelOptions) {
|
|
|
57
57
|
if (result.createdFiles.length > 0) {
|
|
58
58
|
console.log("\n📝 Next steps:");
|
|
59
59
|
console.log(" 1. Customize the generated model fields in shared/models/");
|
|
60
|
-
console.log(` 2. Run '${getCommands(detectFromProject()).
|
|
60
|
+
console.log(` 2. Run '${getCommands(detectFromProject()).exec} swallowkit scaffold <model>' to generate CRUD code`);
|
|
61
61
|
}
|
|
62
62
|
}
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -34,6 +34,7 @@ interface InitOptions {
|
|
|
34
34
|
cosmosDbMode?: CosmosDbMode;
|
|
35
35
|
vnet?: VNetOption;
|
|
36
36
|
swaPlan?: StaticWebAppPlan;
|
|
37
|
+
packageManager?: PackageManager;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
type CiCdProvider = 'github' | 'azure' | 'skip';
|
|
@@ -255,6 +256,9 @@ const VALID_VNET: VNetOption[] = ['none', 'outbound'];
|
|
|
255
256
|
const VALID_SWA_PLAN: StaticWebAppPlan[] = ['free', 'standard'];
|
|
256
257
|
|
|
257
258
|
function validateInitFlags(options: InitOptions): void {
|
|
259
|
+
if (options.packageManager && !['npm', 'pnpm'].includes(options.packageManager)) {
|
|
260
|
+
throw new Error('Invalid --package-manager: expected npm or pnpm');
|
|
261
|
+
}
|
|
258
262
|
if (options.cicd && !VALID_CICD.includes(options.cicd)) {
|
|
259
263
|
console.error(`❌ Invalid --cicd value: "${options.cicd}". Must be: ${VALID_CICD.join(', ')}`);
|
|
260
264
|
process.exit(1);
|
|
@@ -285,7 +289,7 @@ export async function initCommand(options: InitOptions) {
|
|
|
285
289
|
console.log(`📋 Template: ${options.template}`);
|
|
286
290
|
|
|
287
291
|
// Detect package manager from invocation context (npx → npm, pnpm dlx → pnpm)
|
|
288
|
-
const pm: PackageManager = detectFromUserAgent();
|
|
292
|
+
const pm: PackageManager = options.packageManager || detectFromUserAgent();
|
|
289
293
|
const pmCmd = getCommands(pm);
|
|
290
294
|
console.log(`📦 Package manager: ${pm}`);
|
|
291
295
|
|
|
@@ -388,11 +392,11 @@ export async function initCommand(options: InitOptions) {
|
|
|
388
392
|
console.log(`\n✅ Project "${options.name}" created successfully!`);
|
|
389
393
|
console.log("\n📝 Next steps:");
|
|
390
394
|
console.log(` cd ${options.name}`);
|
|
391
|
-
console.log(` ${pmCmd.
|
|
392
|
-
console.log(` ${pmCmd.
|
|
393
|
-
console.log(` ${pmCmd.
|
|
395
|
+
console.log(` ${pmCmd.exec} swallowkit create-model <name> # Create your first model`);
|
|
396
|
+
console.log(` ${pmCmd.exec} swallowkit scaffold shared/models/<name>.ts # Generate CRUD code`);
|
|
397
|
+
console.log(` ${pmCmd.exec} swallowkit dev # Start development servers`);
|
|
394
398
|
console.log("\n🚀 Deploy to Azure:");
|
|
395
|
-
console.log(` ${pmCmd.
|
|
399
|
+
console.log(` ${pmCmd.exec} swallowkit provision --resource-group <name>`);
|
|
396
400
|
if (cicdProvider !== 'skip') {
|
|
397
401
|
console.log(" Configure CI/CD secrets and push to repository");
|
|
398
402
|
}
|
|
@@ -416,7 +420,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
|
|
|
416
420
|
// for npm use "npx create-next-app@latest ..."
|
|
417
421
|
const baseArgs = pm === 'pnpm'
|
|
418
422
|
? ['dlx', 'create-next-app@latest']
|
|
419
|
-
: ['create-next-app@latest'];
|
|
423
|
+
: ['--yes', '--package', 'create-next-app@latest', '--', 'create-next-app'];
|
|
420
424
|
|
|
421
425
|
const args = [
|
|
422
426
|
...baseArgs,
|
|
@@ -426,6 +430,7 @@ async function createNextJsProject(projectName: string, pm: PackageManager): Pro
|
|
|
426
430
|
'--app',
|
|
427
431
|
'--no-src',
|
|
428
432
|
'--disable-git',
|
|
433
|
+
'--skip-install',
|
|
429
434
|
'--import-alias',
|
|
430
435
|
'@/*',
|
|
431
436
|
...(pmCmd.createNextAppFlag ? [pmCmd.createNextAppFlag] : []),
|
|
@@ -462,8 +467,8 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
|
|
|
462
467
|
|
|
463
468
|
// pnpm: pnpm add next@... ; npm: npm install next@...
|
|
464
469
|
const args = pm === 'pnpm'
|
|
465
|
-
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact']
|
|
466
|
-
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact'];
|
|
470
|
+
? ['add', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--lockfile-only']
|
|
471
|
+
: ['install', `next@${version}`, `react@latest`, `react-dom@latest`, '--save-exact', '--package-lock-only'];
|
|
467
472
|
|
|
468
473
|
await runPackageManagerCommand(pm, args, projectDir, `${pm} add next@${version}`);
|
|
469
474
|
|
|
@@ -472,19 +477,7 @@ async function upgradeNextJs(projectDir: string, version: string, pm: PackageMan
|
|
|
472
477
|
|
|
473
478
|
async function installDependencies(projectDir: string, pm: PackageManager = 'pnpm'): Promise<void> {
|
|
474
479
|
console.log('\n📦 Installing dependencies...\n');
|
|
475
|
-
await runPackageManagerCommand(pm, ['install'], projectDir, `${pm} install`);
|
|
476
|
-
|
|
477
|
-
if (pm === 'pnpm') {
|
|
478
|
-
console.log('\n📦 Generating npm lockfile for CI/CD...\n');
|
|
479
|
-
await withNpmLockfileSafeManifests(projectDir, async () => {
|
|
480
|
-
await runPackageManagerCommand(
|
|
481
|
-
'npm',
|
|
482
|
-
['install', '--package-lock-only', '--ignore-scripts'],
|
|
483
|
-
projectDir,
|
|
484
|
-
'npm install --package-lock-only'
|
|
485
|
-
);
|
|
486
|
-
});
|
|
487
|
-
}
|
|
480
|
+
await runPackageManagerCommand(pm, pm === 'pnpm' ? ['install', '--no-frozen-lockfile'] : ['install'], projectDir, `${pm} install`);
|
|
488
481
|
|
|
489
482
|
console.log('\n✅ Dependencies installed\n');
|
|
490
483
|
}
|
|
@@ -579,44 +572,22 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
|
|
|
579
572
|
}
|
|
580
573
|
}
|
|
581
574
|
|
|
582
|
-
/**
|
|
583
|
-
* Run a package-manager command with stdio passed through to the user.
|
|
584
|
-
*
|
|
585
|
-
* For pnpm, additionally tee stdout/stderr to a buffer so that if the command
|
|
586
|
-
* fails due to `ERR_PNPM_IGNORED_BUILDS`, we can:
|
|
587
|
-
* 1. detect which packages were ignored,
|
|
588
|
-
* 2. approve them (interactively or automatically),
|
|
589
|
-
* 3. clean-reinstall and retry — repeating if pnpm reports further ignored
|
|
590
|
-
* packages (pnpm does not always list every unapproved package at once).
|
|
591
|
-
*/
|
|
575
|
+
/** Run installs without changing the project security policy. */
|
|
592
576
|
async function runPackageManagerCommand(
|
|
593
577
|
pm: PackageManager,
|
|
594
578
|
args: string[],
|
|
595
579
|
projectDir: string,
|
|
596
580
|
label: string,
|
|
597
581
|
): Promise<void> {
|
|
598
|
-
|
|
582
|
+
const { code, output } = await spawnAndCapture(pm, args, projectDir, pm === 'pnpm');
|
|
599
583
|
|
|
600
584
|
if (code === 0) return;
|
|
601
585
|
|
|
602
586
|
if (pm === 'pnpm') {
|
|
603
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
604
|
-
const ignoredBuilds = parseIgnoredBuilds(output);
|
|
605
|
-
if (ignoredBuilds.length === 0) break;
|
|
606
|
-
const approved = await maybeApproveBuilds(projectDir, ignoredBuilds);
|
|
607
|
-
if (!approved) break;
|
|
608
|
-
// 中断された install はパッケージ抽出が不完全なまま "Already up to date"
|
|
609
|
-
// と判定されることがあるため、node_modules を消して決定論的に入れ直す
|
|
610
|
-
removeWorkspaceNodeModules(projectDir);
|
|
611
|
-
const retry = await spawnAndCapture(pm, args, projectDir, true);
|
|
612
|
-
code = retry.code;
|
|
613
|
-
output = retry.output;
|
|
614
|
-
if (code === 0) return;
|
|
615
|
-
}
|
|
616
587
|
if (parseIgnoredBuilds(output).length > 0) {
|
|
617
588
|
throw new Error(
|
|
618
589
|
`${label} exited with code ${code}: pnpm still reports unapproved build scripts. ` +
|
|
619
|
-
'
|
|
590
|
+
'Review the named dependencies and project security policy before allowing any scripts. SwallowKit does not auto-approve them.'
|
|
620
591
|
);
|
|
621
592
|
}
|
|
622
593
|
}
|
|
@@ -624,19 +595,6 @@ async function runPackageManagerCommand(
|
|
|
624
595
|
throw new Error(`${label} exited with code ${code}`);
|
|
625
596
|
}
|
|
626
597
|
|
|
627
|
-
/** workspace root と直下パッケージの node_modules を削除する(クリーン再インストール用)。 */
|
|
628
|
-
function removeWorkspaceNodeModules(projectDir: string): void {
|
|
629
|
-
const targets = [projectDir, ...fs.readdirSync(projectDir, { withFileTypes: true })
|
|
630
|
-
.filter((entry) => entry.isDirectory() && entry.name !== 'node_modules' && !entry.name.startsWith('.'))
|
|
631
|
-
.map((entry) => path.join(projectDir, entry.name))];
|
|
632
|
-
for (const dir of targets) {
|
|
633
|
-
const nodeModules = path.join(dir, 'node_modules');
|
|
634
|
-
if (fs.existsSync(nodeModules)) {
|
|
635
|
-
fs.rmSync(nodeModules, { recursive: true, force: true });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
598
|
/**
|
|
641
599
|
* Spawn a child process inheriting stdin (so interactive prompts still work)
|
|
642
600
|
* while either inheriting or capturing+teeing stdout/stderr.
|
|
@@ -691,102 +649,6 @@ export function parseIgnoredBuilds(output: string): string[] {
|
|
|
691
649
|
return Array.from(new Set(list));
|
|
692
650
|
}
|
|
693
651
|
|
|
694
|
-
/**
|
|
695
|
-
* Prompt the user and, if approved, run `pnpm approve-builds` followed by
|
|
696
|
-
* `pnpm rebuild <packages>`. Returns true if the user approved (regardless of
|
|
697
|
-
* which packages they actually selected inside approve-builds).
|
|
698
|
-
*
|
|
699
|
-
* In non-interactive environments (CI, coding agents, piped output) prompting
|
|
700
|
-
* would hang forever, so build scripts are approved automatically via
|
|
701
|
-
* package.json `pnpm.onlyBuiltDependencies` instead.
|
|
702
|
-
*/
|
|
703
|
-
async function maybeApproveBuilds(projectDir: string, ignoredBuilds: string[]): Promise<boolean> {
|
|
704
|
-
console.log(
|
|
705
|
-
`\n⚠️ pnpm refused to run build scripts for: ${ignoredBuilds.join(', ')}\n` +
|
|
706
|
-
' These packages (e.g. sharp) need their build scripts to run correctly.\n',
|
|
707
|
-
);
|
|
708
|
-
|
|
709
|
-
const interactive =
|
|
710
|
-
process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI;
|
|
711
|
-
if (!interactive) {
|
|
712
|
-
console.log(
|
|
713
|
-
'ℹ️ Non-interactive environment detected — approving build scripts automatically\n' +
|
|
714
|
-
' via "allowBuilds" in pnpm-workspace.yaml.\n',
|
|
715
|
-
);
|
|
716
|
-
// rebuild は不要: 呼び出し側がクリーン再インストールで build script を実行する
|
|
717
|
-
approveBuildsViaWorkspaceYaml(projectDir, ignoredBuilds);
|
|
718
|
-
return true;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const response = await prompts({
|
|
722
|
-
type: 'confirm',
|
|
723
|
-
name: 'approve',
|
|
724
|
-
message: 'Run `pnpm approve-builds` now to approve these build scripts?',
|
|
725
|
-
initial: true,
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
if (!response.approve) {
|
|
729
|
-
console.log(
|
|
730
|
-
'\nℹ️ Skipped. You can run `pnpm approve-builds` later inside the project directory.\n',
|
|
731
|
-
);
|
|
732
|
-
return false;
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
await runSimple('pnpm', ['approve-builds'], projectDir);
|
|
736
|
-
await runSimple('pnpm', ['rebuild', ...ignoredBuilds], projectDir);
|
|
737
|
-
|
|
738
|
-
console.log('\n✅ Build scripts approved and packages rebuilt\n');
|
|
739
|
-
return true;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* pnpm-workspace.yaml の allowBuilds にパッケージを true で登録して build script を許可する。
|
|
744
|
-
* (pnpm 11+ の正準設定。失敗時に pnpm が追記する placeholder 行も true に置き換える)
|
|
745
|
-
*/
|
|
746
|
-
export function approveBuildsViaWorkspaceYaml(projectDir: string, packages: string[]): void {
|
|
747
|
-
const workspaceYamlPath = path.join(projectDir, 'pnpm-workspace.yaml');
|
|
748
|
-
const existing = fs.existsSync(workspaceYamlPath)
|
|
749
|
-
? fs.readFileSync(workspaceYamlPath, 'utf-8')
|
|
750
|
-
: '';
|
|
751
|
-
const yamlKey = (pkg: string) => (/^[A-Za-z0-9_.-]+$/.test(pkg) ? pkg : `'${pkg}'`);
|
|
752
|
-
|
|
753
|
-
const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
|
|
754
|
-
const keyIndex = lines.findIndex((line) => /^allowBuilds\s*:/.test(line));
|
|
755
|
-
|
|
756
|
-
if (keyIndex === -1) {
|
|
757
|
-
const block = `allowBuilds:\n${packages.map((pkg) => ` ${yamlKey(pkg)}: true`).join('\n')}\n`;
|
|
758
|
-
const base = existing.length > 0 && !existing.endsWith('\n') ? `${existing}\n` : existing;
|
|
759
|
-
fs.writeFileSync(workspaceYamlPath, `${base}${block}`, 'utf-8');
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// 既存マップ内の対象パッケージ(placeholder 含む)を true に更新し、無いものは追記する
|
|
764
|
-
let endIndex = keyIndex + 1;
|
|
765
|
-
const updated = new Set<string>();
|
|
766
|
-
while (endIndex < lines.length && /^\s+\S/.test(lines[endIndex])) {
|
|
767
|
-
const entry = lines[endIndex].match(/^\s+['"]?([A-Za-z0-9@/_.-]+)['"]?\s*:/);
|
|
768
|
-
if (entry && packages.includes(entry[1])) {
|
|
769
|
-
lines[endIndex] = ` ${yamlKey(entry[1])}: true`;
|
|
770
|
-
updated.add(entry[1]);
|
|
771
|
-
}
|
|
772
|
-
endIndex++;
|
|
773
|
-
}
|
|
774
|
-
const additions = packages.filter((pkg) => !updated.has(pkg)).map((pkg) => ` ${yamlKey(pkg)}: true`);
|
|
775
|
-
lines.splice(endIndex, 0, ...additions);
|
|
776
|
-
fs.writeFileSync(workspaceYamlPath, lines.join('\n'), 'utf-8');
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
function runSimple(command: string, args: string[], cwd: string): Promise<void> {
|
|
780
|
-
return new Promise((resolve, reject) => {
|
|
781
|
-
const child = spawn(command, args, { cwd, stdio: 'inherit', shell: true });
|
|
782
|
-
child.on('close', (code) => {
|
|
783
|
-
if (code !== 0) reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
|
784
|
-
else resolve();
|
|
785
|
-
});
|
|
786
|
-
child.on('error', reject);
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
|
|
790
652
|
export function injectSwallowKitNextConfig(nextConfigContent: string, projectName: string): string {
|
|
791
653
|
return nextConfigContent.replace(
|
|
792
654
|
/(const\s+nextConfig[:\s]*(?::\s*NextConfig\s*)?=\s*\{)(\s*\/\*[^*]*\*\/)?/,
|
|
@@ -937,7 +799,7 @@ async function addSwallowKitFiles(
|
|
|
937
799
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
938
800
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
939
801
|
|
|
940
|
-
//
|
|
802
|
+
// Root BFF/auth code imports Zod directly; declare it as well as the shared package.
|
|
941
803
|
packageJson.dependencies = {
|
|
942
804
|
...packageJson.dependencies,
|
|
943
805
|
...buildGeneratedProjectDependencies(projectName, pm),
|
|
@@ -955,8 +817,13 @@ async function addSwallowKitFiles(
|
|
|
955
817
|
};
|
|
956
818
|
|
|
957
819
|
packageJson.engines = {
|
|
958
|
-
node: '
|
|
820
|
+
node: '>=22.14.0',
|
|
821
|
+
...(pm === 'pnpm' ? { pnpm: '>=11 <13' } : {}),
|
|
822
|
+
};
|
|
823
|
+
packageJson.devEngines = {
|
|
824
|
+
packageManager: { name: pm, version: pm === 'pnpm' ? '>=11 <13' : '>=10', onFail: 'error' },
|
|
959
825
|
};
|
|
826
|
+
delete packageJson.packageManager;
|
|
960
827
|
|
|
961
828
|
// Keep package.json workspaces for npm-based CI/CD even when local development uses pnpm.
|
|
962
829
|
const workspacePackages = usesNodeFunctionsProject(backendLanguage) ? ['shared', 'functions'] : ['shared'];
|
|
@@ -1823,7 +1690,7 @@ export default function Home() {
|
|
|
1823
1690
|
Create your first model with Zod and generate CRUD operations automatically.
|
|
1824
1691
|
</p>
|
|
1825
1692
|
<code className="block bg-gray-100 dark:bg-gray-900 p-4 rounded text-left text-sm">
|
|
1826
|
-
${pmCmd.
|
|
1693
|
+
${pmCmd.exec} swallowkit scaffold shared/models/your-model.ts
|
|
1827
1694
|
</code>
|
|
1828
1695
|
</div>
|
|
1829
1696
|
</section>
|
|
@@ -1924,7 +1791,7 @@ This project was initialized with the following settings:
|
|
|
1924
1791
|
|
|
1925
1792
|
Before you begin, ensure you have the following installed:
|
|
1926
1793
|
|
|
1927
|
-
1. **Node.js
|
|
1794
|
+
1. **Node.js 22.14+**: [Download](https://nodejs.org/)${pm === 'pnpm' ? `\n2. **pnpm**: \`corepack enable\` or \`npm install -g pnpm\`` : ''}
|
|
1928
1795
|
${pm === 'pnpm' ? '3' : '2'}. **Azure CLI**: Required for provisioning Azure resources
|
|
1929
1796
|
- Install: \`winget install Microsoft.AzureCLI\` (Windows)
|
|
1930
1797
|
- Or: [Download](https://aka.ms/installazurecliwindows)
|
|
@@ -1932,7 +1799,7 @@ ${pm === 'pnpm' ? '4' : '3'}. **Azure Cosmos DB Emulator**: Required for local d
|
|
|
1932
1799
|
- Windows: \`winget install Microsoft.Azure.CosmosEmulator\`
|
|
1933
1800
|
- Or: [Download](https://aka.ms/cosmosdb-emulator)
|
|
1934
1801
|
- Docker: \`docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator\`
|
|
1935
|
-
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools**:
|
|
1802
|
+
${pm === 'pnpm' ? '6' : '5'}. **Azure Functions Core Tools v4**: Install separately for local Azure Functions execution
|
|
1936
1803
|
|
|
1937
1804
|
## 📁 Project Structure
|
|
1938
1805
|
|
|
@@ -1958,7 +1825,7 @@ ${functionsTree}
|
|
|
1958
1825
|
Define your data model with Zod schema:
|
|
1959
1826
|
|
|
1960
1827
|
\`\`\`bash
|
|
1961
|
-
${pmCmd.
|
|
1828
|
+
${pmCmd.exec} swallowkit create-model <model-name>
|
|
1962
1829
|
\`\`\`
|
|
1963
1830
|
|
|
1964
1831
|
This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to define your schema.
|
|
@@ -1968,7 +1835,7 @@ This creates a model file in \`shared/models/<model-name>.ts\`. Edit it to defin
|
|
|
1968
1835
|
Generate complete CRUD operations (Functions, API routes, UI):
|
|
1969
1836
|
|
|
1970
1837
|
\`\`\`bash
|
|
1971
|
-
${pmCmd.
|
|
1838
|
+
${pmCmd.exec} swallowkit scaffold shared/models/<model-name>.ts
|
|
1972
1839
|
\`\`\`
|
|
1973
1840
|
|
|
1974
1841
|
This generates:
|
|
@@ -1980,7 +1847,7 @@ ${backendScaffoldNote}
|
|
|
1980
1847
|
### 3. Start Development Servers
|
|
1981
1848
|
|
|
1982
1849
|
\`\`\`bash
|
|
1983
|
-
${pmCmd.
|
|
1850
|
+
${pmCmd.exec} swallowkit dev
|
|
1984
1851
|
\`\`\`
|
|
1985
1852
|
|
|
1986
1853
|
This starts:
|
|
@@ -1998,7 +1865,7 @@ ${pythonLocalDevNote}
|
|
|
1998
1865
|
Create all required Azure resources using Bicep:
|
|
1999
1866
|
|
|
2000
1867
|
\`\`\`bash
|
|
2001
|
-
${pmCmd.
|
|
1868
|
+
${pmCmd.exec} swallowkit provision --resource-group <rg-name>
|
|
2002
1869
|
\`\`\`
|
|
2003
1870
|
|
|
2004
1871
|
This creates:
|
|
@@ -2054,10 +1921,10 @@ func azure functionapp publish func-${projectName}
|
|
|
2054
1921
|
|
|
2055
1922
|
## 🔧 Available Commands
|
|
2056
1923
|
|
|
2057
|
-
- \`${pmCmd.
|
|
2058
|
-
- \`${pmCmd.
|
|
2059
|
-
- \`${pmCmd.
|
|
2060
|
-
- \`${pmCmd.
|
|
1924
|
+
- \`${pmCmd.exec} swallowkit create-model <name>\` - Create a new data model
|
|
1925
|
+
- \`${pmCmd.exec} swallowkit scaffold <model-file>\` - Generate CRUD code
|
|
1926
|
+
- \`${pmCmd.exec} swallowkit dev\` - Start development servers
|
|
1927
|
+
- \`${pmCmd.exec} swallowkit provision -g <rg-name>\` - Provision Azure resources
|
|
2061
1928
|
${azureConfig.vnetOption !== 'none' ? `
|
|
2062
1929
|
## 🔒 Network Security (VNet Configuration)
|
|
2063
1930
|
|
|
@@ -2113,7 +1980,7 @@ function createAiAgentFiles(
|
|
|
2113
1980
|
pm: PackageManager
|
|
2114
1981
|
) {
|
|
2115
1982
|
console.log('🤖 Creating AI agent instruction files...\n');
|
|
2116
|
-
const runCmd = pm
|
|
1983
|
+
const runCmd = getCommands(pm).exec;
|
|
2117
1984
|
|
|
2118
1985
|
writeAgentInstructionFiles(projectDir, {
|
|
2119
1986
|
projectName,
|
|
@@ -2902,11 +2769,13 @@ export function getGitHubFunctionsWorkflow(pm: PackageManager, backendLanguage:
|
|
|
2902
2769
|
|
|
2903
2770
|
const commonSetup = ` - uses: actions/checkout@v4
|
|
2904
2771
|
|
|
2772
|
+
${pnpmSetupStep ? `${pnpmSetupStep}\n` : ''}
|
|
2905
2773
|
- name: Setup Node.js
|
|
2906
2774
|
uses: actions/setup-node@v4
|
|
2907
2775
|
with:
|
|
2908
2776
|
node-version: '22'
|
|
2909
|
-
|
|
2777
|
+
cache: ${pm}
|
|
2778
|
+
cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
2910
2779
|
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
2911
2780
|
- name: Install dependencies
|
|
2912
2781
|
run: |
|
|
@@ -3441,7 +3310,7 @@ async function createGitHubActionsWorkflows(
|
|
|
3441
3310
|
const workflowsDir = path.join(projectDir, '.github', 'workflows');
|
|
3442
3311
|
fs.mkdirSync(workflowsDir, { recursive: true });
|
|
3443
3312
|
|
|
3444
|
-
const actionsPm: PackageManager =
|
|
3313
|
+
const actionsPm: PackageManager = pm;
|
|
3445
3314
|
|
|
3446
3315
|
const swaWorkflow = buildGitHubSwaWorkflow(actionsPm, pm);
|
|
3447
3316
|
fs.writeFileSync(path.join(workflowsDir, 'deploy-swa.yml'), swaWorkflow);
|
|
@@ -3469,6 +3338,7 @@ on:
|
|
|
3469
3338
|
- 'shared/**'
|
|
3470
3339
|
- 'public/**'
|
|
3471
3340
|
- 'package.json'
|
|
3341
|
+
- '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3472
3342
|
- 'next.config.js'
|
|
3473
3343
|
- 'next.config.ts'
|
|
3474
3344
|
workflow_dispatch:
|
|
@@ -3482,6 +3352,7 @@ on:
|
|
|
3482
3352
|
- 'shared/**'
|
|
3483
3353
|
- 'public/**'
|
|
3484
3354
|
- 'package.json'
|
|
3355
|
+
- '${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}'
|
|
3485
3356
|
- 'next.config.js'
|
|
3486
3357
|
- 'next.config.ts'
|
|
3487
3358
|
|
|
@@ -3495,11 +3366,13 @@ jobs:
|
|
|
3495
3366
|
with:
|
|
3496
3367
|
submodules: true
|
|
3497
3368
|
|
|
3369
|
+
${getCiSetupStep(pm) ? `${getCiSetupStep(pm)}\n` : ''}
|
|
3498
3370
|
- name: Setup Node.js
|
|
3499
3371
|
uses: actions/setup-node@v4
|
|
3500
3372
|
with:
|
|
3501
3373
|
node-version: '22'
|
|
3502
|
-
|
|
3374
|
+
cache: ${pm}
|
|
3375
|
+
cache-dependency-path: ${pm === 'pnpm' ? 'pnpm-lock.yaml' : 'package-lock.json'}
|
|
3503
3376
|
${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
3504
3377
|
- name: Install and build app
|
|
3505
3378
|
run: |
|
|
@@ -3524,7 +3397,7 @@ ${npmWorkspaceNormalizationStep ? `\n${npmWorkspaceNormalizationStep}` : ''}
|
|
|
3524
3397
|
async function createAzurePipelines(projectDir: string, pm: PackageManager, backendLanguage: BackendLanguage) {
|
|
3525
3398
|
console.log('📦 Creating Azure Pipelines...\n');
|
|
3526
3399
|
|
|
3527
|
-
const pipelinesPm: PackageManager =
|
|
3400
|
+
const pipelinesPm: PackageManager = pm;
|
|
3528
3401
|
const pipelinesDir = path.join(projectDir, 'pipelines');
|
|
3529
3402
|
fs.mkdirSync(pipelinesDir, { recursive: true });
|
|
3530
3403
|
|