spfn 0.2.0-beta.47 → 0.2.0-beta.49
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/LICENSE +1 -1
- package/README.md +309 -211
- package/dist/index.js +442 -1678
- package/dist/templates/.dockerignore +1 -0
- package/dist/templates/Dockerfile +1 -1
- package/dist/templates/lib/api-client.ts +1 -1
- package/dist/templates/server/config/env.config.ts +1 -1
- package/dist/templates/server/entities/config.ts +1 -1
- package/dist/templates/server/entities/example.entity.ts +1 -1
- package/dist/templates/server/repositories/example.repository.ts +1 -1
- package/dist/templates/server/router.ts +2 -3
- package/dist/templates/server/routes/examples.ts +2 -12
- package/dist/templates/server/routes/health.ts +1 -1
- package/dist/templates/server/routes/root.ts +1 -2
- package/dist/templates/server/server.config.ts +2 -2
- package/package.json +3 -3
- package/dist/commands/generate/templates/contract.template +0 -58
- package/dist/commands/generate/templates/entity.template +0 -28
- package/dist/commands/generate/templates/init-migration.template +0 -3
- package/dist/commands/generate/templates/repository.template +0 -56
- package/dist/commands/generate/templates/route.template +0 -65
- package/dist/commands/generate/templates/schema.template +0 -13
- package/dist/templates/Dockerfile.optimized +0 -67
- package/dist/templates/config/index.ts +0 -22
- package/dist/templates/config/schema.ts +0 -42
package/dist/index.js
CHANGED
|
@@ -535,8 +535,8 @@ async function setupApiProxy(cwd, includeAuth) {
|
|
|
535
535
|
const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
|
|
536
536
|
const rpcRoutePath = join6(rpcDir, "route.ts");
|
|
537
537
|
if (existsSync6(rpcRoutePath)) {
|
|
538
|
-
logger.
|
|
539
|
-
|
|
538
|
+
logger.warn(`RPC proxy route already exists, skipping: ${rpcRoutePath.replace(cwd + "/", "")}`);
|
|
539
|
+
return;
|
|
540
540
|
}
|
|
541
541
|
ensureDirSync3(rpcDir);
|
|
542
542
|
const authImport = includeAuth ? `import '@spfn/auth/nextjs/api';
|
|
@@ -573,10 +573,17 @@ var init_api_proxy = __esm({
|
|
|
573
573
|
});
|
|
574
574
|
|
|
575
575
|
// src/commands/init/steps/docker.ts
|
|
576
|
-
import { existsSync as existsSync7 } from "fs";
|
|
576
|
+
import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
|
|
577
577
|
import { join as join7 } from "path";
|
|
578
578
|
import fse4 from "fs-extra";
|
|
579
|
-
|
|
579
|
+
function adaptDockerfileForPm(content, pm) {
|
|
580
|
+
const adaptations = PM_DOCKERFILE_ADAPTATIONS[pm];
|
|
581
|
+
if (!adaptations) {
|
|
582
|
+
return content;
|
|
583
|
+
}
|
|
584
|
+
return adaptations.reduce((acc, [from, to]) => acc.replace(from, to), content);
|
|
585
|
+
}
|
|
586
|
+
async function setupDockerFiles(cwd, pm = "pnpm") {
|
|
580
587
|
const templatesDir = findTemplatesPath();
|
|
581
588
|
const dockerComposePath = join7(cwd, "docker-compose.yml");
|
|
582
589
|
if (!existsSync7(dockerComposePath)) {
|
|
@@ -595,8 +602,9 @@ async function setupDockerFiles(cwd) {
|
|
|
595
602
|
if (!existsSync7(dockerfilePath)) {
|
|
596
603
|
const dockerfileTemplate = join7(templatesDir, "Dockerfile");
|
|
597
604
|
if (existsSync7(dockerfileTemplate)) {
|
|
598
|
-
|
|
599
|
-
|
|
605
|
+
const content = adaptDockerfileForPm(readFileSync2(dockerfileTemplate, "utf-8"), pm);
|
|
606
|
+
writeFileSync3(dockerfilePath, content);
|
|
607
|
+
logger.success(`Created Dockerfile (${pm})`);
|
|
600
608
|
}
|
|
601
609
|
}
|
|
602
610
|
const dockerignorePath = join7(cwd, ".dockerignore");
|
|
@@ -619,13 +627,39 @@ async function setupDockerFiles(cwd) {
|
|
|
619
627
|
logger.warn("Could not copy Docker files (you can create them manually)");
|
|
620
628
|
}
|
|
621
629
|
}
|
|
622
|
-
var copySync2;
|
|
630
|
+
var copySync2, writeFileSync3, PM_DOCKERFILE_ADAPTATIONS;
|
|
623
631
|
var init_docker = __esm({
|
|
624
632
|
"src/commands/init/steps/docker.ts"() {
|
|
625
633
|
"use strict";
|
|
626
634
|
init_logger();
|
|
627
635
|
init_templates();
|
|
628
|
-
({ copySync: copySync2 } = fse4);
|
|
636
|
+
({ copySync: copySync2, writeFileSync: writeFileSync3 } = fse4);
|
|
637
|
+
PM_DOCKERFILE_ADAPTATIONS = {
|
|
638
|
+
npm: [
|
|
639
|
+
["# Install pnpm\nRUN corepack enable pnpm\n\n", ""],
|
|
640
|
+
["pnpm-lock.yaml*", "package-lock.json*"],
|
|
641
|
+
["RUN pnpm install --frozen-lockfile --prod=false", "RUN npm ci --include=dev"],
|
|
642
|
+
["RUN pnpm run spfn:build", "RUN npm run spfn:build"],
|
|
643
|
+
["RUN pnpm prune --prod", "RUN npm prune --omit=dev"],
|
|
644
|
+
['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["npm", "run", "spfn:start"]']
|
|
645
|
+
],
|
|
646
|
+
yarn: [
|
|
647
|
+
["RUN corepack enable pnpm", "RUN corepack enable yarn"],
|
|
648
|
+
["pnpm-lock.yaml*", "yarn.lock*"],
|
|
649
|
+
["RUN pnpm install --frozen-lockfile --prod=false", "RUN yarn install --frozen-lockfile"],
|
|
650
|
+
["RUN pnpm run spfn:build", "RUN yarn run spfn:build"],
|
|
651
|
+
["# Remove dev dependencies (optional, reduces image size)\nRUN pnpm prune --prod\n\n", ""],
|
|
652
|
+
['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["yarn", "run", "spfn:start"]']
|
|
653
|
+
],
|
|
654
|
+
bun: [
|
|
655
|
+
["# Install pnpm\nRUN corepack enable pnpm", "# Install bun\nRUN npm install -g bun"],
|
|
656
|
+
["pnpm-lock.yaml*", "bun.lockb*"],
|
|
657
|
+
["RUN pnpm install --frozen-lockfile --prod=false", "RUN bun install --frozen-lockfile"],
|
|
658
|
+
["RUN pnpm run spfn:build", "RUN bun run spfn:build"],
|
|
659
|
+
["# Remove dev dependencies (optional, reduces image size)\nRUN pnpm prune --prod\n\n", ""],
|
|
660
|
+
['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["bun", "run", "spfn:start"]']
|
|
661
|
+
]
|
|
662
|
+
};
|
|
629
663
|
}
|
|
630
664
|
});
|
|
631
665
|
|
|
@@ -737,25 +771,25 @@ export default {
|
|
|
737
771
|
}
|
|
738
772
|
}
|
|
739
773
|
`;
|
|
740
|
-
|
|
774
|
+
writeFileSync4(deploymentConfigPath, configContent);
|
|
741
775
|
logger.success(`Created spfn.config.js (subdomain: ${projectName}.spfn.app)`);
|
|
742
776
|
} catch (error) {
|
|
743
777
|
logger.warn("Could not create spfn.config.js");
|
|
744
778
|
}
|
|
745
779
|
}
|
|
746
780
|
}
|
|
747
|
-
var
|
|
781
|
+
var writeFileSync4;
|
|
748
782
|
var init_deployment_config = __esm({
|
|
749
783
|
"src/commands/init/steps/deployment-config.ts"() {
|
|
750
784
|
"use strict";
|
|
751
785
|
init_logger();
|
|
752
|
-
({ writeFileSync:
|
|
786
|
+
({ writeFileSync: writeFileSync4 } = fse5);
|
|
753
787
|
}
|
|
754
788
|
});
|
|
755
789
|
|
|
756
790
|
// src/utils/version.ts
|
|
757
791
|
function getCliVersion() {
|
|
758
|
-
return "0.2.0-beta.
|
|
792
|
+
return "0.2.0-beta.49";
|
|
759
793
|
}
|
|
760
794
|
function getTagFromVersion(version) {
|
|
761
795
|
const match = version.match(/-([a-z]+)\./i);
|
|
@@ -784,6 +818,8 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
784
818
|
packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
|
|
785
819
|
packageJson.dependencies["drizzle-orm"] = "^0.45.0";
|
|
786
820
|
packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
|
|
821
|
+
packageJson.dependencies["postgres"] = "^3.4.0";
|
|
822
|
+
packageJson.dependencies["pg"] = "^8.16.3";
|
|
787
823
|
packageJson.dependencies["spfn"] = spfnTag;
|
|
788
824
|
packageJson.dependencies["concurrently"] = "^9.2.1";
|
|
789
825
|
if (includeAuth) {
|
|
@@ -806,7 +842,7 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
806
842
|
packageJson.scripts["spfn:start"] = "spfn start";
|
|
807
843
|
packageJson.scripts["spfn:build"] = "spfn build";
|
|
808
844
|
packageJson.scripts["codegen"] = "spfn codegen run";
|
|
809
|
-
|
|
845
|
+
writeFileSync5(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
810
846
|
spinner.succeed("package.json updated");
|
|
811
847
|
spinner.start("Installing dependencies...");
|
|
812
848
|
try {
|
|
@@ -819,18 +855,18 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
819
855
|
process.exit(1);
|
|
820
856
|
}
|
|
821
857
|
}
|
|
822
|
-
var
|
|
858
|
+
var writeFileSync5;
|
|
823
859
|
var init_package = __esm({
|
|
824
860
|
"src/commands/init/steps/package.ts"() {
|
|
825
861
|
"use strict";
|
|
826
862
|
init_logger();
|
|
827
863
|
init_version();
|
|
828
|
-
({ writeFileSync:
|
|
864
|
+
({ writeFileSync: writeFileSync5 } = fse6);
|
|
829
865
|
}
|
|
830
866
|
});
|
|
831
867
|
|
|
832
868
|
// src/commands/init/steps/config-files.ts
|
|
833
|
-
import { existsSync as existsSync9, readFileSync as
|
|
869
|
+
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
|
|
834
870
|
import { join as join9 } from "path";
|
|
835
871
|
import fse7 from "fs-extra";
|
|
836
872
|
async function setupConfigFiles(cwd) {
|
|
@@ -857,7 +893,7 @@ export default defineConfig({
|
|
|
857
893
|
]
|
|
858
894
|
});
|
|
859
895
|
`;
|
|
860
|
-
|
|
896
|
+
writeFileSync6(spfnrcPath, spfnrcContent);
|
|
861
897
|
logger.success("Created .spfnrc.ts (codegen configuration)");
|
|
862
898
|
}
|
|
863
899
|
updateGitignore(cwd);
|
|
@@ -892,7 +928,8 @@ CACHE_URL=redis://localhost:6379
|
|
|
892
928
|
# SPFN App URL (optional, for CORS and redirects)
|
|
893
929
|
# SPFN_APP_URL=http://localhost:3790
|
|
894
930
|
`);
|
|
895
|
-
writeEnvExample(cwd, ".env.server.example", `# Server-only
|
|
931
|
+
writeEnvExample(cwd, ".env.server.example", `# Server-only environment (template)
|
|
932
|
+
# Copy to .env.server (gitignored) and fill in real values.
|
|
896
933
|
# These values are only loaded by the SPFN server, not by Next.js.
|
|
897
934
|
|
|
898
935
|
# Database pool
|
|
@@ -902,9 +939,8 @@ DB_POOL_IDLE_TIMEOUT=30
|
|
|
902
939
|
# Server timeouts
|
|
903
940
|
SERVER_TIMEOUT=120000
|
|
904
941
|
SHUTDOWN_TIMEOUT=30000
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
# Server-only sensitive values. Never commit this file.
|
|
942
|
+
|
|
943
|
+
# --- Secrets (never commit .env.server) ---
|
|
908
944
|
|
|
909
945
|
# Database write/read URLs (master-replica pattern, optional)
|
|
910
946
|
# DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
|
|
@@ -919,7 +955,7 @@ function writeEnvExample(cwd, filename, content) {
|
|
|
919
955
|
if (existsSync9(filePath)) {
|
|
920
956
|
return;
|
|
921
957
|
}
|
|
922
|
-
|
|
958
|
+
writeFileSync6(filePath, content);
|
|
923
959
|
logger.success(`Created ${filename}`);
|
|
924
960
|
}
|
|
925
961
|
function updateGitignore(cwd) {
|
|
@@ -928,7 +964,7 @@ function updateGitignore(cwd) {
|
|
|
928
964
|
return;
|
|
929
965
|
}
|
|
930
966
|
try {
|
|
931
|
-
const content =
|
|
967
|
+
const content = readFileSync3(gitignorePath, "utf-8");
|
|
932
968
|
let updated = content;
|
|
933
969
|
let changed = false;
|
|
934
970
|
if (!content.includes(".spfn")) {
|
|
@@ -943,11 +979,18 @@ function updateGitignore(cwd) {
|
|
|
943
979
|
# environment secrets (local overrides)
|
|
944
980
|
.env.local
|
|
945
981
|
.env.*.local
|
|
982
|
+
`;
|
|
983
|
+
changed = true;
|
|
984
|
+
}
|
|
985
|
+
if (!content.includes(".env.server")) {
|
|
986
|
+
updated += `
|
|
987
|
+
# spfn server env (secrets)
|
|
988
|
+
.env.server
|
|
946
989
|
`;
|
|
947
990
|
changed = true;
|
|
948
991
|
}
|
|
949
992
|
if (changed) {
|
|
950
|
-
|
|
993
|
+
writeFileSync6(gitignorePath, updated);
|
|
951
994
|
logger.success("Updated .gitignore with .spfn directory and env patterns");
|
|
952
995
|
}
|
|
953
996
|
} catch (error) {
|
|
@@ -960,26 +1003,26 @@ function updateTsconfig(cwd) {
|
|
|
960
1003
|
return;
|
|
961
1004
|
}
|
|
962
1005
|
try {
|
|
963
|
-
const tsconfigContent =
|
|
1006
|
+
const tsconfigContent = readFileSync3(tsconfigPath, "utf-8");
|
|
964
1007
|
const tsconfig = JSON.parse(tsconfigContent);
|
|
965
1008
|
if (!tsconfig.exclude) {
|
|
966
1009
|
tsconfig.exclude = [];
|
|
967
1010
|
}
|
|
968
1011
|
if (!tsconfig.exclude.includes("src/server")) {
|
|
969
1012
|
tsconfig.exclude.push("src/server");
|
|
970
|
-
|
|
1013
|
+
writeFileSync6(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
971
1014
|
logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
|
|
972
1015
|
}
|
|
973
1016
|
} catch (error) {
|
|
974
1017
|
logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
|
|
975
1018
|
}
|
|
976
1019
|
}
|
|
977
|
-
var
|
|
1020
|
+
var writeFileSync6;
|
|
978
1021
|
var init_config_files = __esm({
|
|
979
1022
|
"src/commands/init/steps/config-files.ts"() {
|
|
980
1023
|
"use strict";
|
|
981
1024
|
init_logger();
|
|
982
|
-
({ writeFileSync:
|
|
1025
|
+
({ writeFileSync: writeFileSync6 } = fse7);
|
|
983
1026
|
}
|
|
984
1027
|
});
|
|
985
1028
|
|
|
@@ -998,7 +1041,7 @@ async function initializeSpfn(options = {}) {
|
|
|
998
1041
|
logger.step(`Detected package manager: ${pm}`);
|
|
999
1042
|
await setupServerStructure(cwd);
|
|
1000
1043
|
await setupApiProxy(cwd, includeAuth);
|
|
1001
|
-
await setupDockerFiles(cwd);
|
|
1044
|
+
await setupDockerFiles(cwd, pm);
|
|
1002
1045
|
await setupDeploymentConfig(cwd, packageJson, pm);
|
|
1003
1046
|
await setupPackageJson(cwd, packageJsonPath, packageJson, pm, includeAuth);
|
|
1004
1047
|
await setupConfigFiles(cwd);
|
|
@@ -1046,7 +1089,7 @@ import chalk12 from "chalk";
|
|
|
1046
1089
|
import { join as join15 } from "path";
|
|
1047
1090
|
import { env as env2 } from "@spfn/core/config";
|
|
1048
1091
|
import { loadEnv as loadEnv2 } from "@spfn/core/server";
|
|
1049
|
-
import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as
|
|
1092
|
+
import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
1050
1093
|
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1051
1094
|
const nodeModulesPath = join15(cwd, "node_modules");
|
|
1052
1095
|
if (!existsSync16(nodeModulesPath)) {
|
|
@@ -1065,7 +1108,7 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
|
1065
1108
|
continue;
|
|
1066
1109
|
}
|
|
1067
1110
|
try {
|
|
1068
|
-
const packageJson = JSON.parse(
|
|
1111
|
+
const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
1069
1112
|
const spfnConfig = packageJson.spfn;
|
|
1070
1113
|
if (!spfnConfig?.migrations) {
|
|
1071
1114
|
continue;
|
|
@@ -1111,7 +1154,7 @@ async function migrateLegacyTable(db, functionMigrations) {
|
|
|
1111
1154
|
if (!existsSync16(journalPath)) {
|
|
1112
1155
|
continue;
|
|
1113
1156
|
}
|
|
1114
|
-
const journal = JSON.parse(
|
|
1157
|
+
const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
|
|
1115
1158
|
const entries = journal.entries || [];
|
|
1116
1159
|
const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
|
|
1117
1160
|
await db.execute(
|
|
@@ -1137,7 +1180,7 @@ async function migrateLegacyTable(db, functionMigrations) {
|
|
|
1137
1180
|
if (!existsSync16(sqlPath)) {
|
|
1138
1181
|
continue;
|
|
1139
1182
|
}
|
|
1140
|
-
const sqlContent =
|
|
1183
|
+
const sqlContent = readFileSync6(sqlPath, "utf-8");
|
|
1141
1184
|
const hash = createHash("sha256").update(sqlContent).digest("hex");
|
|
1142
1185
|
const found = legacyRows.find((r) => r.hash === hash);
|
|
1143
1186
|
if (found) {
|
|
@@ -1190,7 +1233,7 @@ var init_function_migrations = __esm({
|
|
|
1190
1233
|
});
|
|
1191
1234
|
|
|
1192
1235
|
// src/index.ts
|
|
1193
|
-
import { Command as
|
|
1236
|
+
import { Command as Command12 } from "commander";
|
|
1194
1237
|
|
|
1195
1238
|
// src/commands/create.ts
|
|
1196
1239
|
init_logger();
|
|
@@ -1366,7 +1409,6 @@ async function createProject(projectName, options) {
|
|
|
1366
1409
|
console.log(" " + chalk4.cyan("Or deploy with Docker:"));
|
|
1367
1410
|
console.log(` ${chalk4.cyan("docker compose -f docker-compose.production.yml up --build -d")}
|
|
1368
1411
|
`);
|
|
1369
|
-
console.log(chalk4.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide"));
|
|
1370
1412
|
console.log(chalk4.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
|
|
1371
1413
|
}
|
|
1372
1414
|
var createCommand = new Command3("create").description("Create a new Next.js project with SPFN").argument("<project-name>", "Name of the project directory").option("--skip-install", "Skip installing dependencies").option("--skip-git", "Skip initializing a git repository").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--shadcn", "Setup shadcn/ui (component library)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
|
|
@@ -1380,7 +1422,7 @@ init_init();
|
|
|
1380
1422
|
init_logger();
|
|
1381
1423
|
init_package_manager();
|
|
1382
1424
|
import { Command as Command4 } from "commander";
|
|
1383
|
-
import { existsSync as existsSync11, readFileSync as
|
|
1425
|
+
import { existsSync as existsSync11, readFileSync as readFileSync4, writeFileSync as writeFileSync7, mkdirSync, unlinkSync, rmSync, watch } from "fs";
|
|
1384
1426
|
import { join as join11 } from "path";
|
|
1385
1427
|
import { execa as execa4 } from "execa";
|
|
1386
1428
|
import chokidar from "chokidar";
|
|
@@ -1395,11 +1437,11 @@ function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
|
1395
1437
|
}, timeoutMs);
|
|
1396
1438
|
const dir = join11(filePath, "..");
|
|
1397
1439
|
const fileName = filePath.split("/").pop();
|
|
1398
|
-
const watcher = watch(dir, (
|
|
1440
|
+
const watcher = watch(dir, (_event, name) => {
|
|
1399
1441
|
if (name === fileName && existsSync11(filePath)) {
|
|
1400
1442
|
watcher.close();
|
|
1401
1443
|
clearTimeout(timer);
|
|
1402
|
-
resolve3(
|
|
1444
|
+
resolve3(readFileSync4(filePath, "utf-8").trim());
|
|
1403
1445
|
}
|
|
1404
1446
|
});
|
|
1405
1447
|
});
|
|
@@ -1419,7 +1461,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1419
1461
|
const packageJsonPath = join11(cwd, "package.json");
|
|
1420
1462
|
let hasNext = false;
|
|
1421
1463
|
if (existsSync11(packageJsonPath)) {
|
|
1422
|
-
const packageJson = JSON.parse(
|
|
1464
|
+
const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
|
|
1423
1465
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
1424
1466
|
}
|
|
1425
1467
|
const tempDir = join11(cwd, ".spfn");
|
|
@@ -1441,7 +1483,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1441
1483
|
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
1442
1484
|
configParts.push("debug: true");
|
|
1443
1485
|
const readyFile = join11(tempDir, "server-ready");
|
|
1444
|
-
|
|
1486
|
+
writeFileSync7(serverEntry, `
|
|
1445
1487
|
import { writeFileSync } from 'fs';
|
|
1446
1488
|
|
|
1447
1489
|
// Load environment variables FIRST (before any imports that depend on them)
|
|
@@ -1457,7 +1499,7 @@ const instance = await startServer({
|
|
|
1457
1499
|
// Signal ready with actual port
|
|
1458
1500
|
writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
|
|
1459
1501
|
`);
|
|
1460
|
-
|
|
1502
|
+
writeFileSync7(watcherEntry, `
|
|
1461
1503
|
// Load environment variables
|
|
1462
1504
|
// const { loadEnvFiles } = await import('@spfn/core/server');
|
|
1463
1505
|
// loadEnvFiles();
|
|
@@ -1730,7 +1772,7 @@ catch (error)
|
|
|
1730
1772
|
init_logger();
|
|
1731
1773
|
init_package_manager();
|
|
1732
1774
|
import { Command as Command5 } from "commander";
|
|
1733
|
-
import { existsSync as existsSync12, writeFileSync as
|
|
1775
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
1734
1776
|
import { join as join12 } from "path";
|
|
1735
1777
|
import { execa as execa5 } from "execa";
|
|
1736
1778
|
import ora5 from "ora";
|
|
@@ -1846,7 +1888,7 @@ await startServer({
|
|
|
1846
1888
|
debug: false
|
|
1847
1889
|
});
|
|
1848
1890
|
`;
|
|
1849
|
-
|
|
1891
|
+
writeFileSync8(prodServerPath, prodServerContent);
|
|
1850
1892
|
spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
|
|
1851
1893
|
const routesDir = join12(cwd, ".spfn", "server", "routes");
|
|
1852
1894
|
if (existsSync12(routesDir)) {
|
|
@@ -1905,7 +1947,7 @@ var buildCommand = new Command5("build").description("Build SPFN project for pro
|
|
|
1905
1947
|
init_logger();
|
|
1906
1948
|
init_package_manager();
|
|
1907
1949
|
import { Command as Command6 } from "commander";
|
|
1908
|
-
import { existsSync as existsSync13, readFileSync as
|
|
1950
|
+
import { existsSync as existsSync13, readFileSync as readFileSync5 } from "fs";
|
|
1909
1951
|
import { join as join13 } from "path";
|
|
1910
1952
|
import { execa as execa6 } from "execa";
|
|
1911
1953
|
import chalk6 from "chalk";
|
|
@@ -1917,7 +1959,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
1917
1959
|
const packageJsonPath = join13(cwd, "package.json");
|
|
1918
1960
|
let hasNext = false;
|
|
1919
1961
|
if (existsSync13(packageJsonPath)) {
|
|
1920
|
-
const packageJson = JSON.parse(
|
|
1962
|
+
const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
|
|
1921
1963
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
1922
1964
|
}
|
|
1923
1965
|
const builtServerDir = join13(cwd, ".spfn", "server");
|
|
@@ -1969,7 +2011,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
1969
2011
|
return;
|
|
1970
2012
|
}
|
|
1971
2013
|
const nextCmd = "next start -H 0.0.0.0 -p 3790";
|
|
1972
|
-
const serverCmd = `node ${serverEntry}`;
|
|
2014
|
+
const serverCmd = `node "${serverEntry}"`;
|
|
1973
2015
|
console.log(chalk6.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
|
|
1974
2016
|
logger.info("Next.js: http://0.0.0.0:3790");
|
|
1975
2017
|
logger.info(`SPFN API: http://${options.host}:${options.port}
|
|
@@ -1977,11 +2019,10 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
1977
2019
|
try {
|
|
1978
2020
|
await execa6(
|
|
1979
2021
|
pm === "npm" ? "npx" : pm,
|
|
1980
|
-
pm === "npm" ? ["concurrently", "--raw", "--kill-others",
|
|
2022
|
+
pm === "npm" ? ["concurrently", "--raw", "--kill-others", nextCmd, serverCmd] : ["exec", "concurrently", "--raw", "--kill-others", nextCmd, serverCmd],
|
|
1981
2023
|
{
|
|
1982
2024
|
stdio: "inherit",
|
|
1983
2025
|
cwd,
|
|
1984
|
-
shell: true,
|
|
1985
2026
|
env: { ...process.env }
|
|
1986
2027
|
}
|
|
1987
2028
|
);
|
|
@@ -1998,7 +2039,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
1998
2039
|
// src/commands/codegen.ts
|
|
1999
2040
|
init_logger();
|
|
2000
2041
|
import { Command as Command7 } from "commander";
|
|
2001
|
-
import { existsSync as existsSync14, writeFileSync as
|
|
2042
|
+
import { existsSync as existsSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
2002
2043
|
import { join as join14 } from "path";
|
|
2003
2044
|
import chalk7 from "chalk";
|
|
2004
2045
|
async function initCodegen(options) {
|
|
@@ -2009,20 +2050,29 @@ async function initCodegen(options) {
|
|
|
2009
2050
|
logger.info("Edit manually to add custom generators");
|
|
2010
2051
|
process.exit(0);
|
|
2011
2052
|
}
|
|
2012
|
-
const
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2053
|
+
const content = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* SPFN Codegen Configuration
|
|
2057
|
+
*
|
|
2058
|
+
* Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
|
|
2059
|
+
*/
|
|
2060
|
+
|
|
2061
|
+
export default defineConfig({
|
|
2062
|
+
generators: [
|
|
2063
|
+
// Route map generator - generates routeName \u2192 {method, path} mappings
|
|
2064
|
+
// Used by RPC proxy to resolve routes without importing server code
|
|
2065
|
+
defineGenerator({
|
|
2066
|
+
name: '@spfn/core:route-map',
|
|
2067
|
+
routerPath: './src/server/router.ts',
|
|
2068
|
+
outputPath: './src/generated/route-map.ts',
|
|
2069
|
+
}),
|
|
2070
|
+
],
|
|
2071
|
+
});
|
|
2072
|
+
`;
|
|
2073
|
+
writeFileSync9(rcPath, content);
|
|
2023
2074
|
console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2024
|
-
console.log("
|
|
2025
|
-
console.log(chalk7.gray(JSON.stringify(config, null, 2)));
|
|
2075
|
+
console.log(chalk7.gray("Configured the @spfn/core:route-map generator."));
|
|
2026
2076
|
if (options.withExample) {
|
|
2027
2077
|
console.log("\n" + chalk7.yellow("\u{1F4DD} To add custom generators:"));
|
|
2028
2078
|
console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
|
|
@@ -2234,7 +2284,7 @@ import { Command as Command9 } from "commander";
|
|
|
2234
2284
|
import chalk10 from "chalk";
|
|
2235
2285
|
|
|
2236
2286
|
// src/commands/db/utils/drizzle.ts
|
|
2237
|
-
import { existsSync as existsSync15, writeFileSync as
|
|
2287
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
|
|
2238
2288
|
import { spawn } from "child_process";
|
|
2239
2289
|
import { pathToFileURL } from "url";
|
|
2240
2290
|
import chalk9 from "chalk";
|
|
@@ -2269,7 +2319,7 @@ async function runDrizzleCommand(command) {
|
|
|
2269
2319
|
expandGlobs: true,
|
|
2270
2320
|
autoDetectSchemas: true
|
|
2271
2321
|
});
|
|
2272
|
-
|
|
2322
|
+
writeFileSync10(tempConfigPath, configContent);
|
|
2273
2323
|
console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
|
|
2274
2324
|
}
|
|
2275
2325
|
const args = command.split(" ");
|
|
@@ -2590,7 +2640,7 @@ async function applyFunctionMigrations() {
|
|
|
2590
2640
|
}
|
|
2591
2641
|
|
|
2592
2642
|
// src/commands/db/migrate.ts
|
|
2593
|
-
import
|
|
2643
|
+
import chalk18 from "chalk";
|
|
2594
2644
|
import { join as join16 } from "path";
|
|
2595
2645
|
import { existsSync as existsSync18 } from "fs";
|
|
2596
2646
|
|
|
@@ -2598,11 +2648,13 @@ import { existsSync as existsSync18 } from "fs";
|
|
|
2598
2648
|
import { promises as fs3 } from "fs";
|
|
2599
2649
|
import path3 from "path";
|
|
2600
2650
|
import { spawn as spawn2 } from "child_process";
|
|
2601
|
-
import
|
|
2651
|
+
import chalk17 from "chalk";
|
|
2602
2652
|
import ora7 from "ora";
|
|
2603
2653
|
|
|
2604
2654
|
// src/commands/db/utils/database.ts
|
|
2605
2655
|
import net from "net";
|
|
2656
|
+
import chalk14 from "chalk";
|
|
2657
|
+
import prompts4 from "prompts";
|
|
2606
2658
|
function parseDatabaseUrl(dbUrl) {
|
|
2607
2659
|
try {
|
|
2608
2660
|
const url = new URL(dbUrl);
|
|
@@ -2618,6 +2670,24 @@ function parseDatabaseUrl(dbUrl) {
|
|
|
2618
2670
|
throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2619
2671
|
}
|
|
2620
2672
|
}
|
|
2673
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
2674
|
+
function isRemoteHost(host) {
|
|
2675
|
+
return !LOCAL_HOSTS.has(host);
|
|
2676
|
+
}
|
|
2677
|
+
async function confirmDangerousTarget(dbInfo) {
|
|
2678
|
+
if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
const { typed } = await prompts4({
|
|
2682
|
+
type: "text",
|
|
2683
|
+
name: "typed",
|
|
2684
|
+
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
2685
|
+
});
|
|
2686
|
+
if (typed !== dbInfo.database) {
|
|
2687
|
+
console.log(chalk14.gray("Cancelled (database name mismatch)."));
|
|
2688
|
+
process.exit(0);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2621
2691
|
async function isPortAvailable(port) {
|
|
2622
2692
|
return new Promise((resolve3) => {
|
|
2623
2693
|
const server = net.createServer();
|
|
@@ -2667,14 +2737,14 @@ function formatTimestamp() {
|
|
|
2667
2737
|
import { promises as fs2 } from "fs";
|
|
2668
2738
|
import { existsSync as existsSync17 } from "fs";
|
|
2669
2739
|
import path2 from "path";
|
|
2670
|
-
import
|
|
2740
|
+
import chalk16 from "chalk";
|
|
2671
2741
|
|
|
2672
2742
|
// src/commands/db/utils/metadata.ts
|
|
2673
2743
|
import { promises as fs } from "fs";
|
|
2674
2744
|
import path from "path";
|
|
2675
2745
|
import { promisify } from "util";
|
|
2676
2746
|
import { exec } from "child_process";
|
|
2677
|
-
import
|
|
2747
|
+
import chalk15 from "chalk";
|
|
2678
2748
|
var execAsync = promisify(exec);
|
|
2679
2749
|
async function collectGitInfo() {
|
|
2680
2750
|
try {
|
|
@@ -2737,7 +2807,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
2737
2807
|
await pool.end();
|
|
2738
2808
|
}
|
|
2739
2809
|
} catch (error) {
|
|
2740
|
-
console.log(
|
|
2810
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
2741
2811
|
return void 0;
|
|
2742
2812
|
}
|
|
2743
2813
|
}
|
|
@@ -2745,9 +2815,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
2745
2815
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
2746
2816
|
try {
|
|
2747
2817
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
2748
|
-
console.log(
|
|
2818
|
+
console.log(chalk15.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
2749
2819
|
} catch (error) {
|
|
2750
|
-
console.log(
|
|
2820
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not save metadata"));
|
|
2751
2821
|
}
|
|
2752
2822
|
}
|
|
2753
2823
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -2776,10 +2846,10 @@ async function ensureBackupInGitignore() {
|
|
|
2776
2846
|
if (!hasBackupsIgnore) {
|
|
2777
2847
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
2778
2848
|
await fs2.appendFile(gitignorePath, entry);
|
|
2779
|
-
console.log(
|
|
2849
|
+
console.log(chalk16.dim("\u2713 Added backups/ to .gitignore"));
|
|
2780
2850
|
}
|
|
2781
2851
|
} catch (error) {
|
|
2782
|
-
console.log(
|
|
2852
|
+
console.log(chalk16.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
2783
2853
|
}
|
|
2784
2854
|
}
|
|
2785
2855
|
async function ensureBackupDir() {
|
|
@@ -2829,12 +2899,12 @@ async function listBackupFiles() {
|
|
|
2829
2899
|
import { env as env3 } from "@spfn/core/config";
|
|
2830
2900
|
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
2831
2901
|
async function dbBackup(options) {
|
|
2832
|
-
console.log(
|
|
2902
|
+
console.log(chalk17.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2833
2903
|
loadEnv4();
|
|
2834
2904
|
const dbUrl = env3.DATABASE_URL;
|
|
2835
2905
|
if (!dbUrl) {
|
|
2836
|
-
console.error(
|
|
2837
|
-
console.log(
|
|
2906
|
+
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2907
|
+
console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2838
2908
|
process.exit(1);
|
|
2839
2909
|
}
|
|
2840
2910
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2844,7 +2914,7 @@ async function dbBackup(options) {
|
|
|
2844
2914
|
const ext = format === "sql" ? "sql" : "dump";
|
|
2845
2915
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
2846
2916
|
if (options.dataOnly && options.schemaOnly) {
|
|
2847
|
-
console.error(
|
|
2917
|
+
console.error(chalk17.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2848
2918
|
process.exit(1);
|
|
2849
2919
|
}
|
|
2850
2920
|
const args = [
|
|
@@ -2890,11 +2960,11 @@ async function dbBackup(options) {
|
|
|
2890
2960
|
const stats = await fs3.stat(filename);
|
|
2891
2961
|
const size = formatBytes(stats.size);
|
|
2892
2962
|
spinner.succeed("Backup created");
|
|
2893
|
-
console.log(
|
|
2963
|
+
console.log(chalk17.green(`
|
|
2894
2964
|
\u2705 Backup created successfully`));
|
|
2895
|
-
console.log(
|
|
2896
|
-
console.log(
|
|
2897
|
-
console.log(
|
|
2965
|
+
console.log(chalk17.gray(` File: ${filename}`));
|
|
2966
|
+
console.log(chalk17.gray(` Size: ${size}`));
|
|
2967
|
+
console.log(chalk17.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
2898
2968
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
2899
2969
|
collectGitInfo(),
|
|
2900
2970
|
collectMigrationInfo(dbUrl)
|
|
@@ -2934,11 +3004,11 @@ async function dbBackup(options) {
|
|
|
2934
3004
|
reject(error);
|
|
2935
3005
|
});
|
|
2936
3006
|
}).catch((error) => {
|
|
2937
|
-
console.error(
|
|
3007
|
+
console.error(chalk17.red("\n\u274C Failed to create backup"));
|
|
2938
3008
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
2939
|
-
console.error(
|
|
3009
|
+
console.error(chalk17.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
2940
3010
|
} else {
|
|
2941
|
-
console.error(
|
|
3011
|
+
console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2942
3012
|
}
|
|
2943
3013
|
process.exit(1);
|
|
2944
3014
|
});
|
|
@@ -2955,7 +3025,7 @@ async function dbMigrate(options = {}) {
|
|
|
2955
3025
|
process.exit(1);
|
|
2956
3026
|
}
|
|
2957
3027
|
if (options.withBackup) {
|
|
2958
|
-
console.log(
|
|
3028
|
+
console.log(chalk18.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
2959
3029
|
await dbBackup({
|
|
2960
3030
|
format: "custom",
|
|
2961
3031
|
tag: "pre-migration",
|
|
@@ -2968,57 +3038,56 @@ async function dbMigrate(options = {}) {
|
|
|
2968
3038
|
const postgres = await import("postgres");
|
|
2969
3039
|
loadEnv5();
|
|
2970
3040
|
if (!env4.DATABASE_URL) {
|
|
2971
|
-
console.error(
|
|
3041
|
+
console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
|
|
2972
3042
|
process.exit(1);
|
|
2973
3043
|
}
|
|
2974
3044
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2975
3045
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2976
3046
|
if (functions.length > 0) {
|
|
2977
|
-
console.log(
|
|
3047
|
+
console.log(chalk18.blue("\u{1F4E6} Applying function package migrations:"));
|
|
2978
3048
|
functions.forEach((func) => {
|
|
2979
|
-
console.log(
|
|
3049
|
+
console.log(chalk18.dim(` - ${func.packageName}`));
|
|
2980
3050
|
});
|
|
2981
3051
|
await executeFunctionMigrations2(functions);
|
|
2982
|
-
console.log(
|
|
3052
|
+
console.log(chalk18.green("\u2705 Function migrations applied\n"));
|
|
2983
3053
|
}
|
|
2984
3054
|
const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
|
|
2985
3055
|
if (existsSync18(projectMigrationsDir)) {
|
|
2986
3056
|
const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2987
3057
|
const projDb = drizzle(projConn);
|
|
2988
3058
|
try {
|
|
2989
|
-
console.log(
|
|
3059
|
+
console.log(chalk18.blue("\u{1F4E6} Running project migrations..."));
|
|
2990
3060
|
await migrate(projDb, {
|
|
2991
3061
|
migrationsFolder: projectMigrationsDir,
|
|
2992
3062
|
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
2993
3063
|
});
|
|
2994
|
-
console.log(
|
|
3064
|
+
console.log(chalk18.green("\u2705 Project migrations applied successfully"));
|
|
2995
3065
|
} finally {
|
|
2996
3066
|
await projConn.end();
|
|
2997
3067
|
}
|
|
2998
3068
|
} else {
|
|
2999
|
-
console.log(
|
|
3069
|
+
console.log(chalk18.dim("No project migrations found (src/server/drizzle)"));
|
|
3000
3070
|
}
|
|
3001
3071
|
}
|
|
3002
3072
|
|
|
3003
3073
|
// src/commands/db/studio.ts
|
|
3004
|
-
import
|
|
3005
|
-
import { existsSync as existsSync19, writeFileSync as
|
|
3074
|
+
import chalk19 from "chalk";
|
|
3075
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3 } from "fs";
|
|
3006
3076
|
import { spawn as spawn3 } from "child_process";
|
|
3007
3077
|
import { env as env5 } from "@spfn/core/config";
|
|
3008
|
-
import "@spfn/core/config";
|
|
3009
3078
|
async function dbStudio(requestedPort) {
|
|
3010
|
-
console.log(
|
|
3079
|
+
console.log(chalk19.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
3011
3080
|
const defaultPort = 4983;
|
|
3012
3081
|
const startPort = requestedPort || defaultPort;
|
|
3013
3082
|
let port;
|
|
3014
3083
|
try {
|
|
3015
3084
|
port = await findAvailablePort(startPort);
|
|
3016
3085
|
if (port !== startPort) {
|
|
3017
|
-
console.log(
|
|
3086
|
+
console.log(chalk19.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
3018
3087
|
`));
|
|
3019
3088
|
}
|
|
3020
3089
|
} catch (error) {
|
|
3021
|
-
console.error(
|
|
3090
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
3022
3091
|
process.exit(1);
|
|
3023
3092
|
}
|
|
3024
3093
|
const hasUserConfig = existsSync19("./drizzle.config.ts");
|
|
@@ -3027,8 +3096,8 @@ async function dbStudio(requestedPort) {
|
|
|
3027
3096
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3028
3097
|
if (!hasUserConfig) {
|
|
3029
3098
|
if (!env5.DATABASE_URL) {
|
|
3030
|
-
console.error(
|
|
3031
|
-
console.log(
|
|
3099
|
+
console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
|
|
3100
|
+
console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3032
3101
|
process.exit(1);
|
|
3033
3102
|
}
|
|
3034
3103
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -3038,8 +3107,8 @@ async function dbStudio(requestedPort) {
|
|
|
3038
3107
|
expandGlobs: true
|
|
3039
3108
|
// Expand glob patterns for Studio compatibility
|
|
3040
3109
|
});
|
|
3041
|
-
|
|
3042
|
-
console.log(
|
|
3110
|
+
writeFileSync11(tempConfigPath, configContent);
|
|
3111
|
+
console.log(chalk19.dim("Using auto-generated Drizzle config\n"));
|
|
3043
3112
|
}
|
|
3044
3113
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
3045
3114
|
stdio: "inherit",
|
|
@@ -3054,19 +3123,19 @@ async function dbStudio(requestedPort) {
|
|
|
3054
3123
|
studioProcess.on("exit", (code) => {
|
|
3055
3124
|
cleanup();
|
|
3056
3125
|
if (code !== 0 && code !== null) {
|
|
3057
|
-
console.error(
|
|
3126
|
+
console.error(chalk19.red(`
|
|
3058
3127
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
3059
3128
|
process.exit(code);
|
|
3060
3129
|
}
|
|
3061
3130
|
});
|
|
3062
3131
|
studioProcess.on("error", (error) => {
|
|
3063
3132
|
cleanup();
|
|
3064
|
-
console.error(
|
|
3065
|
-
console.error(
|
|
3133
|
+
console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
|
|
3134
|
+
console.error(chalk19.red(error.message));
|
|
3066
3135
|
process.exit(1);
|
|
3067
3136
|
});
|
|
3068
3137
|
process.on("SIGINT", () => {
|
|
3069
|
-
console.log(
|
|
3138
|
+
console.log(chalk19.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
3070
3139
|
studioProcess.kill("SIGTERM");
|
|
3071
3140
|
cleanup();
|
|
3072
3141
|
process.exit(0);
|
|
@@ -3080,27 +3149,39 @@ async function dbStudio(requestedPort) {
|
|
|
3080
3149
|
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3081
3150
|
unlinkSync3(tempConfigPath);
|
|
3082
3151
|
}
|
|
3083
|
-
console.error(
|
|
3084
|
-
console.error(
|
|
3152
|
+
console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
|
|
3153
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3085
3154
|
process.exit(1);
|
|
3086
3155
|
}
|
|
3087
3156
|
}
|
|
3088
3157
|
|
|
3089
3158
|
// src/commands/db/drop.ts
|
|
3090
|
-
import
|
|
3091
|
-
import
|
|
3159
|
+
import chalk20 from "chalk";
|
|
3160
|
+
import prompts5 from "prompts";
|
|
3161
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3162
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
3092
3163
|
async function dbDrop() {
|
|
3093
|
-
|
|
3094
|
-
|
|
3164
|
+
loadEnv6();
|
|
3165
|
+
if (!env6.DATABASE_URL) {
|
|
3166
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
3167
|
+
console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3168
|
+
process.exit(1);
|
|
3169
|
+
}
|
|
3170
|
+
const dbInfo = parseDatabaseUrl(env6.DATABASE_URL);
|
|
3171
|
+
console.log(chalk20.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3172
|
+
console.log(chalk20.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
3173
|
+
`));
|
|
3174
|
+
const { confirm } = await prompts5({
|
|
3095
3175
|
type: "confirm",
|
|
3096
3176
|
name: "confirm",
|
|
3097
3177
|
message: "Are you sure you want to drop all tables?",
|
|
3098
3178
|
initial: false
|
|
3099
3179
|
});
|
|
3100
3180
|
if (!confirm) {
|
|
3101
|
-
console.log(
|
|
3181
|
+
console.log(chalk20.gray("Cancelled."));
|
|
3102
3182
|
process.exit(0);
|
|
3103
3183
|
}
|
|
3184
|
+
await confirmDangerousTarget(dbInfo);
|
|
3104
3185
|
await runWithSpinner(
|
|
3105
3186
|
"Dropping all tables...",
|
|
3106
3187
|
"drop",
|
|
@@ -3110,7 +3191,7 @@ async function dbDrop() {
|
|
|
3110
3191
|
}
|
|
3111
3192
|
|
|
3112
3193
|
// src/commands/db/check.ts
|
|
3113
|
-
import
|
|
3194
|
+
import chalk21 from "chalk";
|
|
3114
3195
|
import ora8 from "ora";
|
|
3115
3196
|
async function dbCheck() {
|
|
3116
3197
|
const spinner = ora8("Checking database connection...").start();
|
|
@@ -3119,7 +3200,7 @@ async function dbCheck() {
|
|
|
3119
3200
|
spinner.succeed("Database connection OK");
|
|
3120
3201
|
} catch (error) {
|
|
3121
3202
|
spinner.fail("Database connection failed");
|
|
3122
|
-
console.error(
|
|
3203
|
+
console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3123
3204
|
process.exit(1);
|
|
3124
3205
|
}
|
|
3125
3206
|
}
|
|
@@ -3127,28 +3208,28 @@ async function dbCheck() {
|
|
|
3127
3208
|
// src/commands/db/restore.ts
|
|
3128
3209
|
import path4 from "path";
|
|
3129
3210
|
import { spawn as spawn4 } from "child_process";
|
|
3130
|
-
import
|
|
3211
|
+
import chalk22 from "chalk";
|
|
3131
3212
|
import ora9 from "ora";
|
|
3132
|
-
import
|
|
3133
|
-
import { env as
|
|
3134
|
-
import { loadEnv as
|
|
3213
|
+
import prompts6 from "prompts";
|
|
3214
|
+
import { env as env7 } from "@spfn/core/config";
|
|
3215
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3135
3216
|
async function dbRestore(backupFile, options = {}) {
|
|
3136
|
-
console.log(
|
|
3137
|
-
|
|
3138
|
-
const dbUrl =
|
|
3217
|
+
console.log(chalk22.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
3218
|
+
loadEnv7();
|
|
3219
|
+
const dbUrl = env7.DATABASE_URL;
|
|
3139
3220
|
if (!dbUrl) {
|
|
3140
|
-
console.error(
|
|
3141
|
-
console.log(
|
|
3221
|
+
console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
|
|
3222
|
+
console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3142
3223
|
process.exit(1);
|
|
3143
3224
|
}
|
|
3144
3225
|
let file = backupFile;
|
|
3145
3226
|
if (!file) {
|
|
3146
3227
|
const backups = await listBackupFiles();
|
|
3147
3228
|
if (backups.length === 0) {
|
|
3148
|
-
console.log(
|
|
3229
|
+
console.log(chalk22.yellow("No backups found in ./backups directory"));
|
|
3149
3230
|
process.exit(0);
|
|
3150
3231
|
}
|
|
3151
|
-
const { selected } = await
|
|
3232
|
+
const { selected } = await prompts6({
|
|
3152
3233
|
type: "select",
|
|
3153
3234
|
name: "selected",
|
|
3154
3235
|
message: "Select backup to restore:",
|
|
@@ -3158,31 +3239,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3158
3239
|
}))
|
|
3159
3240
|
});
|
|
3160
3241
|
if (!selected) {
|
|
3161
|
-
console.log(
|
|
3242
|
+
console.log(chalk22.gray("Cancelled"));
|
|
3162
3243
|
process.exit(0);
|
|
3163
3244
|
}
|
|
3164
3245
|
file = selected;
|
|
3165
3246
|
}
|
|
3166
3247
|
if (!file) {
|
|
3167
|
-
console.error(
|
|
3248
|
+
console.error(chalk22.red("\u274C No backup file selected"));
|
|
3168
3249
|
process.exit(1);
|
|
3169
3250
|
}
|
|
3170
3251
|
const metadata = await loadBackupMetadata(file);
|
|
3171
3252
|
if (metadata) {
|
|
3172
|
-
console.log(
|
|
3173
|
-
console.log(
|
|
3174
|
-
console.log(
|
|
3253
|
+
console.log(chalk22.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
3254
|
+
console.log(chalk22.dim(` Database: ${metadata.database}`));
|
|
3255
|
+
console.log(chalk22.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
3175
3256
|
if (metadata.environment) {
|
|
3176
|
-
console.log(
|
|
3257
|
+
console.log(chalk22.dim(` Environment: ${metadata.environment}`));
|
|
3177
3258
|
}
|
|
3178
3259
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3179
|
-
console.log(
|
|
3260
|
+
console.log(chalk22.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
3180
3261
|
}
|
|
3181
3262
|
if (metadata.backup.dataOnly) {
|
|
3182
|
-
console.log(
|
|
3263
|
+
console.log(chalk22.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
3183
3264
|
}
|
|
3184
3265
|
if (metadata.backup.schemaOnly) {
|
|
3185
|
-
console.log(
|
|
3266
|
+
console.log(chalk22.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
3186
3267
|
}
|
|
3187
3268
|
const warnings2 = [];
|
|
3188
3269
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
@@ -3205,26 +3286,27 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3205
3286
|
}
|
|
3206
3287
|
}
|
|
3207
3288
|
if (warnings2.length > 0) {
|
|
3208
|
-
console.log(
|
|
3209
|
-
warnings2.forEach((warning) => console.log(
|
|
3289
|
+
console.log(chalk22.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
3290
|
+
warnings2.forEach((warning) => console.log(chalk22.yellow(` - ${warning}`)));
|
|
3210
3291
|
console.log("");
|
|
3211
3292
|
}
|
|
3212
3293
|
}
|
|
3213
|
-
const
|
|
3294
|
+
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
3295
|
+
const { confirm } = await prompts6({
|
|
3214
3296
|
type: "confirm",
|
|
3215
3297
|
name: "confirm",
|
|
3216
|
-
message:
|
|
3298
|
+
message: chalk22.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
3217
3299
|
initial: false
|
|
3218
3300
|
});
|
|
3219
3301
|
if (!confirm) {
|
|
3220
|
-
console.log(
|
|
3302
|
+
console.log(chalk22.gray("Cancelled"));
|
|
3221
3303
|
process.exit(0);
|
|
3222
3304
|
}
|
|
3305
|
+
await confirmDangerousTarget(dbInfo);
|
|
3223
3306
|
if (options.dataOnly && options.schemaOnly) {
|
|
3224
|
-
console.error(
|
|
3307
|
+
console.error(chalk22.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3225
3308
|
process.exit(1);
|
|
3226
3309
|
}
|
|
3227
|
-
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
3228
3310
|
const ext = path4.extname(file);
|
|
3229
3311
|
const isCustomFormat = ext === ".dump";
|
|
3230
3312
|
const command = isCustomFormat ? "pg_restore" : "psql";
|
|
@@ -3236,7 +3318,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3236
3318
|
args.push("-d", dbInfo.database);
|
|
3237
3319
|
args.push("--verbose");
|
|
3238
3320
|
if (options.drop) {
|
|
3239
|
-
args.push("--clean");
|
|
3321
|
+
args.push("--clean", "--if-exists");
|
|
3240
3322
|
}
|
|
3241
3323
|
if (options.schema) {
|
|
3242
3324
|
args.push("-n", options.schema);
|
|
@@ -3249,9 +3331,12 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3249
3331
|
}
|
|
3250
3332
|
args.push(file);
|
|
3251
3333
|
} else {
|
|
3334
|
+
if (options.drop) {
|
|
3335
|
+
console.log(chalk22.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
|
|
3336
|
+
}
|
|
3252
3337
|
if (options.dataOnly || options.schemaOnly) {
|
|
3253
|
-
console.log(
|
|
3254
|
-
console.log(
|
|
3338
|
+
console.log(chalk22.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
3339
|
+
console.log(chalk22.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
3255
3340
|
}
|
|
3256
3341
|
args.push("-h", dbInfo.host);
|
|
3257
3342
|
args.push("-p", dbInfo.port);
|
|
@@ -3272,7 +3357,6 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3272
3357
|
const warnings = [];
|
|
3273
3358
|
const errors = [];
|
|
3274
3359
|
let objectCount = 0;
|
|
3275
|
-
let lastObject = "";
|
|
3276
3360
|
restoreProcess.stderr?.on("data", (data) => {
|
|
3277
3361
|
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
3278
3362
|
for (const line of lines) {
|
|
@@ -3286,18 +3370,16 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3286
3370
|
objectCount = Number(objectMatch[2]);
|
|
3287
3371
|
const current = Number(objectMatch[1]);
|
|
3288
3372
|
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3289
|
-
lastObject = desc;
|
|
3290
3373
|
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
3291
3374
|
} else if (isCustomFormat) {
|
|
3292
3375
|
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3293
3376
|
if (desc && !/warning:|error:/i.test(desc)) {
|
|
3294
|
-
lastObject = desc;
|
|
3295
3377
|
spinner.text = `Restoring backup... ${desc}`;
|
|
3296
3378
|
}
|
|
3297
3379
|
}
|
|
3298
3380
|
if (verbose) {
|
|
3299
3381
|
spinner.stop();
|
|
3300
|
-
console.log(
|
|
3382
|
+
console.log(chalk22.dim(` ${line.trim()}`));
|
|
3301
3383
|
spinner.start();
|
|
3302
3384
|
}
|
|
3303
3385
|
}
|
|
@@ -3305,7 +3387,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3305
3387
|
restoreProcess.stdout?.on("data", (data) => {
|
|
3306
3388
|
if (verbose) {
|
|
3307
3389
|
spinner.stop();
|
|
3308
|
-
console.log(
|
|
3390
|
+
console.log(chalk22.dim(` ${data.toString().trim()}`));
|
|
3309
3391
|
spinner.start();
|
|
3310
3392
|
}
|
|
3311
3393
|
});
|
|
@@ -3315,31 +3397,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3315
3397
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
3316
3398
|
spinner.succeed(`Restore completed${summary}`);
|
|
3317
3399
|
if (warnings.length > 0) {
|
|
3318
|
-
console.log(
|
|
3400
|
+
console.log(chalk22.yellow(`
|
|
3319
3401
|
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
3320
3402
|
`));
|
|
3321
3403
|
for (const w of warnings) {
|
|
3322
|
-
console.log(
|
|
3404
|
+
console.log(chalk22.yellow(` - ${w}`));
|
|
3323
3405
|
}
|
|
3324
3406
|
}
|
|
3325
|
-
console.log(
|
|
3407
|
+
console.log(chalk22.green("\n\u2705 Database restored successfully"));
|
|
3326
3408
|
resolve3();
|
|
3327
3409
|
} else {
|
|
3328
3410
|
spinner.fail("Restore failed");
|
|
3329
3411
|
if (errors.length > 0) {
|
|
3330
|
-
console.error(
|
|
3412
|
+
console.error(chalk22.red(`
|
|
3331
3413
|
\u274C Errors (${errors.length}):
|
|
3332
3414
|
`));
|
|
3333
3415
|
for (const e of errors) {
|
|
3334
|
-
console.error(
|
|
3416
|
+
console.error(chalk22.red(` - ${e}`));
|
|
3335
3417
|
}
|
|
3336
3418
|
}
|
|
3337
3419
|
if (warnings.length > 0) {
|
|
3338
|
-
console.log(
|
|
3420
|
+
console.log(chalk22.yellow(`
|
|
3339
3421
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
3340
3422
|
`));
|
|
3341
3423
|
for (const w of warnings) {
|
|
3342
|
-
console.log(
|
|
3424
|
+
console.log(chalk22.yellow(` - ${w}`));
|
|
3343
3425
|
}
|
|
3344
3426
|
}
|
|
3345
3427
|
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
@@ -3353,7 +3435,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3353
3435
|
}).catch((error) => {
|
|
3354
3436
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3355
3437
|
if (msg) {
|
|
3356
|
-
console.error(
|
|
3438
|
+
console.error(chalk22.red(`
|
|
3357
3439
|
\u274C ${msg}`));
|
|
3358
3440
|
}
|
|
3359
3441
|
process.exit(1);
|
|
@@ -3361,17 +3443,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3361
3443
|
}
|
|
3362
3444
|
|
|
3363
3445
|
// src/commands/db/list.ts
|
|
3364
|
-
import
|
|
3446
|
+
import chalk23 from "chalk";
|
|
3365
3447
|
async function dbBackupList() {
|
|
3366
|
-
console.log(
|
|
3448
|
+
console.log(chalk23.blue("\u{1F4CB} Database backups:\n"));
|
|
3367
3449
|
const backups = await listBackupFiles();
|
|
3368
3450
|
if (backups.length === 0) {
|
|
3369
|
-
console.log(
|
|
3370
|
-
console.log(
|
|
3451
|
+
console.log(chalk23.yellow("No backups found in ./backups directory"));
|
|
3452
|
+
console.log(chalk23.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
3371
3453
|
return;
|
|
3372
3454
|
}
|
|
3373
|
-
console.log(
|
|
3374
|
-
console.log(
|
|
3455
|
+
console.log(chalk23.bold(" Date Size File"));
|
|
3456
|
+
console.log(chalk23.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3375
3457
|
backups.forEach((backup) => {
|
|
3376
3458
|
const date = backup.date.toLocaleString("en-US", {
|
|
3377
3459
|
year: "numeric",
|
|
@@ -3382,23 +3464,23 @@ async function dbBackupList() {
|
|
|
3382
3464
|
second: "2-digit"
|
|
3383
3465
|
});
|
|
3384
3466
|
const sizeStr = backup.size.padEnd(10);
|
|
3385
|
-
console.log(
|
|
3467
|
+
console.log(chalk23.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
3386
3468
|
});
|
|
3387
|
-
console.log(
|
|
3469
|
+
console.log(chalk23.gray(`
|
|
3388
3470
|
Total: ${backups.length} backup(s)
|
|
3389
3471
|
`));
|
|
3390
3472
|
}
|
|
3391
3473
|
|
|
3392
3474
|
// src/commands/db/clean.ts
|
|
3393
3475
|
import { promises as fs4 } from "fs";
|
|
3394
|
-
import
|
|
3476
|
+
import chalk24 from "chalk";
|
|
3395
3477
|
import ora10 from "ora";
|
|
3396
|
-
import
|
|
3478
|
+
import prompts7 from "prompts";
|
|
3397
3479
|
async function dbBackupClean(options) {
|
|
3398
|
-
console.log(
|
|
3480
|
+
console.log(chalk24.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
3399
3481
|
const backups = await listBackupFiles();
|
|
3400
3482
|
if (backups.length === 0) {
|
|
3401
|
-
console.log(
|
|
3483
|
+
console.log(chalk24.yellow("No backups found"));
|
|
3402
3484
|
return;
|
|
3403
3485
|
}
|
|
3404
3486
|
let toDelete = [];
|
|
@@ -3415,42 +3497,46 @@ async function dbBackupClean(options) {
|
|
|
3415
3497
|
toDelete = backups.slice(defaultKeep);
|
|
3416
3498
|
}
|
|
3417
3499
|
if (toDelete.length === 0) {
|
|
3418
|
-
console.log(
|
|
3500
|
+
console.log(chalk24.green("\u2705 No backups to clean"));
|
|
3419
3501
|
return;
|
|
3420
3502
|
}
|
|
3421
|
-
console.log(
|
|
3503
|
+
console.log(chalk24.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
3422
3504
|
`));
|
|
3423
3505
|
toDelete.forEach((backup) => {
|
|
3424
|
-
console.log(
|
|
3506
|
+
console.log(chalk24.gray(` - ${backup.name} (${backup.size})`));
|
|
3425
3507
|
});
|
|
3426
|
-
const { confirm } = await
|
|
3508
|
+
const { confirm } = await prompts7({
|
|
3427
3509
|
type: "confirm",
|
|
3428
3510
|
name: "confirm",
|
|
3429
3511
|
message: "\nProceed with deletion?",
|
|
3430
3512
|
initial: false
|
|
3431
3513
|
});
|
|
3432
3514
|
if (!confirm) {
|
|
3433
|
-
console.log(
|
|
3515
|
+
console.log(chalk24.gray("Cancelled"));
|
|
3434
3516
|
return;
|
|
3435
3517
|
}
|
|
3436
3518
|
const spinner = ora10("Deleting backups...").start();
|
|
3437
3519
|
try {
|
|
3438
|
-
await Promise.all(toDelete.
|
|
3520
|
+
await Promise.all(toDelete.flatMap((backup) => [
|
|
3521
|
+
fs4.unlink(backup.path),
|
|
3522
|
+
// Remove the .meta.json sidecar too (force: ok if it doesn't exist)
|
|
3523
|
+
fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
|
|
3524
|
+
]));
|
|
3439
3525
|
spinner.succeed("Backups deleted");
|
|
3440
|
-
console.log(
|
|
3526
|
+
console.log(chalk24.green(`
|
|
3441
3527
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
3442
3528
|
} catch (error) {
|
|
3443
3529
|
spinner.fail("Failed to delete backups");
|
|
3444
|
-
console.error(
|
|
3530
|
+
console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3445
3531
|
process.exit(1);
|
|
3446
3532
|
}
|
|
3447
3533
|
}
|
|
3448
3534
|
|
|
3449
3535
|
// src/commands/db/reindex.ts
|
|
3450
|
-
import { existsSync as existsSync20, readFileSync as
|
|
3536
|
+
import { existsSync as existsSync20, readFileSync as readFileSync7, writeFileSync as writeFileSync12, renameSync, copyFileSync } from "fs";
|
|
3451
3537
|
import { join as join17 } from "path";
|
|
3452
|
-
import
|
|
3453
|
-
import { loadEnv as
|
|
3538
|
+
import chalk25 from "chalk";
|
|
3539
|
+
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
3454
3540
|
function isTimestampPrefix(tag) {
|
|
3455
3541
|
const prefix = tag.split("_")[0];
|
|
3456
3542
|
return /^\d{5,}$/.test(prefix);
|
|
@@ -3466,19 +3552,19 @@ function parseTag(tag) {
|
|
|
3466
3552
|
};
|
|
3467
3553
|
}
|
|
3468
3554
|
async function dbReindex(options = {}) {
|
|
3469
|
-
|
|
3555
|
+
loadEnv8();
|
|
3470
3556
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3471
3557
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
3472
3558
|
const outDir = config.out;
|
|
3473
3559
|
const journalPath = join17(outDir, "meta", "_journal.json");
|
|
3474
3560
|
if (!existsSync20(journalPath)) {
|
|
3475
|
-
console.error(
|
|
3476
|
-
console.log(
|
|
3561
|
+
console.error(chalk25.red("\u274C No _journal.json found at:"), journalPath);
|
|
3562
|
+
console.log(chalk25.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
3477
3563
|
process.exit(1);
|
|
3478
3564
|
}
|
|
3479
|
-
const journal = JSON.parse(
|
|
3565
|
+
const journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
|
|
3480
3566
|
if (journal.entries.length === 0) {
|
|
3481
|
-
console.log(
|
|
3567
|
+
console.log(chalk25.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
3482
3568
|
return;
|
|
3483
3569
|
}
|
|
3484
3570
|
const renames = [];
|
|
@@ -3505,33 +3591,33 @@ async function dbReindex(options = {}) {
|
|
|
3505
3591
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
3506
3592
|
}
|
|
3507
3593
|
if (tagUpdates.length === 0) {
|
|
3508
|
-
console.log(
|
|
3594
|
+
console.log(chalk25.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
3509
3595
|
if (skipped > 0) {
|
|
3510
|
-
console.log(
|
|
3596
|
+
console.log(chalk25.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
3511
3597
|
}
|
|
3512
3598
|
return;
|
|
3513
3599
|
}
|
|
3514
|
-
console.log(
|
|
3600
|
+
console.log(chalk25.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
3515
3601
|
for (const update of tagUpdates) {
|
|
3516
3602
|
console.log(
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3603
|
+
chalk25.dim(` [${update.idx}]`),
|
|
3604
|
+
chalk25.red(update.oldTag),
|
|
3605
|
+
chalk25.dim("\u2192"),
|
|
3606
|
+
chalk25.green(update.newTag)
|
|
3521
3607
|
);
|
|
3522
3608
|
}
|
|
3523
|
-
console.log(
|
|
3609
|
+
console.log(chalk25.dim(`
|
|
3524
3610
|
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
3525
3611
|
if (skipped > 0) {
|
|
3526
|
-
console.log(
|
|
3612
|
+
console.log(chalk25.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
3527
3613
|
}
|
|
3528
3614
|
if (options.dryRun) {
|
|
3529
|
-
console.log(
|
|
3615
|
+
console.log(chalk25.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
3530
3616
|
return;
|
|
3531
3617
|
}
|
|
3532
3618
|
const backupPath = journalPath + ".bak";
|
|
3533
3619
|
copyFileSync(journalPath, backupPath);
|
|
3534
|
-
console.log(
|
|
3620
|
+
console.log(chalk25.dim(`
|
|
3535
3621
|
Backed up journal \u2192 ${backupPath}`));
|
|
3536
3622
|
for (const rename of renames) {
|
|
3537
3623
|
renameSync(rename.from, rename.to);
|
|
@@ -3542,8 +3628,8 @@ async function dbReindex(options = {}) {
|
|
|
3542
3628
|
entry.tag = update.newTag;
|
|
3543
3629
|
}
|
|
3544
3630
|
}
|
|
3545
|
-
|
|
3546
|
-
console.log(
|
|
3631
|
+
writeFileSync12(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
3632
|
+
console.log(chalk25.green(`
|
|
3547
3633
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
3548
3634
|
}
|
|
3549
3635
|
|
|
@@ -3562,1415 +3648,96 @@ dbCommand.command("backup:clean").description("Clean old database backups").opti
|
|
|
3562
3648
|
dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
|
|
3563
3649
|
|
|
3564
3650
|
// src/commands/add.ts
|
|
3651
|
+
init_package_manager();
|
|
3565
3652
|
import { Command as Command10 } from "commander";
|
|
3566
|
-
import { existsSync as existsSync21, readFileSync as
|
|
3653
|
+
import { existsSync as existsSync21, readFileSync as readFileSync8 } from "fs";
|
|
3567
3654
|
import { join as join18 } from "path";
|
|
3568
|
-
import {
|
|
3569
|
-
import
|
|
3570
|
-
import chalk25 from "chalk";
|
|
3655
|
+
import { execa as execa7 } from "execa";
|
|
3656
|
+
import chalk26 from "chalk";
|
|
3571
3657
|
import ora11 from "ora";
|
|
3572
|
-
var execAsync2 = promisify2(exec2);
|
|
3573
3658
|
async function addPackage(packageName) {
|
|
3574
3659
|
if (!packageName.includes("/")) {
|
|
3575
|
-
console.error(
|
|
3576
|
-
console.log(
|
|
3577
|
-
console.log(
|
|
3578
|
-
console.log(
|
|
3660
|
+
console.error(chalk26.red("\u274C Please specify full package name"));
|
|
3661
|
+
console.log(chalk26.yellow("\n\u{1F4A1} Examples:"));
|
|
3662
|
+
console.log(chalk26.gray(" pnpm spfn add @spfn/cms"));
|
|
3663
|
+
console.log(chalk26.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
3579
3664
|
process.exit(1);
|
|
3580
3665
|
}
|
|
3581
|
-
console.log(
|
|
3666
|
+
console.log(chalk26.blue(`
|
|
3582
3667
|
\u{1F4E6} Setting up ${packageName}...
|
|
3583
3668
|
`));
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
} else {
|
|
3597
|
-
console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
|
|
3598
|
-
}
|
|
3599
|
-
if (!existsSync21(pkgJsonPath)) {
|
|
3600
|
-
throw new Error(`Package ${packageName} not found after installation`);
|
|
3669
|
+
const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3670
|
+
const pkgJsonPath = join18(pkgPath, "package.json");
|
|
3671
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
3672
|
+
const pm = detectPackageManager(process.cwd());
|
|
3673
|
+
const installSpinner = ora11("Installing package...").start();
|
|
3674
|
+
try {
|
|
3675
|
+
await execa7(pm, ["add", packageName]);
|
|
3676
|
+
installSpinner.succeed("Package installed");
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
installSpinner.fail("Failed to install package");
|
|
3679
|
+
console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3680
|
+
process.exit(1);
|
|
3601
3681
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3682
|
+
} else {
|
|
3683
|
+
console.log(chalk26.gray("\u2713 Package already installed (using local version)\n"));
|
|
3684
|
+
}
|
|
3685
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
3686
|
+
console.error(chalk26.red(`\u274C Package ${packageName} not found after installation`));
|
|
3687
|
+
process.exit(1);
|
|
3688
|
+
}
|
|
3689
|
+
const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
|
|
3690
|
+
if (pkgJson.spfn?.migrations) {
|
|
3691
|
+
console.log(chalk26.blue(`
|
|
3605
3692
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
3606
3693
|
`));
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
}
|
|
3626
|
-
} else {
|
|
3627
|
-
console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
|
|
3694
|
+
const { env: env8 } = await import("@spfn/core/config");
|
|
3695
|
+
if (!env8.DATABASE_URL) {
|
|
3696
|
+
console.log(chalk26.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
3697
|
+
console.log(chalk26.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
3698
|
+
console.log(chalk26.gray(" pnpm spfn db push\n"));
|
|
3699
|
+
} else {
|
|
3700
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
3701
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
3702
|
+
const targetFunction = functions.find((f) => f.packageName === packageName);
|
|
3703
|
+
if (targetFunction) {
|
|
3704
|
+
const spinner = ora11("Applying migrations...").start();
|
|
3705
|
+
try {
|
|
3706
|
+
await executeFunctionMigrations2([targetFunction]);
|
|
3707
|
+
spinner.succeed("Migrations applied");
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
spinner.fail("Failed to apply migrations");
|
|
3710
|
+
console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3711
|
+
process.exit(1);
|
|
3628
3712
|
}
|
|
3713
|
+
} else {
|
|
3714
|
+
console.log(chalk26.gray("\u2139\uFE0F No migrations found for this package"));
|
|
3629
3715
|
}
|
|
3630
|
-
} else {
|
|
3631
|
-
console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
3632
3716
|
}
|
|
3633
|
-
|
|
3717
|
+
} else {
|
|
3718
|
+
console.log(chalk26.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
3719
|
+
}
|
|
3720
|
+
console.log(chalk26.green(`
|
|
3634
3721
|
\u2705 ${packageName} installed successfully!
|
|
3635
3722
|
`));
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
}
|
|
3641
|
-
} catch (error) {
|
|
3642
|
-
console.error(chalk25.red(`
|
|
3643
|
-
\u274C Failed to install ${packageName}
|
|
3644
|
-
`));
|
|
3645
|
-
console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3646
|
-
process.exit(1);
|
|
3723
|
+
if (pkgJson.spfn?.setupMessage) {
|
|
3724
|
+
console.log(chalk26.cyan("\u{1F4DA} Setup Guide:"));
|
|
3725
|
+
console.log(pkgJson.spfn.setupMessage);
|
|
3726
|
+
console.log();
|
|
3647
3727
|
}
|
|
3648
3728
|
}
|
|
3649
3729
|
var addCommand = new Command10("add").description("Install and set up SPFN ecosystem packages").argument("<package>", "Package name (e.g., @spfn/cms, @mycompany/spfn-analytics)").action(addPackage);
|
|
3650
3730
|
|
|
3651
|
-
// src/commands/
|
|
3652
|
-
init_logger();
|
|
3731
|
+
// src/commands/env.ts
|
|
3653
3732
|
import { Command as Command11 } from "commander";
|
|
3654
|
-
import ora12 from "ora";
|
|
3655
|
-
import { join as join27 } from "path";
|
|
3656
|
-
import { existsSync as existsSync24 } from "fs";
|
|
3657
3733
|
import chalk27 from "chalk";
|
|
3658
|
-
|
|
3659
|
-
// src/commands/generate/prompts.ts
|
|
3660
|
-
init_logger();
|
|
3661
|
-
import prompts7 from "prompts";
|
|
3662
|
-
import chalk26 from "chalk";
|
|
3663
|
-
async function promptScope() {
|
|
3664
|
-
const response = await prompts7({
|
|
3665
|
-
type: "text",
|
|
3666
|
-
name: "scope",
|
|
3667
|
-
message: "NPM scope (e.g., @mycompany, @username):",
|
|
3668
|
-
initial: "@spfn",
|
|
3669
|
-
validate: (value) => {
|
|
3670
|
-
if (!value || value.length === 0) {
|
|
3671
|
-
return "Scope is required";
|
|
3672
|
-
}
|
|
3673
|
-
if (!/^@[a-z0-9-]+$/.test(value)) {
|
|
3674
|
-
return "Scope must start with @ and contain lowercase alphanumeric with hyphens";
|
|
3675
|
-
}
|
|
3676
|
-
return true;
|
|
3677
|
-
}
|
|
3678
|
-
});
|
|
3679
|
-
if (!response.scope) {
|
|
3680
|
-
logger.info("Cancelled");
|
|
3681
|
-
process.exit(0);
|
|
3682
|
-
}
|
|
3683
|
-
return response.scope;
|
|
3684
|
-
}
|
|
3685
|
-
async function promptFunctionName() {
|
|
3686
|
-
const response = await prompts7({
|
|
3687
|
-
type: "text",
|
|
3688
|
-
name: "fnName",
|
|
3689
|
-
message: "Function name:",
|
|
3690
|
-
validate: (value) => {
|
|
3691
|
-
if (!value || value.length === 0) {
|
|
3692
|
-
return "Function name is required";
|
|
3693
|
-
}
|
|
3694
|
-
if (!/^[a-z][a-z0-9-]*$/.test(value)) {
|
|
3695
|
-
return "Function name must be lowercase alphanumeric with hyphens";
|
|
3696
|
-
}
|
|
3697
|
-
return true;
|
|
3698
|
-
}
|
|
3699
|
-
});
|
|
3700
|
-
if (!response.fnName) {
|
|
3701
|
-
logger.info("Cancelled");
|
|
3702
|
-
process.exit(0);
|
|
3703
|
-
}
|
|
3704
|
-
return response.fnName;
|
|
3705
|
-
}
|
|
3706
|
-
async function promptDescription(fnName) {
|
|
3707
|
-
const response = await prompts7({
|
|
3708
|
-
type: "text",
|
|
3709
|
-
name: "description",
|
|
3710
|
-
message: "Function description:",
|
|
3711
|
-
initial: "A description of what this module does"
|
|
3712
|
-
});
|
|
3713
|
-
return response.description || "A description of what this module does";
|
|
3714
|
-
}
|
|
3715
|
-
async function promptEntities() {
|
|
3716
|
-
const response = await prompts7({
|
|
3717
|
-
type: "list",
|
|
3718
|
-
name: "entities",
|
|
3719
|
-
message: "Entity names (comma-separated, press enter to skip):",
|
|
3720
|
-
separator: ",",
|
|
3721
|
-
initial: ""
|
|
3722
|
-
});
|
|
3723
|
-
return response.entities || [];
|
|
3724
|
-
}
|
|
3725
|
-
async function confirmConfiguration(config) {
|
|
3726
|
-
const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
|
|
3727
|
-
console.log("");
|
|
3728
|
-
logger.info(chalk26.bold("\u26A1 Function Configuration:"));
|
|
3729
|
-
console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
|
|
3730
|
-
console.log(` ${chalk26.gray("Description:")} ${description}`);
|
|
3731
|
-
console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
|
|
3732
|
-
console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3733
|
-
console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
|
|
3734
|
-
console.log("");
|
|
3735
|
-
const { confirmed } = await prompts7({
|
|
3736
|
-
type: "confirm",
|
|
3737
|
-
name: "confirmed",
|
|
3738
|
-
message: "Create function?",
|
|
3739
|
-
initial: true
|
|
3740
|
-
});
|
|
3741
|
-
if (!confirmed) {
|
|
3742
|
-
logger.info("Cancelled");
|
|
3743
|
-
return false;
|
|
3744
|
-
}
|
|
3745
|
-
return true;
|
|
3746
|
-
}
|
|
3747
|
-
|
|
3748
|
-
// src/commands/generate/generators/structure.ts
|
|
3749
|
-
import { join as join26 } from "path";
|
|
3750
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
3751
|
-
|
|
3752
|
-
// src/commands/generate/generators/config.ts
|
|
3753
|
-
import { join as join20 } from "path";
|
|
3754
|
-
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
|
|
3755
|
-
|
|
3756
|
-
// src/commands/generate/string-utils.ts
|
|
3757
|
-
function toPascalCase(str) {
|
|
3758
|
-
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3759
|
-
}
|
|
3760
|
-
function toKebabCase(str) {
|
|
3761
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
3762
|
-
}
|
|
3763
|
-
function toSnakeCase(str) {
|
|
3764
|
-
return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/-/g, "_");
|
|
3765
|
-
}
|
|
3766
|
-
function toCamelCase(str) {
|
|
3767
|
-
return str.split(/[-_]/).map(
|
|
3768
|
-
(word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
|
|
3769
|
-
).join("");
|
|
3770
|
-
}
|
|
3771
|
-
function toSafeSchemaName(str) {
|
|
3772
|
-
let result = str.toLowerCase().replace(/[@]/g, "").replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
3773
|
-
if (/^[0-9]/.test(result)) {
|
|
3774
|
-
result = "_" + result;
|
|
3775
|
-
}
|
|
3776
|
-
if (result.length > 63) {
|
|
3777
|
-
result = result.substring(0, 63);
|
|
3778
|
-
}
|
|
3779
|
-
return result;
|
|
3780
|
-
}
|
|
3781
|
-
|
|
3782
|
-
// src/commands/generate/template-loader.ts
|
|
3783
|
-
import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
|
|
3784
|
-
import { join as join19, dirname as dirname3 } from "path";
|
|
3785
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3786
|
-
function findTemplatesPath2() {
|
|
3787
|
-
const __filename = fileURLToPath2(import.meta.url);
|
|
3788
|
-
const __dirname2 = dirname3(__filename);
|
|
3789
|
-
const distPath = join19(__dirname2, "commands", "generate", "templates");
|
|
3790
|
-
if (existsSync22(distPath)) {
|
|
3791
|
-
return distPath;
|
|
3792
|
-
}
|
|
3793
|
-
const sameDirPath = join19(__dirname2, "templates");
|
|
3794
|
-
if (existsSync22(sameDirPath)) {
|
|
3795
|
-
return sameDirPath;
|
|
3796
|
-
}
|
|
3797
|
-
const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
|
|
3798
|
-
if (existsSync22(srcPath)) {
|
|
3799
|
-
return srcPath;
|
|
3800
|
-
}
|
|
3801
|
-
throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
|
|
3802
|
-
}
|
|
3803
|
-
function loadTemplate(templateName, variables) {
|
|
3804
|
-
const templatesDir = findTemplatesPath2();
|
|
3805
|
-
const templatePath = join19(templatesDir, `${templateName}.template`);
|
|
3806
|
-
let content = readFileSync8(templatePath, "utf-8");
|
|
3807
|
-
for (const [key, value] of Object.entries(variables)) {
|
|
3808
|
-
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
3809
|
-
content = content.replace(regex, value);
|
|
3810
|
-
}
|
|
3811
|
-
return content;
|
|
3812
|
-
}
|
|
3813
|
-
|
|
3814
|
-
// src/commands/generate/generators/config.ts
|
|
3815
|
-
function generatePackageJson(fnDir, scope, fnName, description) {
|
|
3816
|
-
const content = {
|
|
3817
|
-
name: `${scope}/${fnName}`,
|
|
3818
|
-
version: "0.1.0-alpha.1",
|
|
3819
|
-
description,
|
|
3820
|
-
type: "module",
|
|
3821
|
-
main: "./dist/index.js",
|
|
3822
|
-
types: "./dist/index.d.ts",
|
|
3823
|
-
exports: {
|
|
3824
|
-
".": {
|
|
3825
|
-
types: "./dist/index.d.ts",
|
|
3826
|
-
import: "./dist/index.js",
|
|
3827
|
-
require: "./dist/index.js"
|
|
3828
|
-
},
|
|
3829
|
-
"./server": {
|
|
3830
|
-
types: "./dist/server.d.ts",
|
|
3831
|
-
import: "./dist/server.js",
|
|
3832
|
-
require: "./dist/server.js"
|
|
3833
|
-
},
|
|
3834
|
-
"./client": {
|
|
3835
|
-
types: "./dist/client.d.ts",
|
|
3836
|
-
import: "./dist/client.js",
|
|
3837
|
-
require: "./dist/client.js"
|
|
3838
|
-
}
|
|
3839
|
-
},
|
|
3840
|
-
files: ["dist", "migrations", "README.md"],
|
|
3841
|
-
scripts: {
|
|
3842
|
-
build: "npm run db:generate && tsup",
|
|
3843
|
-
watch: "tsup --watch",
|
|
3844
|
-
dev: "tsup --watch",
|
|
3845
|
-
"type-check": "tsc --noEmit",
|
|
3846
|
-
clean: "rm -rf dist migrations",
|
|
3847
|
-
"db:generate": "drizzle-kit generate",
|
|
3848
|
-
codegen: "spfn codegen run",
|
|
3849
|
-
test: "vitest",
|
|
3850
|
-
"test:watch": "vitest --watch",
|
|
3851
|
-
"test:coverage": "vitest run --coverage",
|
|
3852
|
-
"test:routes": "vitest src/server/routes",
|
|
3853
|
-
"docker:test:up": "docker compose -f docker-compose.test.yml up -d",
|
|
3854
|
-
"docker:test:down": "docker compose -f docker-compose.test.yml down",
|
|
3855
|
-
"docker:test:logs": "docker compose -f docker-compose.test.yml logs -f"
|
|
3856
|
-
},
|
|
3857
|
-
keywords: ["spfn", fnName, "nextjs", "typescript"],
|
|
3858
|
-
author: "SPFN Team",
|
|
3859
|
-
license: "MIT",
|
|
3860
|
-
spfn: {
|
|
3861
|
-
prefix: `/_${fnName}`,
|
|
3862
|
-
schemas: ["./dist/server/entities/*.js"],
|
|
3863
|
-
routes: { dir: "./dist/server/routes" },
|
|
3864
|
-
migrations: { dir: "./migrations" },
|
|
3865
|
-
codegen: {
|
|
3866
|
-
generators: [
|
|
3867
|
-
{
|
|
3868
|
-
name: "@spfn/core:contract",
|
|
3869
|
-
contractsDir: "src/lib/contracts",
|
|
3870
|
-
outputPath: "src/api",
|
|
3871
|
-
runOn: ["build", "manual"]
|
|
3872
|
-
},
|
|
3873
|
-
{
|
|
3874
|
-
name: "${scope}/${fnName}:example",
|
|
3875
|
-
runOn: ["manual"]
|
|
3876
|
-
}
|
|
3877
|
-
]
|
|
3878
|
-
},
|
|
3879
|
-
setupMessage: ` \u{1F4DA} Next steps:
|
|
3880
|
-
1. Import from '${scope}/${fnName}'
|
|
3881
|
-
2. View data: pnpm spfn db studio
|
|
3882
|
-
3. API at: http://localhost:8790/_${fnName}`
|
|
3883
|
-
},
|
|
3884
|
-
peerDependencies: {
|
|
3885
|
-
"@spfn/core": "workspace:*",
|
|
3886
|
-
"drizzle-orm": "^0.44.0",
|
|
3887
|
-
next: "^15.0.0",
|
|
3888
|
-
react: "^18.0.0 || ^19.0.0"
|
|
3889
|
-
},
|
|
3890
|
-
dependencies: {
|
|
3891
|
-
"@sinclair/typebox": "^0.34.0"
|
|
3892
|
-
},
|
|
3893
|
-
devDependencies: {
|
|
3894
|
-
"@types/node": "^20.11.0",
|
|
3895
|
-
"@types/react": "^19",
|
|
3896
|
-
"@vitest/coverage-v8": "^4.0.6",
|
|
3897
|
-
"drizzle-kit": "^0.31.6",
|
|
3898
|
-
"drizzle-typebox": "^0.1.0",
|
|
3899
|
-
glob: "^11.0.3",
|
|
3900
|
-
postgres: "^3.4.0",
|
|
3901
|
-
spfn: "workspace:*",
|
|
3902
|
-
tsup: "^8.3.5",
|
|
3903
|
-
tsx: "^4.19.2",
|
|
3904
|
-
typescript: "^5",
|
|
3905
|
-
vitest: "^4.0.6"
|
|
3906
|
-
}
|
|
3907
|
-
};
|
|
3908
|
-
writeFileSync12(
|
|
3909
|
-
join20(fnDir, "package.json"),
|
|
3910
|
-
JSON.stringify(content, null, 4) + "\n"
|
|
3911
|
-
);
|
|
3912
|
-
}
|
|
3913
|
-
function generateTsConfig(fnDir) {
|
|
3914
|
-
const content = {
|
|
3915
|
-
compilerOptions: {
|
|
3916
|
-
target: "ES2022",
|
|
3917
|
-
module: "ESNext",
|
|
3918
|
-
lib: ["ES2022"],
|
|
3919
|
-
moduleResolution: "bundler",
|
|
3920
|
-
esModuleInterop: true,
|
|
3921
|
-
skipLibCheck: true,
|
|
3922
|
-
strict: true,
|
|
3923
|
-
noUncheckedIndexedAccess: true,
|
|
3924
|
-
declaration: true,
|
|
3925
|
-
declarationMap: true,
|
|
3926
|
-
sourceMap: true,
|
|
3927
|
-
outDir: "./dist",
|
|
3928
|
-
rootDir: "./src",
|
|
3929
|
-
composite: false,
|
|
3930
|
-
isolatedModules: true,
|
|
3931
|
-
resolveJsonModule: true,
|
|
3932
|
-
allowSyntheticDefaultImports: true,
|
|
3933
|
-
forceConsistentCasingInFileNames: true,
|
|
3934
|
-
baseUrl: ".",
|
|
3935
|
-
paths: {
|
|
3936
|
-
"@/*": ["./src/*"]
|
|
3937
|
-
}
|
|
3938
|
-
},
|
|
3939
|
-
include: ["src/**/*"],
|
|
3940
|
-
exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
|
|
3941
|
-
};
|
|
3942
|
-
writeFileSync12(
|
|
3943
|
-
join20(fnDir, "tsconfig.json"),
|
|
3944
|
-
JSON.stringify(content, null, 4) + "\n"
|
|
3945
|
-
);
|
|
3946
|
-
}
|
|
3947
|
-
function generateTsupConfig(fnDir) {
|
|
3948
|
-
const content = `import { defineConfig } from 'tsup';
|
|
3949
|
-
import { glob } from 'glob';
|
|
3950
|
-
import * as path from 'path';
|
|
3951
|
-
|
|
3952
|
-
// Find all route handler files
|
|
3953
|
-
const routeFiles = glob.sync('src/server/routes/**/index.ts');
|
|
3954
|
-
const routeEntries = Object.fromEntries(
|
|
3955
|
-
routeFiles.map((file) =>
|
|
3956
|
-
{
|
|
3957
|
-
const key = file
|
|
3958
|
-
.replace('src/', '')
|
|
3959
|
-
.replace('/index.ts', '/index')
|
|
3960
|
-
.replace('.ts', '');
|
|
3961
|
-
return [key, file];
|
|
3962
|
-
})
|
|
3963
|
-
);
|
|
3964
|
-
|
|
3965
|
-
// Find all entity files
|
|
3966
|
-
const entityFiles = glob.sync('src/server/entities/**/*.ts');
|
|
3967
|
-
const entityEntries = Object.fromEntries(
|
|
3968
|
-
entityFiles.map((file) =>
|
|
3969
|
-
{
|
|
3970
|
-
const key = file.replace('src/', '').replace('.ts', '');
|
|
3971
|
-
return [key, file];
|
|
3972
|
-
})
|
|
3973
|
-
);
|
|
3974
|
-
|
|
3975
|
-
// Find all contract files
|
|
3976
|
-
const contractFiles = glob.sync('src/lib/contracts/**/*.ts');
|
|
3977
|
-
const contractEntries = Object.fromEntries(
|
|
3978
|
-
contractFiles.map((file) =>
|
|
3979
|
-
{
|
|
3980
|
-
const key = file.replace('src/', '').replace('.ts', '');
|
|
3981
|
-
return [key, file];
|
|
3982
|
-
})
|
|
3983
|
-
);
|
|
3984
|
-
|
|
3985
|
-
export default defineConfig({
|
|
3986
|
-
entry: {
|
|
3987
|
-
index: 'src/index.ts',
|
|
3988
|
-
server: 'src/server.ts',
|
|
3989
|
-
client: 'src/client.ts',
|
|
3990
|
-
...entityEntries,
|
|
3991
|
-
...contractEntries,
|
|
3992
|
-
...routeEntries,
|
|
3993
|
-
},
|
|
3994
|
-
format: ['esm'],
|
|
3995
|
-
dts: true,
|
|
3996
|
-
sourcemap: true,
|
|
3997
|
-
clean: true,
|
|
3998
|
-
splitting: false,
|
|
3999
|
-
esbuildOptions(options)
|
|
4000
|
-
{
|
|
4001
|
-
// Add path alias support
|
|
4002
|
-
options.alias = {
|
|
4003
|
-
'@': path.resolve(__dirname, './src'),
|
|
4004
|
-
};
|
|
4005
|
-
},
|
|
4006
|
-
external: [
|
|
4007
|
-
'@spfn/core',
|
|
4008
|
-
'drizzle-orm',
|
|
4009
|
-
'next',
|
|
4010
|
-
'react',
|
|
4011
|
-
'@sinclair/typebox',
|
|
4012
|
-
'jiti',
|
|
4013
|
-
'zustand',
|
|
4014
|
-
'server-only',
|
|
4015
|
-
'react-dom',
|
|
4016
|
-
],
|
|
4017
|
-
});
|
|
4018
|
-
`;
|
|
4019
|
-
writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
|
|
4020
|
-
}
|
|
4021
|
-
function generateDrizzleConfig(fnDir, scope, fnName) {
|
|
4022
|
-
const schemaName = `spfn_${toSnakeCase(fnName)}`;
|
|
4023
|
-
const content = `import { defineConfig } from 'drizzle-kit';
|
|
4024
|
-
|
|
4025
|
-
/**
|
|
4026
|
-
* Drizzle Config for ${scope}/${fnName} package
|
|
4027
|
-
*
|
|
4028
|
-
* This generates migrations for the ${fnName} schema that will be
|
|
4029
|
-
* bundled with the package and applied automatically when
|
|
4030
|
-
* users run \`spfn db push\` or \`spfn db migrate\`
|
|
4031
|
-
*/
|
|
4032
|
-
export default defineConfig({
|
|
4033
|
-
schema: './src/server/entities/*.ts',
|
|
4034
|
-
out: './migrations',
|
|
4035
|
-
dialect: 'postgresql',
|
|
4036
|
-
schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
|
|
4037
|
-
});
|
|
4038
|
-
`;
|
|
4039
|
-
writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
|
|
4040
|
-
}
|
|
4041
|
-
function generateExampleGenerator(fnDir, scope, fnName) {
|
|
4042
|
-
const pascalName = toPascalCase(fnName);
|
|
4043
|
-
const content = `/**
|
|
4044
|
-
* Example Custom Generator for ${fnName}
|
|
4045
|
-
*
|
|
4046
|
-
* This is a template for creating custom code generators.
|
|
4047
|
-
* Uncomment and modify to create your own generator.
|
|
4048
|
-
*/
|
|
4049
|
-
|
|
4050
|
-
import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
|
|
4051
|
-
import { writeFileSync, mkdirSync } from 'fs';
|
|
4052
|
-
import { join } from 'path';
|
|
4053
|
-
|
|
4054
|
-
/**
|
|
4055
|
-
* Example: Auto-generate index files for types
|
|
4056
|
-
*/
|
|
4057
|
-
export function create${pascalName}ExampleGenerator(): Generator
|
|
4058
|
-
{
|
|
4059
|
-
return {
|
|
4060
|
-
name: '${fnName}:example',
|
|
4061
|
-
|
|
4062
|
-
// Define when this generator should run
|
|
4063
|
-
// Options: 'build', 'manual', 'watch', 'start'
|
|
4064
|
-
runOn: ['build', 'manual'],
|
|
4065
|
-
|
|
4066
|
-
// Optional: watch for file changes
|
|
4067
|
-
watchPatterns: [
|
|
4068
|
-
'src/lib/types/**/*.ts',
|
|
4069
|
-
],
|
|
4070
|
-
|
|
4071
|
-
async generate(options: GeneratorOptions): Promise<void>
|
|
4072
|
-
{
|
|
4073
|
-
const { cwd, debug } = options;
|
|
4074
|
-
|
|
4075
|
-
if (debug)
|
|
4076
|
-
{
|
|
4077
|
-
console.log('Running ${fnName} example generator...');
|
|
4078
|
-
}
|
|
4079
|
-
|
|
4080
|
-
// Your generation logic here
|
|
4081
|
-
const outputDir = join(cwd, 'src/generated');
|
|
4082
|
-
const outputPath = join(outputDir, 'example.ts');
|
|
4083
|
-
|
|
4084
|
-
// Ensure output directory exists
|
|
4085
|
-
mkdirSync(outputDir, { recursive: true });
|
|
4086
|
-
|
|
4087
|
-
// Generate content
|
|
4088
|
-
const content = \`/**
|
|
4089
|
-
* Auto-generated file
|
|
4090
|
-
*
|
|
4091
|
-
* @generated \${new Date().toISOString()}
|
|
4092
|
-
*/
|
|
4093
|
-
|
|
4094
|
-
export const generatedAt = '\${new Date().toISOString()}';
|
|
4095
|
-
export const moduleName = '${fnName}';
|
|
4096
|
-
\`;
|
|
4097
|
-
|
|
4098
|
-
writeFileSync(outputPath, content, 'utf-8');
|
|
4099
|
-
|
|
4100
|
-
if (debug)
|
|
4101
|
-
{
|
|
4102
|
-
console.log('\u2713 Generated:', outputPath);
|
|
4103
|
-
}
|
|
4104
|
-
},
|
|
4105
|
-
};
|
|
4106
|
-
}
|
|
4107
|
-
`;
|
|
4108
|
-
const generatorsDir = join20(fnDir, "src/server/generators");
|
|
4109
|
-
mkdirSync3(generatorsDir, { recursive: true });
|
|
4110
|
-
writeFileSync12(
|
|
4111
|
-
join20(generatorsDir, "example-generator.ts"),
|
|
4112
|
-
content
|
|
4113
|
-
);
|
|
4114
|
-
const indexContent = `/**
|
|
4115
|
-
* Custom Generators
|
|
4116
|
-
*
|
|
4117
|
-
* Export your custom generators here.
|
|
4118
|
-
*
|
|
4119
|
-
* The example generator is already registered in package.json.
|
|
4120
|
-
* Comment it out if you don't want to use it.
|
|
4121
|
-
*/
|
|
4122
|
-
|
|
4123
|
-
export { create${pascalName}ExampleGenerator } from './example-generator.js';
|
|
4124
|
-
`;
|
|
4125
|
-
writeFileSync12(
|
|
4126
|
-
join20(generatorsDir, "index.ts"),
|
|
4127
|
-
indexContent
|
|
4128
|
-
);
|
|
4129
|
-
}
|
|
4130
|
-
function generateReadme(fnDir, scope, fnName, description) {
|
|
4131
|
-
const content = `# ${scope}/${fnName}
|
|
4132
|
-
|
|
4133
|
-
${description}
|
|
4134
|
-
|
|
4135
|
-
## \u{1F4E6} Installation
|
|
4136
|
-
|
|
4137
|
-
For users of your module:
|
|
4138
|
-
|
|
4139
|
-
\`\`\`bash
|
|
4140
|
-
spfn add ${scope}/${fnName}
|
|
4141
|
-
\`\`\`
|
|
4142
|
-
|
|
4143
|
-
## \u{1F680} Usage
|
|
4144
|
-
|
|
4145
|
-
\`\`\`typescript
|
|
4146
|
-
// Server-side
|
|
4147
|
-
import { /* your entities, repositories */ } from '${scope}/${fnName}/server';
|
|
4148
|
-
|
|
4149
|
-
// Client-side
|
|
4150
|
-
import { /* your hooks, components */ } from '${scope}/${fnName}/client';
|
|
4151
|
-
|
|
4152
|
-
// Generated API client
|
|
4153
|
-
import { ${fnName}Api } from '${scope}/${fnName}/api';
|
|
4154
|
-
|
|
4155
|
-
// Example: Using the generated API client
|
|
4156
|
-
const data = await ${fnName}Api.list();
|
|
4157
|
-
\`\`\`
|
|
4158
|
-
|
|
4159
|
-
## \u{1F4C1} Project Structure
|
|
4160
|
-
|
|
4161
|
-
\`\`\`
|
|
4162
|
-
src/
|
|
4163
|
-
\u251C\u2500\u2500 lib/
|
|
4164
|
-
\u2502 \u251C\u2500\u2500 contracts/ # API contracts (shared types)
|
|
4165
|
-
\u2502 \u2514\u2500\u2500 types/ # Shared TypeScript types
|
|
4166
|
-
\u251C\u2500\u2500 server/
|
|
4167
|
-
\u2502 \u251C\u2500\u2500 entities/ # Drizzle ORM entities
|
|
4168
|
-
\u2502 \u251C\u2500\u2500 repositories/ # Data access layer
|
|
4169
|
-
\u2502 \u251C\u2500\u2500 routes/ # API route handlers
|
|
4170
|
-
\u2502 \u251C\u2500\u2500 helpers/ # Server utilities
|
|
4171
|
-
\u2502 \u2514\u2500\u2500 generators/ # Code generators (optional)
|
|
4172
|
-
\u251C\u2500\u2500 client/
|
|
4173
|
-
\u2502 \u251C\u2500\u2500 hooks/ # React hooks
|
|
4174
|
-
\u2502 \u251C\u2500\u2500 store/ # Zustand stores
|
|
4175
|
-
\u2502 \u2514\u2500\u2500 components/ # React components
|
|
4176
|
-
\u251C\u2500\u2500 api/ # Auto-generated API client (do not edit manually)
|
|
4177
|
-
\u251C\u2500\u2500 index.ts # Main entry point
|
|
4178
|
-
\u251C\u2500\u2500 server.ts # Server exports
|
|
4179
|
-
\u2514\u2500\u2500 client.ts # Client exports
|
|
4180
|
-
\`\`\`
|
|
4181
|
-
|
|
4182
|
-
## \u{1F6E0}\uFE0F Development Guide
|
|
4183
|
-
|
|
4184
|
-
### Available Scripts
|
|
4185
|
-
|
|
4186
|
-
\`\`\`bash
|
|
4187
|
-
# Development
|
|
4188
|
-
pnpm dev # Watch mode - auto rebuild on changes
|
|
4189
|
-
pnpm build # Build for production
|
|
4190
|
-
pnpm type-check # TypeScript type checking
|
|
4191
|
-
|
|
4192
|
-
# Database
|
|
4193
|
-
pnpm db:generate # Generate migrations from entities
|
|
4194
|
-
pnpm clean # Clean dist and migrations
|
|
4195
|
-
|
|
4196
|
-
# Code Generation
|
|
4197
|
-
pnpm codegen # Generate API client from contracts
|
|
4198
|
-
|
|
4199
|
-
# Testing
|
|
4200
|
-
pnpm test # Run tests
|
|
4201
|
-
pnpm test:watch # Watch mode
|
|
4202
|
-
pnpm test:coverage # Generate coverage report
|
|
4203
|
-
pnpm test:routes # Test only routes
|
|
4204
|
-
|
|
4205
|
-
# Docker (for testing)
|
|
4206
|
-
pnpm docker:test:up # Start test database
|
|
4207
|
-
pnpm docker:test:down # Stop test database
|
|
4208
|
-
pnpm docker:test:logs # View database logs
|
|
4209
|
-
\`\`\`
|
|
4210
|
-
|
|
4211
|
-
### Development Workflow
|
|
4212
|
-
|
|
4213
|
-
1. **Define Entities** (\`src/server/entities/\`)
|
|
4214
|
-
\`\`\`typescript
|
|
4215
|
-
// src/server/entities/${fnName}-example.ts
|
|
4216
|
-
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
|
|
4217
|
-
|
|
4218
|
-
export const ${fnName}Examples = pgTable('${fnName}_examples', {
|
|
4219
|
-
id: uuid('id').primaryKey().defaultRandom(),
|
|
4220
|
-
name: text('name').notNull(),
|
|
4221
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
4222
|
-
});
|
|
4223
|
-
\`\`\`
|
|
4224
|
-
|
|
4225
|
-
2. **Generate Migrations**
|
|
4226
|
-
\`\`\`bash
|
|
4227
|
-
pnpm db:generate
|
|
4228
|
-
\`\`\`
|
|
4229
|
-
|
|
4230
|
-
3. **Create Repositories** (\`src/server/repositories/\`)
|
|
4231
|
-
\`\`\`typescript
|
|
4232
|
-
// src/server/repositories/example-repository.ts
|
|
4233
|
-
import { db } from '@spfn/core/db';
|
|
4234
|
-
import { ${fnName}Examples } from '../entities/${fnName}-example';
|
|
4235
|
-
|
|
4236
|
-
export class ExampleRepository {
|
|
4237
|
-
static async findAll() {
|
|
4238
|
-
return db.select().from(${fnName}Examples);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
\`\`\`
|
|
4242
|
-
|
|
4243
|
-
4. **Define Contracts** (\`src/lib/contracts/\`)
|
|
4244
|
-
\`\`\`typescript
|
|
4245
|
-
// src/lib/contracts/example.ts
|
|
4246
|
-
import { contract } from '@spfn/core';
|
|
4247
|
-
import { Type as t } from '@sinclair/typebox';
|
|
4248
|
-
|
|
4249
|
-
export const getExamplesContract = contract({
|
|
4250
|
-
method: 'GET',
|
|
4251
|
-
path: '/_${fnName}/examples',
|
|
4252
|
-
responses: {
|
|
4253
|
-
200: t.Array(t.Object({
|
|
4254
|
-
id: t.String(),
|
|
4255
|
-
name: t.String(),
|
|
4256
|
-
createdAt: t.String(),
|
|
4257
|
-
})),
|
|
4258
|
-
},
|
|
4259
|
-
});
|
|
4260
|
-
\`\`\`
|
|
4261
|
-
|
|
4262
|
-
5. **Create Routes** (\`src/server/routes/\`)
|
|
4263
|
-
\`\`\`typescript
|
|
4264
|
-
// src/server/routes/examples/index.ts
|
|
4265
|
-
import { route } from '@spfn/core';
|
|
4266
|
-
import { getExamplesContract } from '@/lib/contracts/example';
|
|
4267
|
-
import { ExampleRepository } from '@/server/repositories/example-repository';
|
|
4268
|
-
|
|
4269
|
-
export const GET = route(getExamplesContract, async () => {
|
|
4270
|
-
const examples = await ExampleRepository.findAll();
|
|
4271
|
-
return { status: 200, body: examples };
|
|
4272
|
-
});
|
|
4273
|
-
\`\`\`
|
|
4274
|
-
|
|
4275
|
-
6. **Generate API Client**
|
|
4276
|
-
\`\`\`bash
|
|
4277
|
-
pnpm codegen
|
|
4278
|
-
\`\`\`
|
|
4279
|
-
|
|
4280
|
-
This generates type-safe API client in \`src/api/\`:
|
|
4281
|
-
\`\`\`typescript
|
|
4282
|
-
// Auto-generated
|
|
4283
|
-
export const ${fnName}Api = {
|
|
4284
|
-
examples: Examples,
|
|
4285
|
-
// ... other resources
|
|
4286
|
-
};
|
|
4287
|
-
\`\`\`
|
|
4288
|
-
|
|
4289
|
-
7. **Write Tests** (\`src/server/routes/__tests__/\`)
|
|
4290
|
-
\`\`\`typescript
|
|
4291
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
4292
|
-
import { GET } from '../examples/index';
|
|
4293
|
-
|
|
4294
|
-
describe('GET /_${fnName}/examples', () => {
|
|
4295
|
-
it('should return all examples', async () => {
|
|
4296
|
-
const response = await GET();
|
|
4297
|
-
expect(response.status).toBe(200);
|
|
4298
|
-
expect(Array.isArray(response.body)).toBe(true);
|
|
4299
|
-
});
|
|
4300
|
-
});
|
|
4301
|
-
\`\`\`
|
|
4302
|
-
|
|
4303
|
-
8. **Create Client Hooks** (optional)
|
|
4304
|
-
\`\`\`typescript
|
|
4305
|
-
// src/client/hooks/use-examples.ts
|
|
4306
|
-
'use client';
|
|
4307
|
-
import { useState, useEffect } from 'react';
|
|
4308
|
-
import { ${fnName}Api } from '@/api';
|
|
4309
|
-
|
|
4310
|
-
export function useExamples() {
|
|
4311
|
-
const [examples, setExamples] = useState([]);
|
|
4312
|
-
const [loading, setLoading] = useState(true);
|
|
4313
|
-
|
|
4314
|
-
useEffect(() => {
|
|
4315
|
-
${fnName}Api.examples.list()
|
|
4316
|
-
.then(setExamples)
|
|
4317
|
-
.finally(() => setLoading(false));
|
|
4318
|
-
}, []);
|
|
4319
|
-
|
|
4320
|
-
return { examples, loading };
|
|
4321
|
-
}
|
|
4322
|
-
\`\`\`
|
|
4323
|
-
|
|
4324
|
-
### Configuration
|
|
4325
|
-
|
|
4326
|
-
The module is configured via \`package.json\`:
|
|
4327
|
-
|
|
4328
|
-
\`\`\`json
|
|
4329
|
-
{
|
|
4330
|
-
"spfn": {
|
|
4331
|
-
"prefix": "/_${fnName}",
|
|
4332
|
-
"schemas": ["./dist/server/entities/*.js"],
|
|
4333
|
-
"routes": { "dir": "./dist/server/routes" },
|
|
4334
|
-
"migrations": { "dir": "./migrations" },
|
|
4335
|
-
"codegen": {
|
|
4336
|
-
"generators": [{
|
|
4337
|
-
"name": "@spfn/core:contract",
|
|
4338
|
-
"contractsDir": "src/lib/contracts",
|
|
4339
|
-
"outputPath": "src/api"
|
|
4340
|
-
}]
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
}
|
|
4344
|
-
\`\`\`
|
|
4345
|
-
|
|
4346
|
-
#### Configuration Options
|
|
4347
|
-
|
|
4348
|
-
| Field | Type | Description |
|
|
4349
|
-
|-------|------|-------------|
|
|
4350
|
-
| \`prefix\` | \`string\` | API route prefix for this module. Used for namespacing routes and generating API client name. Example: \`"/_${fnName}"\` becomes \`${fnName}Api\` |
|
|
4351
|
-
| \`schemas\` | \`string[]\` | Glob patterns for Drizzle entity files. These are used by \`spfn db push\` to apply migrations. |
|
|
4352
|
-
| \`routes.dir\` | \`string\` | Directory containing compiled route handlers. Used by \`spfn start\` to load routes. |
|
|
4353
|
-
| \`migrations.dir\` | \`string\` | Directory containing database migration files generated by \`drizzle-kit generate\`. |
|
|
4354
|
-
| \`codegen.generators\` | \`Generator[]\` | Array of code generators to run. Built-in: \`@spfn/core:contract\` for API client generation. |
|
|
4355
|
-
| \`setupMessage\` | \`string\` | (Optional) Message shown after \`spfn add ${scope}/${fnName}\` to guide users. |
|
|
4356
|
-
|
|
4357
|
-
#### Built-in Generator: @spfn/core:contract
|
|
4358
|
-
|
|
4359
|
-
The contract generator automatically creates type-safe API clients from your route contracts.
|
|
4360
|
-
|
|
4361
|
-
\`\`\`json
|
|
4362
|
-
{
|
|
4363
|
-
"name": "@spfn/core:contract",
|
|
4364
|
-
"contractsDir": "src/lib/contracts", // Where to scan for contracts
|
|
4365
|
-
"outputPath": "src/api" // Where to generate API client
|
|
4366
|
-
}
|
|
4367
|
-
\`\`\`
|
|
4368
|
-
|
|
4369
|
-
**How it works:**
|
|
4370
|
-
1. Scans \`contractsDir\` for contract exports
|
|
4371
|
-
2. Groups routes by resource (based on file name)
|
|
4372
|
-
3. Generates:
|
|
4373
|
-
- Resource files (\`src/api/Examples.ts\`)
|
|
4374
|
-
- Type exports for params, query, body, response
|
|
4375
|
-
- Main index file with \`${fnName}Api\` object
|
|
4376
|
-
4. Uses \`prefix\` to generate unique API name (avoids naming conflicts)
|
|
4377
|
-
|
|
4378
|
-
**Generated API structure:**
|
|
4379
|
-
\`\`\`typescript
|
|
4380
|
-
// src/api/index.ts
|
|
4381
|
-
export const ${fnName}Api = {
|
|
4382
|
-
examples: Examples, // Camel case keys
|
|
4383
|
-
exampleById: ExampleById,
|
|
4384
|
-
// ...
|
|
4385
|
-
};
|
|
4386
|
-
\`\`\`
|
|
4387
|
-
|
|
4388
|
-
## \u{1F3A8} Custom Generators
|
|
4389
|
-
|
|
4390
|
-
You can create custom code generators for your module. Generators run automatically during development and build.
|
|
4391
|
-
|
|
4392
|
-
### Creating a Custom Generator
|
|
4393
|
-
|
|
4394
|
-
1. **Create generator file** (\`src/server/generators/\`)
|
|
4395
|
-
|
|
4396
|
-
\`\`\`typescript
|
|
4397
|
-
// src/server/generators/sync-generator.ts
|
|
4398
|
-
import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
|
|
4399
|
-
import { writeFileSync } from 'fs';
|
|
4400
|
-
import { join } from 'path';
|
|
4401
|
-
|
|
4402
|
-
export function createSyncGenerator(): Generator {
|
|
4403
|
-
return {
|
|
4404
|
-
name: '${fnName}-sync',
|
|
4405
|
-
|
|
4406
|
-
// Define when this generator should run
|
|
4407
|
-
runOn: ['build', 'manual'], // 'build', 'manual', 'watch', 'start'
|
|
4408
|
-
|
|
4409
|
-
// Optional: watch for file changes
|
|
4410
|
-
watchPatterns: [
|
|
4411
|
-
'src/server/entities/**/*.ts',
|
|
4412
|
-
'src/lib/types/**/*.ts',
|
|
4413
|
-
],
|
|
4414
|
-
|
|
4415
|
-
async generate(options: GeneratorOptions): Promise<void> {
|
|
4416
|
-
const { cwd, debug } = options;
|
|
4417
|
-
|
|
4418
|
-
if (debug) {
|
|
4419
|
-
console.log('Running ${fnName} sync generator...');
|
|
4420
|
-
}
|
|
4421
|
-
|
|
4422
|
-
// Your generation logic here
|
|
4423
|
-
const outputPath = join(cwd, 'src/generated/sync.ts');
|
|
4424
|
-
const content = \`// Auto-generated sync file
|
|
4425
|
-
export const generatedAt = '\${new Date().toISOString()}';
|
|
4426
|
-
\`;
|
|
4427
|
-
|
|
4428
|
-
writeFileSync(outputPath, content, 'utf-8');
|
|
4429
|
-
|
|
4430
|
-
if (debug) {
|
|
4431
|
-
console.log('\u2713 Generated:', outputPath);
|
|
4432
|
-
}
|
|
4433
|
-
},
|
|
4434
|
-
};
|
|
4435
|
-
}
|
|
4436
|
-
\`\`\`
|
|
4437
|
-
|
|
4438
|
-
2. **Export generator** (\`src/server/generators/index.ts\`)
|
|
4439
|
-
|
|
4440
|
-
\`\`\`typescript
|
|
4441
|
-
export { createSyncGenerator } from './sync-generator.js';
|
|
4442
|
-
\`\`\`
|
|
4443
|
-
|
|
4444
|
-
3. **Register in package.json**
|
|
4445
|
-
|
|
4446
|
-
\`\`\`json
|
|
4447
|
-
{
|
|
4448
|
-
"spfn": {
|
|
4449
|
-
"codegen": {
|
|
4450
|
-
"generators": [
|
|
4451
|
-
{
|
|
4452
|
-
"name": "@spfn/core:contract",
|
|
4453
|
-
"contractsDir": "src/lib/contracts",
|
|
4454
|
-
"outputPath": "src/api"
|
|
4455
|
-
},
|
|
4456
|
-
{
|
|
4457
|
-
"name": "${scope}/${fnName}:sync", // Custom generator
|
|
4458
|
-
"watchPatterns": ["src/server/entities/**/*.ts"]
|
|
4459
|
-
}
|
|
4460
|
-
]
|
|
4461
|
-
}
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
\`\`\`
|
|
4465
|
-
|
|
4466
|
-
### Generator Options
|
|
4467
|
-
|
|
4468
|
-
\`\`\`typescript
|
|
4469
|
-
interface Generator {
|
|
4470
|
-
name: string; // Generator name
|
|
4471
|
-
runOn?: GeneratorTrigger[]; // When to run: 'build' | 'manual' | 'watch' | 'start'
|
|
4472
|
-
watchPatterns?: string[]; // Glob patterns to watch
|
|
4473
|
-
generate: (options: GeneratorOptions) => Promise<void>;
|
|
4474
|
-
}
|
|
4475
|
-
|
|
4476
|
-
interface GeneratorOptions {
|
|
4477
|
-
cwd: string; // Current working directory
|
|
4478
|
-
debug?: boolean; // Debug mode flag
|
|
4479
|
-
trigger?: { // Trigger information (for watch mode)
|
|
4480
|
-
type: 'watch' | 'manual' | 'build' | 'start';
|
|
4481
|
-
changedFile?: {
|
|
4482
|
-
path: string;
|
|
4483
|
-
event: 'add' | 'change' | 'unlink';
|
|
4484
|
-
};
|
|
4485
|
-
};
|
|
4486
|
-
}
|
|
4487
|
-
\`\`\`
|
|
4488
|
-
|
|
4489
|
-
### Real-World Example: Type Sync Generator
|
|
4490
|
-
|
|
4491
|
-
\`\`\`typescript
|
|
4492
|
-
// src/server/generators/type-sync-generator.ts
|
|
4493
|
-
import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
|
|
4494
|
-
import { readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4495
|
-
import { join } from 'path';
|
|
4496
|
-
|
|
4497
|
-
/**
|
|
4498
|
-
* Generates a type index that auto-exports all types
|
|
4499
|
-
*/
|
|
4500
|
-
export function createTypeSyncGenerator(): Generator {
|
|
4501
|
-
return {
|
|
4502
|
-
name: '${fnName}-type-sync',
|
|
4503
|
-
runOn: ['build', 'watch', 'manual'],
|
|
4504
|
-
watchPatterns: ['src/lib/types/**/*.ts'],
|
|
4505
|
-
|
|
4506
|
-
async generate(options: GeneratorOptions): Promise<void> {
|
|
4507
|
-
const { cwd, debug } = options;
|
|
4508
|
-
const typesDir = join(cwd, 'src/lib/types');
|
|
4509
|
-
const outputFile = join(typesDir, 'index.ts');
|
|
4510
|
-
|
|
4511
|
-
// Read all type files
|
|
4512
|
-
const files = readdirSync(typesDir)
|
|
4513
|
-
.filter(f => f.endsWith('.ts') && f !== 'index.ts')
|
|
4514
|
-
.map(f => f.replace('.ts', ''));
|
|
4515
|
-
|
|
4516
|
-
// Generate exports
|
|
4517
|
-
const content = \`/**
|
|
4518
|
-
* Auto-generated type index
|
|
4519
|
-
*
|
|
4520
|
-
* @generated \${new Date().toISOString()}
|
|
4521
|
-
*/
|
|
4522
|
-
|
|
4523
|
-
\${files.map(f => \`export * from './\${f}.js';\`).join('\\n')}
|
|
4524
|
-
\`;
|
|
4525
|
-
|
|
4526
|
-
writeFileSync(outputFile, content, 'utf-8');
|
|
4527
|
-
|
|
4528
|
-
if (debug) {
|
|
4529
|
-
console.log(\`\u2713 Synced \${files.length} type files\`);
|
|
4530
|
-
}
|
|
4531
|
-
},
|
|
4532
|
-
};
|
|
4533
|
-
}
|
|
4534
|
-
\`\`\`
|
|
4535
|
-
|
|
4536
|
-
### Running Generators
|
|
4537
|
-
|
|
4538
|
-
\`\`\`bash
|
|
4539
|
-
# Run all generators
|
|
4540
|
-
pnpm codegen
|
|
4541
|
-
|
|
4542
|
-
# Run specific generator
|
|
4543
|
-
pnpm spfn codegen run --generator ${scope}/${fnName}:sync
|
|
4544
|
-
|
|
4545
|
-
# Watch mode (auto-runs on file changes)
|
|
4546
|
-
pnpm spfn codegen watch
|
|
4547
|
-
\`\`\`
|
|
4548
|
-
|
|
4549
|
-
## \u{1F9EA} Testing
|
|
4550
|
-
|
|
4551
|
-
### Setup Test Database
|
|
4552
|
-
|
|
4553
|
-
\`\`\`bash
|
|
4554
|
-
# Start PostgreSQL in Docker
|
|
4555
|
-
pnpm docker:test:up
|
|
4556
|
-
|
|
4557
|
-
# Run tests
|
|
4558
|
-
pnpm test
|
|
4559
|
-
|
|
4560
|
-
# Stop database
|
|
4561
|
-
pnpm docker:test:down
|
|
4562
|
-
\`\`\`
|
|
4563
|
-
|
|
4564
|
-
### Test Structure
|
|
4565
|
-
|
|
4566
|
-
\`\`\`
|
|
4567
|
-
src/server/routes/
|
|
4568
|
-
\u251C\u2500\u2500 examples/
|
|
4569
|
-
\u2502 \u251C\u2500\u2500 index.ts
|
|
4570
|
-
\u2502 \u2514\u2500\u2500 __tests__/
|
|
4571
|
-
\u2502 \u2514\u2500\u2500 index.test.ts
|
|
4572
|
-
\`\`\`
|
|
4573
|
-
|
|
4574
|
-
## \u{1F4DA} API Documentation
|
|
4575
|
-
|
|
4576
|
-
### Entities
|
|
4577
|
-
|
|
4578
|
-
Document your database entities here.
|
|
4579
|
-
|
|
4580
|
-
### Routes
|
|
4581
|
-
|
|
4582
|
-
- \`GET /_${fnName}/...\` - Description
|
|
4583
|
-
- \`POST /_${fnName}/...\` - Description
|
|
4584
|
-
- \`GET /_${fnName}/.../:id\` - Description
|
|
4585
|
-
- \`PATCH /_${fnName}/.../:id\` - Description
|
|
4586
|
-
- \`DELETE /_${fnName}/.../:id\` - Description
|
|
4587
|
-
|
|
4588
|
-
### Client API
|
|
4589
|
-
|
|
4590
|
-
The generated \`${fnName}Api\` provides type-safe methods for all routes.
|
|
4591
|
-
|
|
4592
|
-
## \u{1F4E6} Publishing
|
|
4593
|
-
|
|
4594
|
-
1. Update version in \`package.json\`
|
|
4595
|
-
2. Build the package: \`pnpm build\`
|
|
4596
|
-
3. Test in a real project
|
|
4597
|
-
4. Publish: \`npm publish\` (or via your CI/CD)
|
|
4598
|
-
|
|
4599
|
-
## \u{1F3D7}\uFE0F Architecture
|
|
4600
|
-
|
|
4601
|
-
This module follows SPFN's 3-layer architecture:
|
|
4602
|
-
|
|
4603
|
-
- **Shared Layer** (\`lib/\`): Contracts and types shared between client and server
|
|
4604
|
-
- **Server Layer** (\`server/\`): Database entities, repositories, business logic, API routes
|
|
4605
|
-
- **Client Layer** (\`client/\`): React hooks, components, state management
|
|
4606
|
-
|
|
4607
|
-
## \u{1F91D} Contributing
|
|
4608
|
-
|
|
4609
|
-
Contributions are welcome! Please follow the development workflow above.
|
|
4610
|
-
|
|
4611
|
-
## \u{1F4C4} License
|
|
4612
|
-
|
|
4613
|
-
MIT
|
|
4614
|
-
`;
|
|
4615
|
-
writeFileSync12(join20(fnDir, "README.md"), content);
|
|
4616
|
-
}
|
|
4617
|
-
|
|
4618
|
-
// src/commands/generate/generators/entity.ts
|
|
4619
|
-
import { join as join21 } from "path";
|
|
4620
|
-
import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
|
|
4621
|
-
function generateSchema(fnDir, scope, fnName) {
|
|
4622
|
-
const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
|
|
4623
|
-
if (existsSync23(schemaFilePath)) {
|
|
4624
|
-
return;
|
|
4625
|
-
}
|
|
4626
|
-
const packageName = `${scope}/${fnName}`;
|
|
4627
|
-
const fnNamePascal = toPascalCase(fnName);
|
|
4628
|
-
const schemaVarName = `${toCamelCase(fnName)}Schema`;
|
|
4629
|
-
const content = loadTemplate("schema", {
|
|
4630
|
-
FN_NAME_PASCAL: fnNamePascal,
|
|
4631
|
-
PACKAGE_NAME: packageName,
|
|
4632
|
-
SCHEMA_VAR_NAME: schemaVarName
|
|
4633
|
-
});
|
|
4634
|
-
writeFileSync13(schemaFilePath, content);
|
|
4635
|
-
}
|
|
4636
|
-
function generateEntity(fnDir, scope, fnName, entityName) {
|
|
4637
|
-
const safeScope = toSafeSchemaName(scope);
|
|
4638
|
-
const safeFnName = toSafeSchemaName(fnName);
|
|
4639
|
-
const schemaName = `${safeScope}_${safeFnName}`;
|
|
4640
|
-
const tableName = toKebabCase(entityName);
|
|
4641
|
-
const pascalName = toPascalCase(entityName);
|
|
4642
|
-
const schemaVarName = `${toCamelCase(fnName)}Schema`;
|
|
4643
|
-
const schemaFileName = "schema";
|
|
4644
|
-
const content = loadTemplate("entity", {
|
|
4645
|
-
PASCAL_NAME: pascalName,
|
|
4646
|
-
ENTITY_NAME: entityName,
|
|
4647
|
-
TABLE_NAME: tableName,
|
|
4648
|
-
SCHEMA_NAME: schemaName,
|
|
4649
|
-
FN_NAME: fnName,
|
|
4650
|
-
SCHEMA_VAR_NAME: schemaVarName,
|
|
4651
|
-
SCHEMA_FILE_NAME: schemaFileName
|
|
4652
|
-
});
|
|
4653
|
-
writeFileSync13(
|
|
4654
|
-
join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
|
|
4655
|
-
content
|
|
4656
|
-
);
|
|
4657
|
-
}
|
|
4658
|
-
function generateEntitiesIndex(fnDir, entities) {
|
|
4659
|
-
const schemaExport = `export * from './schema';`;
|
|
4660
|
-
const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
|
|
4661
|
-
const content = [schemaExport, entityExports].filter(Boolean).join("\n");
|
|
4662
|
-
writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
|
|
4663
|
-
}
|
|
4664
|
-
|
|
4665
|
-
// src/commands/generate/generators/repository.ts
|
|
4666
|
-
import { join as join22 } from "path";
|
|
4667
|
-
import { writeFileSync as writeFileSync14 } from "fs";
|
|
4668
|
-
function generateRepository(fnDir, entityName) {
|
|
4669
|
-
const pascalName = toPascalCase(entityName);
|
|
4670
|
-
const repoName = `${entityName}Repository`;
|
|
4671
|
-
const content = loadTemplate("repository", {
|
|
4672
|
-
PASCAL_NAME: pascalName,
|
|
4673
|
-
ENTITY_NAME: entityName,
|
|
4674
|
-
REPO_NAME: repoName
|
|
4675
|
-
});
|
|
4676
|
-
writeFileSync14(
|
|
4677
|
-
join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
|
|
4678
|
-
content
|
|
4679
|
-
);
|
|
4680
|
-
}
|
|
4681
|
-
function generateRepositoriesIndex(fnDir, entities) {
|
|
4682
|
-
const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
|
|
4683
|
-
writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
|
-
// src/commands/generate/generators/route.ts
|
|
4687
|
-
import { join as join23 } from "path";
|
|
4688
|
-
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
4689
|
-
function generateRoute(fnDir, entityName) {
|
|
4690
|
-
const pascalName = toPascalCase(entityName);
|
|
4691
|
-
const repoName = `${entityName}Repository`;
|
|
4692
|
-
const kebabName = toKebabCase(entityName);
|
|
4693
|
-
const content = loadTemplate("route", {
|
|
4694
|
-
PASCAL_NAME: pascalName,
|
|
4695
|
-
ENTITY_NAME: entityName,
|
|
4696
|
-
REPO_NAME: repoName,
|
|
4697
|
-
KEBAB_NAME: kebabName
|
|
4698
|
-
});
|
|
4699
|
-
const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
|
|
4700
|
-
mkdirSync4(routeDir, { recursive: true });
|
|
4701
|
-
writeFileSync15(join23(routeDir, "index.ts"), content);
|
|
4702
|
-
}
|
|
4703
|
-
|
|
4704
|
-
// src/commands/generate/generators/contract.ts
|
|
4705
|
-
import { join as join24 } from "path";
|
|
4706
|
-
import { writeFileSync as writeFileSync16 } from "fs";
|
|
4707
|
-
function generateContract(fnDir, entityName) {
|
|
4708
|
-
const pascalName = toPascalCase(entityName);
|
|
4709
|
-
const content = loadTemplate("contract", {
|
|
4710
|
-
PASCAL_NAME: pascalName,
|
|
4711
|
-
ENTITY_NAME: entityName
|
|
4712
|
-
});
|
|
4713
|
-
writeFileSync16(
|
|
4714
|
-
join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
|
|
4715
|
-
content
|
|
4716
|
-
);
|
|
4717
|
-
}
|
|
4718
|
-
|
|
4719
|
-
// src/commands/generate/generators/index-files.ts
|
|
4720
|
-
import { join as join25 } from "path";
|
|
4721
|
-
import { writeFileSync as writeFileSync17 } from "fs";
|
|
4722
|
-
function generateMainIndex(fnDir, fnName) {
|
|
4723
|
-
const content = `/**
|
|
4724
|
-
* @spfn/${fnName}
|
|
4725
|
-
*
|
|
4726
|
-
* Common Module (Types, Entities)
|
|
4727
|
-
* \uACF5\uD1B5 \uBAA8\uB4C8 (\uD0C0\uC785, \uC5D4\uD2F0\uD2F0)
|
|
4728
|
-
*
|
|
4729
|
-
* Import structure:
|
|
4730
|
-
* - @spfn/${fnName} - Common (types, entities)
|
|
4731
|
-
* - @spfn/${fnName}/server - Server-side (routes, repositories, helpers)
|
|
4732
|
-
* - @spfn/${fnName}/client - Client-side (hooks, store, components)
|
|
4733
|
-
*/
|
|
4734
|
-
|
|
4735
|
-
// ============================================================================
|
|
4736
|
-
// Types
|
|
4737
|
-
// ============================================================================
|
|
4738
|
-
|
|
4739
|
-
export * from '@/lib/types/index';
|
|
4740
|
-
|
|
4741
|
-
// ============================================================================
|
|
4742
|
-
// Entities (for type reference)
|
|
4743
|
-
// ============================================================================
|
|
4744
|
-
|
|
4745
|
-
export * from '@/server/entities/index';
|
|
4746
|
-
`;
|
|
4747
|
-
writeFileSync17(join25(fnDir, "src/index.ts"), content);
|
|
4748
|
-
}
|
|
4749
|
-
function generateServerIndex(fnDir) {
|
|
4750
|
-
const content = `/**
|
|
4751
|
-
* @spfn/[name]/server
|
|
4752
|
-
*
|
|
4753
|
-
* Server-side Only Module
|
|
4754
|
-
* \uC11C\uBC84 \uC804\uC6A9 \uBAA8\uB4C8 (\uC11C\uBC84 \uCEF4\uD3EC\uB10C\uD2B8 + \uBC31\uC5D4\uB4DC)
|
|
4755
|
-
*
|
|
4756
|
-
* Includes:
|
|
4757
|
-
* - Routes
|
|
4758
|
-
* - Repositories (DB access)
|
|
4759
|
-
* - Helpers
|
|
4760
|
-
*
|
|
4761
|
-
* @note This module should only be imported in server-side code
|
|
4762
|
-
*/
|
|
4763
|
-
|
|
4764
|
-
// ============================================================================
|
|
4765
|
-
// Routes
|
|
4766
|
-
// ============================================================================
|
|
4767
|
-
|
|
4768
|
-
// TODO: Export routes here
|
|
4769
|
-
|
|
4770
|
-
// ============================================================================
|
|
4771
|
-
// Repositories (DB access)
|
|
4772
|
-
// ============================================================================
|
|
4773
|
-
|
|
4774
|
-
export * from '@/server/repositories/index';
|
|
4775
|
-
|
|
4776
|
-
// ============================================================================
|
|
4777
|
-
// Helpers
|
|
4778
|
-
// ============================================================================
|
|
4779
|
-
|
|
4780
|
-
// TODO: Export helpers here
|
|
4781
|
-
`;
|
|
4782
|
-
writeFileSync17(join25(fnDir, "src/server.ts"), content);
|
|
4783
|
-
}
|
|
4784
|
-
function generateClientIndex(fnDir) {
|
|
4785
|
-
const content = `/**
|
|
4786
|
-
* @spfn/[name]/client
|
|
4787
|
-
*
|
|
4788
|
-
* Client Components Only
|
|
4789
|
-
* \uD074\uB77C\uC774\uC5B8\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC804\uC6A9 (\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2E4\uD589)
|
|
4790
|
-
*
|
|
4791
|
-
* Includes:
|
|
4792
|
-
* - Hooks
|
|
4793
|
-
* - Store (Zustand)
|
|
4794
|
-
* - Components
|
|
4795
|
-
*/
|
|
4796
|
-
|
|
4797
|
-
// ============================================================================
|
|
4798
|
-
// Client-side Hooks
|
|
4799
|
-
// ============================================================================
|
|
4800
|
-
|
|
4801
|
-
export * from './client/hooks';
|
|
4802
|
-
|
|
4803
|
-
// ============================================================================
|
|
4804
|
-
// Client-side Store
|
|
4805
|
-
// ============================================================================
|
|
4806
|
-
|
|
4807
|
-
export * from './client/store';
|
|
4808
|
-
|
|
4809
|
-
// ============================================================================
|
|
4810
|
-
// Client-side Components
|
|
4811
|
-
// ============================================================================
|
|
4812
|
-
|
|
4813
|
-
export * from './client/components';
|
|
4814
|
-
`;
|
|
4815
|
-
writeFileSync17(join25(fnDir, "src/client.ts"), content);
|
|
4816
|
-
}
|
|
4817
|
-
function generateTypesFile(fnDir, fnName) {
|
|
4818
|
-
const content = `/**
|
|
4819
|
-
* @spfn/${fnName} Types
|
|
4820
|
-
*
|
|
4821
|
-
* @deprecated This file is kept for backwards compatibility.
|
|
4822
|
-
* Types are now in src/lib/types/index.ts
|
|
4823
|
-
*/
|
|
4824
|
-
|
|
4825
|
-
export * from '@/lib/types/index';
|
|
4826
|
-
`;
|
|
4827
|
-
writeFileSync17(join25(fnDir, "src/types.ts"), content);
|
|
4828
|
-
}
|
|
4829
|
-
|
|
4830
|
-
// src/commands/generate/generators/structure.ts
|
|
4831
|
-
async function generateFunctionStructure(options) {
|
|
4832
|
-
const { fnDir, scope, fnName, description, enableRoutes } = options;
|
|
4833
|
-
const entities = options.entities.filter((e) => e && e.trim().length > 0);
|
|
4834
|
-
const dirs = [
|
|
4835
|
-
// Shared layer
|
|
4836
|
-
"src/lib/contracts",
|
|
4837
|
-
"src/lib/types",
|
|
4838
|
-
// Server layer
|
|
4839
|
-
"src/server/entities",
|
|
4840
|
-
"src/server/routes",
|
|
4841
|
-
"src/server/repositories",
|
|
4842
|
-
"src/server/helpers",
|
|
4843
|
-
"src/server/generators",
|
|
4844
|
-
// Client layer
|
|
4845
|
-
"src/client/hooks",
|
|
4846
|
-
"src/client/store",
|
|
4847
|
-
"src/client/components"
|
|
4848
|
-
];
|
|
4849
|
-
dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
|
|
4850
|
-
generatePackageJson(fnDir, scope, fnName, description);
|
|
4851
|
-
generateTsConfig(fnDir);
|
|
4852
|
-
generateTsupConfig(fnDir);
|
|
4853
|
-
generateDrizzleConfig(fnDir, scope, fnName);
|
|
4854
|
-
generateExampleGenerator(fnDir, scope, fnName);
|
|
4855
|
-
generateReadme(fnDir, scope, fnName, description);
|
|
4856
|
-
if (entities.length > 0) {
|
|
4857
|
-
generateSchema(fnDir, scope, fnName);
|
|
4858
|
-
for (const entity of entities) {
|
|
4859
|
-
generateEntity(fnDir, scope, fnName, entity);
|
|
4860
|
-
generateRepository(fnDir, entity);
|
|
4861
|
-
if (enableRoutes) {
|
|
4862
|
-
generateRoute(fnDir, entity);
|
|
4863
|
-
generateContract(fnDir, entity);
|
|
4864
|
-
}
|
|
4865
|
-
}
|
|
4866
|
-
generateEntitiesIndex(fnDir, entities);
|
|
4867
|
-
generateRepositoriesIndex(fnDir, entities);
|
|
4868
|
-
} else {
|
|
4869
|
-
writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
|
|
4870
|
-
writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
|
|
4871
|
-
}
|
|
4872
|
-
writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
|
|
4873
|
-
writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
|
|
4874
|
-
writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
|
|
4875
|
-
writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
|
|
4876
|
-
writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
|
|
4877
|
-
writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
|
|
4878
|
-
generateMainIndex(fnDir, fnName);
|
|
4879
|
-
generateServerIndex(fnDir);
|
|
4880
|
-
generateClientIndex(fnDir);
|
|
4881
|
-
generateTypesFile(fnDir, fnName);
|
|
4882
|
-
}
|
|
4883
|
-
|
|
4884
|
-
// src/commands/generate.ts
|
|
4885
|
-
async function generateFunction(name, options) {
|
|
4886
|
-
const cwd = process.cwd();
|
|
4887
|
-
let scope = options.scope;
|
|
4888
|
-
if (!scope && !options.yes) {
|
|
4889
|
-
scope = await promptScope();
|
|
4890
|
-
} else if (!scope) {
|
|
4891
|
-
scope = "@spfn";
|
|
4892
|
-
}
|
|
4893
|
-
let fnName = name;
|
|
4894
|
-
if (!fnName && !options.yes) {
|
|
4895
|
-
fnName = await promptFunctionName();
|
|
4896
|
-
}
|
|
4897
|
-
if (!fnName) {
|
|
4898
|
-
logger.error("Function name is required");
|
|
4899
|
-
process.exit(1);
|
|
4900
|
-
}
|
|
4901
|
-
const fnDir = join27(cwd, fnName);
|
|
4902
|
-
if (existsSync24(fnDir)) {
|
|
4903
|
-
logger.error(`Directory ${fnName} already exists at ${fnDir}`);
|
|
4904
|
-
process.exit(1);
|
|
4905
|
-
}
|
|
4906
|
-
let description = options.description;
|
|
4907
|
-
if (!description && !options.yes) {
|
|
4908
|
-
description = await promptDescription(fnName);
|
|
4909
|
-
} else if (!description) {
|
|
4910
|
-
description = `SPFN ${fnName} function`;
|
|
4911
|
-
}
|
|
4912
|
-
let entities = [];
|
|
4913
|
-
if (options.entities) {
|
|
4914
|
-
entities = options.entities.split(",").map((e) => e.trim()).filter(Boolean);
|
|
4915
|
-
} else if (!options.yes) {
|
|
4916
|
-
entities = await promptEntities();
|
|
4917
|
-
}
|
|
4918
|
-
const enableCache = !options.skipCache;
|
|
4919
|
-
const enableRoutes = !options.skipRoutes;
|
|
4920
|
-
const config = {
|
|
4921
|
-
scope,
|
|
4922
|
-
fnName,
|
|
4923
|
-
description,
|
|
4924
|
-
entities,
|
|
4925
|
-
enableCache,
|
|
4926
|
-
enableRoutes
|
|
4927
|
-
};
|
|
4928
|
-
if (!options.yes) {
|
|
4929
|
-
const confirmed = await confirmConfiguration(config);
|
|
4930
|
-
if (!confirmed) {
|
|
4931
|
-
process.exit(0);
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
const spinner = ora12("Generating function structure...").start();
|
|
4935
|
-
try {
|
|
4936
|
-
await generateFunctionStructure({
|
|
4937
|
-
fnDir,
|
|
4938
|
-
scope,
|
|
4939
|
-
fnName,
|
|
4940
|
-
description,
|
|
4941
|
-
entities,
|
|
4942
|
-
enableCache,
|
|
4943
|
-
enableRoutes
|
|
4944
|
-
});
|
|
4945
|
-
spinner.succeed("Function structure generated");
|
|
4946
|
-
console.log("");
|
|
4947
|
-
logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
|
|
4948
|
-
`);
|
|
4949
|
-
logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
|
|
4950
|
-
console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
|
|
4951
|
-
console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
|
|
4952
|
-
console.log(` ${chalk27.gray("3.")} pnpm build`);
|
|
4953
|
-
console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
|
|
4954
|
-
console.log("");
|
|
4955
|
-
} catch (error) {
|
|
4956
|
-
spinner.fail("Failed to generate function");
|
|
4957
|
-
logger.error(String(error));
|
|
4958
|
-
process.exit(1);
|
|
4959
|
-
}
|
|
4960
|
-
}
|
|
4961
|
-
var generateCommand = new Command11("generate").alias("g").description("Generate SPFN resources");
|
|
4962
|
-
generateCommand.command("fn").description("Generate a new SPFN function module").argument("[name]", "Function name").option("-s, --scope <scope>", "NPM scope (e.g., @spfn, @mycompany)").option("-d, --description <text>", "Function description").option("-e, --entities <list>", "Comma-separated entity names").option("--skip-cache", "Skip cache generation").option("--skip-routes", "Skip route generation").option("-y, --yes", "Skip all prompts").action(generateFunction);
|
|
4963
|
-
|
|
4964
|
-
// src/commands/env.ts
|
|
4965
|
-
import { Command as Command12 } from "commander";
|
|
4966
|
-
import chalk28 from "chalk";
|
|
4967
|
-
import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
|
|
3734
|
+
import { existsSync as existsSync22, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
|
|
4968
3735
|
import { resolve as resolve2 } from "path";
|
|
4969
3736
|
import { parse } from "dotenv";
|
|
4970
3737
|
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
4971
3738
|
var BASE_ENV_FILES = {
|
|
4972
3739
|
nextjs: [".env", ".env.local"],
|
|
4973
|
-
server: [".env.server"
|
|
3740
|
+
server: [".env.server"]
|
|
4974
3741
|
};
|
|
4975
3742
|
function getEnvFilesForEnvironment(nodeEnv) {
|
|
4976
3743
|
const files = [".env"];
|
|
@@ -4984,7 +3751,6 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4984
3751
|
files.push(`.env.${nodeEnv}.local`);
|
|
4985
3752
|
}
|
|
4986
3753
|
files.push(".env.server");
|
|
4987
|
-
files.push(".env.server.local");
|
|
4988
3754
|
return files;
|
|
4989
3755
|
}
|
|
4990
3756
|
function getTargetFile(schema) {
|
|
@@ -4992,7 +3758,7 @@ function getTargetFile(schema) {
|
|
|
4992
3758
|
if (isNextjs) {
|
|
4993
3759
|
return schema.sensitive ? ".env.local" : ".env";
|
|
4994
3760
|
}
|
|
4995
|
-
return
|
|
3761
|
+
return ".env.server";
|
|
4996
3762
|
}
|
|
4997
3763
|
async function loadEnvSchema(packageName) {
|
|
4998
3764
|
try {
|
|
@@ -5012,26 +3778,26 @@ async function loadEnvSchema(packageName) {
|
|
|
5012
3778
|
}
|
|
5013
3779
|
function formatType(type) {
|
|
5014
3780
|
const typeColors = {
|
|
5015
|
-
string:
|
|
5016
|
-
number:
|
|
5017
|
-
boolean:
|
|
5018
|
-
url:
|
|
5019
|
-
enum:
|
|
5020
|
-
json:
|
|
3781
|
+
string: chalk27.green,
|
|
3782
|
+
number: chalk27.blue,
|
|
3783
|
+
boolean: chalk27.yellow,
|
|
3784
|
+
url: chalk27.cyan,
|
|
3785
|
+
enum: chalk27.magenta,
|
|
3786
|
+
json: chalk27.red
|
|
5021
3787
|
};
|
|
5022
|
-
return (typeColors[type] ||
|
|
3788
|
+
return (typeColors[type] || chalk27.white)(type);
|
|
5023
3789
|
}
|
|
5024
3790
|
function formatDefault(value, type) {
|
|
5025
3791
|
if (value === void 0) {
|
|
5026
|
-
return
|
|
3792
|
+
return chalk27.dim("(none)");
|
|
5027
3793
|
}
|
|
5028
3794
|
if (type === "string" || type === "url") {
|
|
5029
|
-
return
|
|
3795
|
+
return chalk27.green(`"${value}"`);
|
|
5030
3796
|
}
|
|
5031
3797
|
if (type === "boolean") {
|
|
5032
|
-
return value ?
|
|
3798
|
+
return value ? chalk27.green("true") : chalk27.red("false");
|
|
5033
3799
|
}
|
|
5034
|
-
return
|
|
3800
|
+
return chalk27.cyan(String(value));
|
|
5035
3801
|
}
|
|
5036
3802
|
async function listEnvVars(options) {
|
|
5037
3803
|
const packageName = options.package || "@spfn/core";
|
|
@@ -5045,28 +3811,28 @@ async function listEnvVars(options) {
|
|
|
5045
3811
|
acc[target].push([key, schema]);
|
|
5046
3812
|
return acc;
|
|
5047
3813
|
}, {});
|
|
5048
|
-
console.log(
|
|
3814
|
+
console.log(chalk27.blue.bold(`
|
|
5049
3815
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
5050
3816
|
`));
|
|
5051
3817
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5052
|
-
console.log(
|
|
3818
|
+
console.log(chalk27.bold.magenta(`
|
|
5053
3819
|
${file}`));
|
|
5054
|
-
console.log(
|
|
3820
|
+
console.log(chalk27.dim("\u2500".repeat(50)));
|
|
5055
3821
|
for (const [key, schema] of vars) {
|
|
5056
3822
|
printEnvVar(key, schema);
|
|
5057
3823
|
}
|
|
5058
3824
|
}
|
|
5059
3825
|
} else {
|
|
5060
|
-
console.log(
|
|
3826
|
+
console.log(chalk27.blue.bold(`
|
|
5061
3827
|
\u{1F4CB} Environment Variables (${packageName})
|
|
5062
3828
|
`));
|
|
5063
3829
|
for (const [key, schema] of allVars) {
|
|
5064
3830
|
printEnvVar(key, schema, true);
|
|
5065
3831
|
}
|
|
5066
3832
|
}
|
|
5067
|
-
console.log(
|
|
3833
|
+
console.log(chalk27.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
5068
3834
|
} catch (error) {
|
|
5069
|
-
console.error(
|
|
3835
|
+
console.error(chalk27.red(`
|
|
5070
3836
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5071
3837
|
`));
|
|
5072
3838
|
process.exit(1);
|
|
@@ -5074,17 +3840,17 @@ ${file}`));
|
|
|
5074
3840
|
}
|
|
5075
3841
|
function printEnvVar(key, schema, showFile = false) {
|
|
5076
3842
|
const typeStr = formatType(schema.type);
|
|
5077
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
5078
|
-
const sensitiveStr = schema.sensitive ?
|
|
5079
|
-
const fileStr = showFile ?
|
|
5080
|
-
console.log(`${
|
|
5081
|
-
console.log(` ${
|
|
3843
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
|
|
3844
|
+
const sensitiveStr = schema.sensitive ? chalk27.yellow(" [sensitive]") : "";
|
|
3845
|
+
const fileStr = showFile ? chalk27.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
3846
|
+
console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
3847
|
+
console.log(` ${chalk27.dim(schema.description)}`);
|
|
5082
3848
|
if (schema.default !== void 0) {
|
|
5083
|
-
console.log(` ${
|
|
3849
|
+
console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5084
3850
|
}
|
|
5085
3851
|
if (schema.examples && schema.examples.length > 0) {
|
|
5086
3852
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
5087
|
-
console.log(` ${
|
|
3853
|
+
console.log(` ${chalk27.dim("Examples:")} ${exampleStr}`);
|
|
5088
3854
|
}
|
|
5089
3855
|
console.log();
|
|
5090
3856
|
}
|
|
@@ -5092,7 +3858,7 @@ async function showEnvStats(options) {
|
|
|
5092
3858
|
const packageName = options.package || "@spfn/core";
|
|
5093
3859
|
try {
|
|
5094
3860
|
const envSchema = await loadEnvSchema(packageName);
|
|
5095
|
-
console.log(
|
|
3861
|
+
console.log(chalk27.blue.bold(`
|
|
5096
3862
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
5097
3863
|
`));
|
|
5098
3864
|
const allVars = Object.entries(envSchema);
|
|
@@ -5114,24 +3880,24 @@ async function showEnvStats(options) {
|
|
|
5114
3880
|
acc[file] = (acc[file] || 0) + 1;
|
|
5115
3881
|
return acc;
|
|
5116
3882
|
}, {});
|
|
5117
|
-
console.log(`${
|
|
5118
|
-
console.log(`${
|
|
5119
|
-
console.log(`${
|
|
5120
|
-
console.log(`${
|
|
5121
|
-
console.log(
|
|
5122
|
-
console.log(` ${
|
|
5123
|
-
console.log(` ${
|
|
5124
|
-
console.log(
|
|
3883
|
+
console.log(`${chalk27.bold("Total variables:")} ${chalk27.cyan(allVars.length)}`);
|
|
3884
|
+
console.log(`${chalk27.bold("Required:")} ${chalk27.red(required.length)}`);
|
|
3885
|
+
console.log(`${chalk27.bold("Optional:")} ${chalk27.dim(optional.length)}`);
|
|
3886
|
+
console.log(`${chalk27.bold("Sensitive:")} ${chalk27.yellow(sensitive.length)}`);
|
|
3887
|
+
console.log(chalk27.bold("\nBy Target:"));
|
|
3888
|
+
console.log(` ${chalk27.blue("Next.js accessible:")} ${chalk27.cyan(nextjsVars.length)}`);
|
|
3889
|
+
console.log(` ${chalk27.magenta("SPFN server only:")} ${chalk27.cyan(serverOnlyVars.length)}`);
|
|
3890
|
+
console.log(chalk27.bold("\nBy File:"));
|
|
5125
3891
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
5126
|
-
console.log(` ${
|
|
3892
|
+
console.log(` ${chalk27.dim(file)}: ${chalk27.cyan(count)}`);
|
|
5127
3893
|
}
|
|
5128
|
-
console.log(
|
|
3894
|
+
console.log(chalk27.bold("\nBy Type:"));
|
|
5129
3895
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
5130
|
-
console.log(` ${formatType(type)}: ${
|
|
3896
|
+
console.log(` ${formatType(type)}: ${chalk27.cyan(count)}`);
|
|
5131
3897
|
}
|
|
5132
3898
|
console.log();
|
|
5133
3899
|
} catch (error) {
|
|
5134
|
-
console.error(
|
|
3900
|
+
console.error(chalk27.red(`
|
|
5135
3901
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5136
3902
|
`));
|
|
5137
3903
|
process.exit(1);
|
|
@@ -5151,40 +3917,40 @@ async function searchEnvVars(query, options) {
|
|
|
5151
3917
|
}
|
|
5152
3918
|
}
|
|
5153
3919
|
if (results.length === 0) {
|
|
5154
|
-
console.log(
|
|
3920
|
+
console.log(chalk27.yellow(`
|
|
5155
3921
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
5156
3922
|
`));
|
|
5157
3923
|
return;
|
|
5158
3924
|
}
|
|
5159
|
-
console.log(
|
|
3925
|
+
console.log(chalk27.blue.bold(`
|
|
5160
3926
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
5161
3927
|
`));
|
|
5162
3928
|
for (const [key, schema] of results) {
|
|
5163
3929
|
const typeStr = formatType(schema.type);
|
|
5164
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
5165
|
-
console.log(`${
|
|
5166
|
-
console.log(` ${
|
|
3930
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
|
|
3931
|
+
console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}`);
|
|
3932
|
+
console.log(` ${chalk27.dim(schema.description)}`);
|
|
5167
3933
|
if (schema.default !== void 0) {
|
|
5168
|
-
console.log(` ${
|
|
3934
|
+
console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5169
3935
|
}
|
|
5170
3936
|
console.log();
|
|
5171
3937
|
}
|
|
5172
3938
|
} catch (error) {
|
|
5173
|
-
console.error(
|
|
3939
|
+
console.error(chalk27.red(`
|
|
5174
3940
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5175
3941
|
`));
|
|
5176
3942
|
process.exit(1);
|
|
5177
3943
|
}
|
|
5178
3944
|
}
|
|
5179
|
-
var envCommand = new
|
|
3945
|
+
var envCommand = new Command11("env").description("Manage environment variables");
|
|
5180
3946
|
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-g, --group", "Group variables by target file").action(listEnvVars);
|
|
5181
3947
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
5182
3948
|
envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(searchEnvVars);
|
|
5183
3949
|
function validateEnvOption(envValue) {
|
|
5184
3950
|
if (!VALID_ENVS.includes(envValue)) {
|
|
5185
|
-
console.error(
|
|
3951
|
+
console.error(chalk27.red(`
|
|
5186
3952
|
\u274C Invalid environment: "${envValue}"`));
|
|
5187
|
-
console.log(
|
|
3953
|
+
console.log(chalk27.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
5188
3954
|
`));
|
|
5189
3955
|
process.exit(1);
|
|
5190
3956
|
}
|
|
@@ -5205,8 +3971,8 @@ async function initEnvFiles(options) {
|
|
|
5205
3971
|
return acc;
|
|
5206
3972
|
}, {});
|
|
5207
3973
|
if (targetEnv) {
|
|
5208
|
-
console.log(
|
|
5209
|
-
\u{1F680} Generating .env template files for ${
|
|
3974
|
+
console.log(chalk27.blue.bold(`
|
|
3975
|
+
\u{1F680} Generating .env template files for ${chalk27.cyan(targetEnv)} environment
|
|
5210
3976
|
`));
|
|
5211
3977
|
const envSpecificFiles = {};
|
|
5212
3978
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -5222,25 +3988,24 @@ async function initEnvFiles(options) {
|
|
|
5222
3988
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5223
3989
|
}
|
|
5224
3990
|
} else {
|
|
5225
|
-
console.log(
|
|
3991
|
+
console.log(chalk27.blue.bold(`
|
|
5226
3992
|
\u{1F680} Generating .env template files
|
|
5227
3993
|
`));
|
|
5228
3994
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5229
3995
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5230
3996
|
}
|
|
5231
3997
|
}
|
|
5232
|
-
console.log(
|
|
5233
|
-
console.log(
|
|
5234
|
-
console.log(
|
|
5235
|
-
console.log(
|
|
5236
|
-
console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
|
|
3998
|
+
console.log(chalk27.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
3999
|
+
console.log(chalk27.dim(" cp .env.example .env"));
|
|
4000
|
+
console.log(chalk27.dim(" cp .env.local.example .env.local"));
|
|
4001
|
+
console.log(chalk27.dim(" cp .env.server.example .env.server"));
|
|
5237
4002
|
if (targetEnv) {
|
|
5238
|
-
console.log(
|
|
5239
|
-
console.log(
|
|
4003
|
+
console.log(chalk27.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
4004
|
+
console.log(chalk27.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
5240
4005
|
}
|
|
5241
4006
|
console.log("");
|
|
5242
4007
|
} catch (error) {
|
|
5243
|
-
console.error(
|
|
4008
|
+
console.error(chalk27.red(`
|
|
5244
4009
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5245
4010
|
`));
|
|
5246
4011
|
process.exit(1);
|
|
@@ -5248,12 +4013,12 @@ async function initEnvFiles(options) {
|
|
|
5248
4013
|
}
|
|
5249
4014
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5250
4015
|
const filePath = resolve2(cwd, file);
|
|
5251
|
-
if (
|
|
5252
|
-
console.log(
|
|
4016
|
+
if (existsSync22(filePath) && !force) {
|
|
4017
|
+
console.log(chalk27.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5253
4018
|
return;
|
|
5254
4019
|
}
|
|
5255
|
-
|
|
5256
|
-
console.log(
|
|
4020
|
+
writeFileSync13(filePath, generateEnvFileContent(vars), "utf-8");
|
|
4021
|
+
console.log(chalk27.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5257
4022
|
}
|
|
5258
4023
|
function generateEnvFileContent(vars) {
|
|
5259
4024
|
const lines = [
|
|
@@ -5288,7 +4053,7 @@ async function checkEnvFiles(options) {
|
|
|
5288
4053
|
const envSchema = await loadEnvSchema(packageName);
|
|
5289
4054
|
const allVars = Object.entries(envSchema);
|
|
5290
4055
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5291
|
-
console.log(
|
|
4056
|
+
console.log(chalk27.blue.bold(`
|
|
5292
4057
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
5293
4058
|
`));
|
|
5294
4059
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -5297,7 +4062,7 @@ async function checkEnvFiles(options) {
|
|
|
5297
4062
|
const warnings = [];
|
|
5298
4063
|
for (const file of filesToCheck) {
|
|
5299
4064
|
const filePath = resolve2(cwd, file);
|
|
5300
|
-
if (!
|
|
4065
|
+
if (!existsSync22(filePath)) {
|
|
5301
4066
|
continue;
|
|
5302
4067
|
}
|
|
5303
4068
|
const content = readFileSync9(filePath, "utf-8");
|
|
@@ -5305,7 +4070,7 @@ async function checkEnvFiles(options) {
|
|
|
5305
4070
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5306
4071
|
loadedEnv[key] = { value: value || "", file };
|
|
5307
4072
|
}
|
|
5308
|
-
console.log(
|
|
4073
|
+
console.log(chalk27.dim(` \u{1F4C4} ${file} loaded`));
|
|
5309
4074
|
}
|
|
5310
4075
|
console.log("");
|
|
5311
4076
|
for (const [key, schema] of allVars) {
|
|
@@ -5313,7 +4078,7 @@ async function checkEnvFiles(options) {
|
|
|
5313
4078
|
const found = loadedEnv[key];
|
|
5314
4079
|
if (!found) {
|
|
5315
4080
|
if (schema.required && schema.default === void 0) {
|
|
5316
|
-
issues.push(`${
|
|
4081
|
+
issues.push(`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is required but not found in any .env file`);
|
|
5317
4082
|
}
|
|
5318
4083
|
continue;
|
|
5319
4084
|
}
|
|
@@ -5323,11 +4088,11 @@ async function checkEnvFiles(options) {
|
|
|
5323
4088
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5324
4089
|
if (schema.sensitive) {
|
|
5325
4090
|
issues.push(
|
|
5326
|
-
`${
|
|
4091
|
+
`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is sensitive and should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.yellow(found.file)} (security risk!)`
|
|
5327
4092
|
);
|
|
5328
4093
|
} else {
|
|
5329
4094
|
warnings.push(
|
|
5330
|
-
`${
|
|
4095
|
+
`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.dim(found.file)}`
|
|
5331
4096
|
);
|
|
5332
4097
|
}
|
|
5333
4098
|
}
|
|
@@ -5335,34 +4100,34 @@ async function checkEnvFiles(options) {
|
|
|
5335
4100
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5336
4101
|
const inSchema = allVars.some(([k]) => k === key);
|
|
5337
4102
|
if (!inSchema) {
|
|
5338
|
-
warnings.push(`${
|
|
4103
|
+
warnings.push(`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} in ${chalk27.dim(file)} is not in schema`);
|
|
5339
4104
|
}
|
|
5340
4105
|
}
|
|
5341
4106
|
if (issues.length > 0) {
|
|
5342
|
-
console.log(
|
|
4107
|
+
console.log(chalk27.red.bold("Issues:"));
|
|
5343
4108
|
for (const issue of issues) {
|
|
5344
4109
|
console.log(` ${issue}`);
|
|
5345
4110
|
}
|
|
5346
4111
|
console.log("");
|
|
5347
4112
|
}
|
|
5348
4113
|
if (warnings.length > 0) {
|
|
5349
|
-
console.log(
|
|
4114
|
+
console.log(chalk27.yellow.bold("Warnings:"));
|
|
5350
4115
|
for (const warning of warnings) {
|
|
5351
4116
|
console.log(` ${warning}`);
|
|
5352
4117
|
}
|
|
5353
4118
|
console.log("");
|
|
5354
4119
|
}
|
|
5355
4120
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5356
|
-
console.log(
|
|
4121
|
+
console.log(chalk27.green("\u2705 All environment variables are correctly configured!\n"));
|
|
5357
4122
|
} else {
|
|
5358
|
-
console.log(
|
|
4123
|
+
console.log(chalk27.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
5359
4124
|
`));
|
|
5360
4125
|
if (issues.length > 0) {
|
|
5361
4126
|
process.exit(1);
|
|
5362
4127
|
}
|
|
5363
4128
|
}
|
|
5364
4129
|
} catch (error) {
|
|
5365
|
-
console.error(
|
|
4130
|
+
console.error(chalk27.red(`
|
|
5366
4131
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5367
4132
|
`));
|
|
5368
4133
|
process.exit(1);
|
|
@@ -5374,17 +4139,17 @@ async function validateEnvVars(options) {
|
|
|
5374
4139
|
const packages = options.packages || ["@spfn/core"];
|
|
5375
4140
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5376
4141
|
if (targetEnv) {
|
|
5377
|
-
const { loadEnv:
|
|
5378
|
-
const result =
|
|
5379
|
-
console.log(
|
|
5380
|
-
\u{1F50D} Validating environment variables for ${
|
|
4142
|
+
const { loadEnv: loadEnv9 } = await import("@spfn/core/env/loader");
|
|
4143
|
+
const result = loadEnv9({ nodeEnv: targetEnv });
|
|
4144
|
+
console.log(chalk27.blue.bold(`
|
|
4145
|
+
\u{1F50D} Validating environment variables for ${chalk27.cyan(targetEnv)}
|
|
5381
4146
|
`));
|
|
5382
4147
|
if (result.loadedFiles.length > 0) {
|
|
5383
|
-
console.log(
|
|
4148
|
+
console.log(chalk27.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5384
4149
|
}
|
|
5385
4150
|
console.log("");
|
|
5386
4151
|
} else {
|
|
5387
|
-
console.log(
|
|
4152
|
+
console.log(chalk27.blue.bold(`
|
|
5388
4153
|
\u{1F50D} Validating environment variables
|
|
5389
4154
|
`));
|
|
5390
4155
|
}
|
|
@@ -5392,7 +4157,7 @@ async function validateEnvVars(options) {
|
|
|
5392
4157
|
const allWarnings = [];
|
|
5393
4158
|
for (const packageName of packages) {
|
|
5394
4159
|
try {
|
|
5395
|
-
console.log(
|
|
4160
|
+
console.log(chalk27.dim(` \u{1F4E6} ${packageName}`));
|
|
5396
4161
|
const envSchema = await loadEnvSchema(packageName);
|
|
5397
4162
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5398
4163
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -5405,10 +4170,10 @@ async function validateEnvVars(options) {
|
|
|
5405
4170
|
}
|
|
5406
4171
|
} catch (error) {
|
|
5407
4172
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5408
|
-
console.log(
|
|
4173
|
+
console.log(chalk27.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5409
4174
|
continue;
|
|
5410
4175
|
}
|
|
5411
|
-
console.error(
|
|
4176
|
+
console.error(chalk27.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5412
4177
|
if (options.strict) {
|
|
5413
4178
|
process.exit(1);
|
|
5414
4179
|
}
|
|
@@ -5416,32 +4181,32 @@ async function validateEnvVars(options) {
|
|
|
5416
4181
|
}
|
|
5417
4182
|
console.log("");
|
|
5418
4183
|
if (allErrors.length > 0) {
|
|
5419
|
-
console.log(
|
|
4184
|
+
console.log(chalk27.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
5420
4185
|
`));
|
|
5421
4186
|
for (const error of allErrors) {
|
|
5422
|
-
console.log(` ${
|
|
5423
|
-
console.log(` ${
|
|
5424
|
-
console.log(` ${
|
|
4187
|
+
console.log(` ${chalk27.red("\u2717")} ${chalk27.cyan(error.key)}`);
|
|
4188
|
+
console.log(` ${chalk27.dim(error.message)}`);
|
|
4189
|
+
console.log(` ${chalk27.dim(`from ${error.package}`)}`);
|
|
5425
4190
|
console.log("");
|
|
5426
4191
|
}
|
|
5427
4192
|
}
|
|
5428
4193
|
if (allWarnings.length > 0) {
|
|
5429
|
-
console.log(
|
|
4194
|
+
console.log(chalk27.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
5430
4195
|
`));
|
|
5431
4196
|
for (const warning of allWarnings) {
|
|
5432
|
-
console.log(` ${
|
|
5433
|
-
console.log(` ${
|
|
4197
|
+
console.log(` ${chalk27.yellow("\u26A0")} ${chalk27.cyan(warning.key)}`);
|
|
4198
|
+
console.log(` ${chalk27.dim(warning.message)}`);
|
|
5434
4199
|
console.log("");
|
|
5435
4200
|
}
|
|
5436
4201
|
}
|
|
5437
4202
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
5438
|
-
console.log(
|
|
4203
|
+
console.log(chalk27.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5439
4204
|
} else if (allErrors.length === 0) {
|
|
5440
|
-
console.log(
|
|
5441
|
-
console.log(
|
|
4205
|
+
console.log(chalk27.green("\u2705 No errors found."));
|
|
4206
|
+
console.log(chalk27.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
5442
4207
|
`));
|
|
5443
4208
|
} else {
|
|
5444
|
-
console.log(
|
|
4209
|
+
console.log(chalk27.red(`
|
|
5445
4210
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
5446
4211
|
`));
|
|
5447
4212
|
process.exit(1);
|
|
@@ -5451,12 +4216,11 @@ envCommand.command("validate").description("Validate environment variables again
|
|
|
5451
4216
|
|
|
5452
4217
|
// src/index.ts
|
|
5453
4218
|
init_version();
|
|
5454
|
-
var program = new
|
|
4219
|
+
var program = new Command12();
|
|
5455
4220
|
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
5456
4221
|
program.addCommand(createCommand);
|
|
5457
4222
|
program.addCommand(initCommand);
|
|
5458
4223
|
program.addCommand(addCommand);
|
|
5459
|
-
program.addCommand(generateCommand);
|
|
5460
4224
|
program.addCommand(devCommand);
|
|
5461
4225
|
program.addCommand(buildCommand);
|
|
5462
4226
|
program.addCommand(startCommand);
|