spfn 0.2.0-beta.46 → 0.2.0-beta.48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -211
- package/dist/index.js +421 -1666
- package/dist/templates/.dockerignore +1 -0
- package/dist/templates/Dockerfile +1 -1
- package/dist/templates/server/config/env.config.ts +9 -74
- package/dist/templates/server/routes/examples.ts +1 -11
- package/dist/templates/server/routes/root.ts +0 -1
- package/package.json +2 -2
- 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.48";
|
|
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) {
|
|
@@ -2019,7 +2060,7 @@ async function initCodegen(options) {
|
|
|
2019
2060
|
]
|
|
2020
2061
|
}
|
|
2021
2062
|
};
|
|
2022
|
-
|
|
2063
|
+
writeFileSync9(rcPath, JSON.stringify(config, null, 2) + "\n");
|
|
2023
2064
|
console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2024
2065
|
console.log("Configuration:");
|
|
2025
2066
|
console.log(chalk7.gray(JSON.stringify(config, null, 2)));
|
|
@@ -2234,7 +2275,7 @@ import { Command as Command9 } from "commander";
|
|
|
2234
2275
|
import chalk10 from "chalk";
|
|
2235
2276
|
|
|
2236
2277
|
// src/commands/db/utils/drizzle.ts
|
|
2237
|
-
import { existsSync as existsSync15, writeFileSync as
|
|
2278
|
+
import { existsSync as existsSync15, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
|
|
2238
2279
|
import { spawn } from "child_process";
|
|
2239
2280
|
import { pathToFileURL } from "url";
|
|
2240
2281
|
import chalk9 from "chalk";
|
|
@@ -2269,7 +2310,7 @@ async function runDrizzleCommand(command) {
|
|
|
2269
2310
|
expandGlobs: true,
|
|
2270
2311
|
autoDetectSchemas: true
|
|
2271
2312
|
});
|
|
2272
|
-
|
|
2313
|
+
writeFileSync10(tempConfigPath, configContent);
|
|
2273
2314
|
console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
|
|
2274
2315
|
}
|
|
2275
2316
|
const args = command.split(" ");
|
|
@@ -2590,7 +2631,7 @@ async function applyFunctionMigrations() {
|
|
|
2590
2631
|
}
|
|
2591
2632
|
|
|
2592
2633
|
// src/commands/db/migrate.ts
|
|
2593
|
-
import
|
|
2634
|
+
import chalk18 from "chalk";
|
|
2594
2635
|
import { join as join16 } from "path";
|
|
2595
2636
|
import { existsSync as existsSync18 } from "fs";
|
|
2596
2637
|
|
|
@@ -2598,11 +2639,13 @@ import { existsSync as existsSync18 } from "fs";
|
|
|
2598
2639
|
import { promises as fs3 } from "fs";
|
|
2599
2640
|
import path3 from "path";
|
|
2600
2641
|
import { spawn as spawn2 } from "child_process";
|
|
2601
|
-
import
|
|
2642
|
+
import chalk17 from "chalk";
|
|
2602
2643
|
import ora7 from "ora";
|
|
2603
2644
|
|
|
2604
2645
|
// src/commands/db/utils/database.ts
|
|
2605
2646
|
import net from "net";
|
|
2647
|
+
import chalk14 from "chalk";
|
|
2648
|
+
import prompts4 from "prompts";
|
|
2606
2649
|
function parseDatabaseUrl(dbUrl) {
|
|
2607
2650
|
try {
|
|
2608
2651
|
const url = new URL(dbUrl);
|
|
@@ -2618,6 +2661,24 @@ function parseDatabaseUrl(dbUrl) {
|
|
|
2618
2661
|
throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2619
2662
|
}
|
|
2620
2663
|
}
|
|
2664
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
|
|
2665
|
+
function isRemoteHost(host) {
|
|
2666
|
+
return !LOCAL_HOSTS.has(host);
|
|
2667
|
+
}
|
|
2668
|
+
async function confirmDangerousTarget(dbInfo) {
|
|
2669
|
+
if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const { typed } = await prompts4({
|
|
2673
|
+
type: "text",
|
|
2674
|
+
name: "typed",
|
|
2675
|
+
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
2676
|
+
});
|
|
2677
|
+
if (typed !== dbInfo.database) {
|
|
2678
|
+
console.log(chalk14.gray("Cancelled (database name mismatch)."));
|
|
2679
|
+
process.exit(0);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2621
2682
|
async function isPortAvailable(port) {
|
|
2622
2683
|
return new Promise((resolve3) => {
|
|
2623
2684
|
const server = net.createServer();
|
|
@@ -2667,14 +2728,14 @@ function formatTimestamp() {
|
|
|
2667
2728
|
import { promises as fs2 } from "fs";
|
|
2668
2729
|
import { existsSync as existsSync17 } from "fs";
|
|
2669
2730
|
import path2 from "path";
|
|
2670
|
-
import
|
|
2731
|
+
import chalk16 from "chalk";
|
|
2671
2732
|
|
|
2672
2733
|
// src/commands/db/utils/metadata.ts
|
|
2673
2734
|
import { promises as fs } from "fs";
|
|
2674
2735
|
import path from "path";
|
|
2675
2736
|
import { promisify } from "util";
|
|
2676
2737
|
import { exec } from "child_process";
|
|
2677
|
-
import
|
|
2738
|
+
import chalk15 from "chalk";
|
|
2678
2739
|
var execAsync = promisify(exec);
|
|
2679
2740
|
async function collectGitInfo() {
|
|
2680
2741
|
try {
|
|
@@ -2737,7 +2798,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
2737
2798
|
await pool.end();
|
|
2738
2799
|
}
|
|
2739
2800
|
} catch (error) {
|
|
2740
|
-
console.log(
|
|
2801
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
2741
2802
|
return void 0;
|
|
2742
2803
|
}
|
|
2743
2804
|
}
|
|
@@ -2745,9 +2806,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
2745
2806
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
2746
2807
|
try {
|
|
2747
2808
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
2748
|
-
console.log(
|
|
2809
|
+
console.log(chalk15.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
2749
2810
|
} catch (error) {
|
|
2750
|
-
console.log(
|
|
2811
|
+
console.log(chalk15.dim("\u26A0\uFE0F Could not save metadata"));
|
|
2751
2812
|
}
|
|
2752
2813
|
}
|
|
2753
2814
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -2776,10 +2837,10 @@ async function ensureBackupInGitignore() {
|
|
|
2776
2837
|
if (!hasBackupsIgnore) {
|
|
2777
2838
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
2778
2839
|
await fs2.appendFile(gitignorePath, entry);
|
|
2779
|
-
console.log(
|
|
2840
|
+
console.log(chalk16.dim("\u2713 Added backups/ to .gitignore"));
|
|
2780
2841
|
}
|
|
2781
2842
|
} catch (error) {
|
|
2782
|
-
console.log(
|
|
2843
|
+
console.log(chalk16.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
2783
2844
|
}
|
|
2784
2845
|
}
|
|
2785
2846
|
async function ensureBackupDir() {
|
|
@@ -2829,12 +2890,12 @@ async function listBackupFiles() {
|
|
|
2829
2890
|
import { env as env3 } from "@spfn/core/config";
|
|
2830
2891
|
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
2831
2892
|
async function dbBackup(options) {
|
|
2832
|
-
console.log(
|
|
2893
|
+
console.log(chalk17.blue("\u{1F4BE} Creating database backup...\n"));
|
|
2833
2894
|
loadEnv4();
|
|
2834
2895
|
const dbUrl = env3.DATABASE_URL;
|
|
2835
2896
|
if (!dbUrl) {
|
|
2836
|
-
console.error(
|
|
2837
|
-
console.log(
|
|
2897
|
+
console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
|
|
2898
|
+
console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2838
2899
|
process.exit(1);
|
|
2839
2900
|
}
|
|
2840
2901
|
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
@@ -2844,7 +2905,7 @@ async function dbBackup(options) {
|
|
|
2844
2905
|
const ext = format === "sql" ? "sql" : "dump";
|
|
2845
2906
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
2846
2907
|
if (options.dataOnly && options.schemaOnly) {
|
|
2847
|
-
console.error(
|
|
2908
|
+
console.error(chalk17.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
2848
2909
|
process.exit(1);
|
|
2849
2910
|
}
|
|
2850
2911
|
const args = [
|
|
@@ -2890,11 +2951,11 @@ async function dbBackup(options) {
|
|
|
2890
2951
|
const stats = await fs3.stat(filename);
|
|
2891
2952
|
const size = formatBytes(stats.size);
|
|
2892
2953
|
spinner.succeed("Backup created");
|
|
2893
|
-
console.log(
|
|
2954
|
+
console.log(chalk17.green(`
|
|
2894
2955
|
\u2705 Backup created successfully`));
|
|
2895
|
-
console.log(
|
|
2896
|
-
console.log(
|
|
2897
|
-
console.log(
|
|
2956
|
+
console.log(chalk17.gray(` File: ${filename}`));
|
|
2957
|
+
console.log(chalk17.gray(` Size: ${size}`));
|
|
2958
|
+
console.log(chalk17.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
2898
2959
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
2899
2960
|
collectGitInfo(),
|
|
2900
2961
|
collectMigrationInfo(dbUrl)
|
|
@@ -2934,11 +2995,11 @@ async function dbBackup(options) {
|
|
|
2934
2995
|
reject(error);
|
|
2935
2996
|
});
|
|
2936
2997
|
}).catch((error) => {
|
|
2937
|
-
console.error(
|
|
2998
|
+
console.error(chalk17.red("\n\u274C Failed to create backup"));
|
|
2938
2999
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
2939
|
-
console.error(
|
|
3000
|
+
console.error(chalk17.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
2940
3001
|
} else {
|
|
2941
|
-
console.error(
|
|
3002
|
+
console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2942
3003
|
}
|
|
2943
3004
|
process.exit(1);
|
|
2944
3005
|
});
|
|
@@ -2955,7 +3016,7 @@ async function dbMigrate(options = {}) {
|
|
|
2955
3016
|
process.exit(1);
|
|
2956
3017
|
}
|
|
2957
3018
|
if (options.withBackup) {
|
|
2958
|
-
console.log(
|
|
3019
|
+
console.log(chalk18.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
2959
3020
|
await dbBackup({
|
|
2960
3021
|
format: "custom",
|
|
2961
3022
|
tag: "pre-migration",
|
|
@@ -2968,57 +3029,56 @@ async function dbMigrate(options = {}) {
|
|
|
2968
3029
|
const postgres = await import("postgres");
|
|
2969
3030
|
loadEnv5();
|
|
2970
3031
|
if (!env4.DATABASE_URL) {
|
|
2971
|
-
console.error(
|
|
3032
|
+
console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
|
|
2972
3033
|
process.exit(1);
|
|
2973
3034
|
}
|
|
2974
3035
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
2975
3036
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
2976
3037
|
if (functions.length > 0) {
|
|
2977
|
-
console.log(
|
|
3038
|
+
console.log(chalk18.blue("\u{1F4E6} Applying function package migrations:"));
|
|
2978
3039
|
functions.forEach((func) => {
|
|
2979
|
-
console.log(
|
|
3040
|
+
console.log(chalk18.dim(` - ${func.packageName}`));
|
|
2980
3041
|
});
|
|
2981
3042
|
await executeFunctionMigrations2(functions);
|
|
2982
|
-
console.log(
|
|
3043
|
+
console.log(chalk18.green("\u2705 Function migrations applied\n"));
|
|
2983
3044
|
}
|
|
2984
3045
|
const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
|
|
2985
3046
|
if (existsSync18(projectMigrationsDir)) {
|
|
2986
3047
|
const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
|
|
2987
3048
|
const projDb = drizzle(projConn);
|
|
2988
3049
|
try {
|
|
2989
|
-
console.log(
|
|
3050
|
+
console.log(chalk18.blue("\u{1F4E6} Running project migrations..."));
|
|
2990
3051
|
await migrate(projDb, {
|
|
2991
3052
|
migrationsFolder: projectMigrationsDir,
|
|
2992
3053
|
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
2993
3054
|
});
|
|
2994
|
-
console.log(
|
|
3055
|
+
console.log(chalk18.green("\u2705 Project migrations applied successfully"));
|
|
2995
3056
|
} finally {
|
|
2996
3057
|
await projConn.end();
|
|
2997
3058
|
}
|
|
2998
3059
|
} else {
|
|
2999
|
-
console.log(
|
|
3060
|
+
console.log(chalk18.dim("No project migrations found (src/server/drizzle)"));
|
|
3000
3061
|
}
|
|
3001
3062
|
}
|
|
3002
3063
|
|
|
3003
3064
|
// src/commands/db/studio.ts
|
|
3004
|
-
import
|
|
3005
|
-
import { existsSync as existsSync19, writeFileSync as
|
|
3065
|
+
import chalk19 from "chalk";
|
|
3066
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3 } from "fs";
|
|
3006
3067
|
import { spawn as spawn3 } from "child_process";
|
|
3007
3068
|
import { env as env5 } from "@spfn/core/config";
|
|
3008
|
-
import "@spfn/core/config";
|
|
3009
3069
|
async function dbStudio(requestedPort) {
|
|
3010
|
-
console.log(
|
|
3070
|
+
console.log(chalk19.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
3011
3071
|
const defaultPort = 4983;
|
|
3012
3072
|
const startPort = requestedPort || defaultPort;
|
|
3013
3073
|
let port;
|
|
3014
3074
|
try {
|
|
3015
3075
|
port = await findAvailablePort(startPort);
|
|
3016
3076
|
if (port !== startPort) {
|
|
3017
|
-
console.log(
|
|
3077
|
+
console.log(chalk19.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
3018
3078
|
`));
|
|
3019
3079
|
}
|
|
3020
3080
|
} catch (error) {
|
|
3021
|
-
console.error(
|
|
3081
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
3022
3082
|
process.exit(1);
|
|
3023
3083
|
}
|
|
3024
3084
|
const hasUserConfig = existsSync19("./drizzle.config.ts");
|
|
@@ -3027,8 +3087,8 @@ async function dbStudio(requestedPort) {
|
|
|
3027
3087
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3028
3088
|
if (!hasUserConfig) {
|
|
3029
3089
|
if (!env5.DATABASE_URL) {
|
|
3030
|
-
console.error(
|
|
3031
|
-
console.log(
|
|
3090
|
+
console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
|
|
3091
|
+
console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3032
3092
|
process.exit(1);
|
|
3033
3093
|
}
|
|
3034
3094
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -3038,8 +3098,8 @@ async function dbStudio(requestedPort) {
|
|
|
3038
3098
|
expandGlobs: true
|
|
3039
3099
|
// Expand glob patterns for Studio compatibility
|
|
3040
3100
|
});
|
|
3041
|
-
|
|
3042
|
-
console.log(
|
|
3101
|
+
writeFileSync11(tempConfigPath, configContent);
|
|
3102
|
+
console.log(chalk19.dim("Using auto-generated Drizzle config\n"));
|
|
3043
3103
|
}
|
|
3044
3104
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
3045
3105
|
stdio: "inherit",
|
|
@@ -3054,19 +3114,19 @@ async function dbStudio(requestedPort) {
|
|
|
3054
3114
|
studioProcess.on("exit", (code) => {
|
|
3055
3115
|
cleanup();
|
|
3056
3116
|
if (code !== 0 && code !== null) {
|
|
3057
|
-
console.error(
|
|
3117
|
+
console.error(chalk19.red(`
|
|
3058
3118
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
3059
3119
|
process.exit(code);
|
|
3060
3120
|
}
|
|
3061
3121
|
});
|
|
3062
3122
|
studioProcess.on("error", (error) => {
|
|
3063
3123
|
cleanup();
|
|
3064
|
-
console.error(
|
|
3065
|
-
console.error(
|
|
3124
|
+
console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
|
|
3125
|
+
console.error(chalk19.red(error.message));
|
|
3066
3126
|
process.exit(1);
|
|
3067
3127
|
});
|
|
3068
3128
|
process.on("SIGINT", () => {
|
|
3069
|
-
console.log(
|
|
3129
|
+
console.log(chalk19.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
3070
3130
|
studioProcess.kill("SIGTERM");
|
|
3071
3131
|
cleanup();
|
|
3072
3132
|
process.exit(0);
|
|
@@ -3080,27 +3140,39 @@ async function dbStudio(requestedPort) {
|
|
|
3080
3140
|
if (!hasUserConfig && existsSync19(tempConfigPath)) {
|
|
3081
3141
|
unlinkSync3(tempConfigPath);
|
|
3082
3142
|
}
|
|
3083
|
-
console.error(
|
|
3084
|
-
console.error(
|
|
3143
|
+
console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
|
|
3144
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3085
3145
|
process.exit(1);
|
|
3086
3146
|
}
|
|
3087
3147
|
}
|
|
3088
3148
|
|
|
3089
3149
|
// src/commands/db/drop.ts
|
|
3090
|
-
import
|
|
3091
|
-
import
|
|
3150
|
+
import chalk20 from "chalk";
|
|
3151
|
+
import prompts5 from "prompts";
|
|
3152
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3153
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
3092
3154
|
async function dbDrop() {
|
|
3093
|
-
|
|
3094
|
-
|
|
3155
|
+
loadEnv6();
|
|
3156
|
+
if (!env6.DATABASE_URL) {
|
|
3157
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
3158
|
+
console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3159
|
+
process.exit(1);
|
|
3160
|
+
}
|
|
3161
|
+
const dbInfo = parseDatabaseUrl(env6.DATABASE_URL);
|
|
3162
|
+
console.log(chalk20.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3163
|
+
console.log(chalk20.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
3164
|
+
`));
|
|
3165
|
+
const { confirm } = await prompts5({
|
|
3095
3166
|
type: "confirm",
|
|
3096
3167
|
name: "confirm",
|
|
3097
3168
|
message: "Are you sure you want to drop all tables?",
|
|
3098
3169
|
initial: false
|
|
3099
3170
|
});
|
|
3100
3171
|
if (!confirm) {
|
|
3101
|
-
console.log(
|
|
3172
|
+
console.log(chalk20.gray("Cancelled."));
|
|
3102
3173
|
process.exit(0);
|
|
3103
3174
|
}
|
|
3175
|
+
await confirmDangerousTarget(dbInfo);
|
|
3104
3176
|
await runWithSpinner(
|
|
3105
3177
|
"Dropping all tables...",
|
|
3106
3178
|
"drop",
|
|
@@ -3110,7 +3182,7 @@ async function dbDrop() {
|
|
|
3110
3182
|
}
|
|
3111
3183
|
|
|
3112
3184
|
// src/commands/db/check.ts
|
|
3113
|
-
import
|
|
3185
|
+
import chalk21 from "chalk";
|
|
3114
3186
|
import ora8 from "ora";
|
|
3115
3187
|
async function dbCheck() {
|
|
3116
3188
|
const spinner = ora8("Checking database connection...").start();
|
|
@@ -3119,7 +3191,7 @@ async function dbCheck() {
|
|
|
3119
3191
|
spinner.succeed("Database connection OK");
|
|
3120
3192
|
} catch (error) {
|
|
3121
3193
|
spinner.fail("Database connection failed");
|
|
3122
|
-
console.error(
|
|
3194
|
+
console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3123
3195
|
process.exit(1);
|
|
3124
3196
|
}
|
|
3125
3197
|
}
|
|
@@ -3127,28 +3199,28 @@ async function dbCheck() {
|
|
|
3127
3199
|
// src/commands/db/restore.ts
|
|
3128
3200
|
import path4 from "path";
|
|
3129
3201
|
import { spawn as spawn4 } from "child_process";
|
|
3130
|
-
import
|
|
3202
|
+
import chalk22 from "chalk";
|
|
3131
3203
|
import ora9 from "ora";
|
|
3132
|
-
import
|
|
3133
|
-
import { env as
|
|
3134
|
-
import { loadEnv as
|
|
3204
|
+
import prompts6 from "prompts";
|
|
3205
|
+
import { env as env7 } from "@spfn/core/config";
|
|
3206
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3135
3207
|
async function dbRestore(backupFile, options = {}) {
|
|
3136
|
-
console.log(
|
|
3137
|
-
|
|
3138
|
-
const dbUrl =
|
|
3208
|
+
console.log(chalk22.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
3209
|
+
loadEnv7();
|
|
3210
|
+
const dbUrl = env7.DATABASE_URL;
|
|
3139
3211
|
if (!dbUrl) {
|
|
3140
|
-
console.error(
|
|
3141
|
-
console.log(
|
|
3212
|
+
console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
|
|
3213
|
+
console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3142
3214
|
process.exit(1);
|
|
3143
3215
|
}
|
|
3144
3216
|
let file = backupFile;
|
|
3145
3217
|
if (!file) {
|
|
3146
3218
|
const backups = await listBackupFiles();
|
|
3147
3219
|
if (backups.length === 0) {
|
|
3148
|
-
console.log(
|
|
3220
|
+
console.log(chalk22.yellow("No backups found in ./backups directory"));
|
|
3149
3221
|
process.exit(0);
|
|
3150
3222
|
}
|
|
3151
|
-
const { selected } = await
|
|
3223
|
+
const { selected } = await prompts6({
|
|
3152
3224
|
type: "select",
|
|
3153
3225
|
name: "selected",
|
|
3154
3226
|
message: "Select backup to restore:",
|
|
@@ -3158,31 +3230,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3158
3230
|
}))
|
|
3159
3231
|
});
|
|
3160
3232
|
if (!selected) {
|
|
3161
|
-
console.log(
|
|
3233
|
+
console.log(chalk22.gray("Cancelled"));
|
|
3162
3234
|
process.exit(0);
|
|
3163
3235
|
}
|
|
3164
3236
|
file = selected;
|
|
3165
3237
|
}
|
|
3166
3238
|
if (!file) {
|
|
3167
|
-
console.error(
|
|
3239
|
+
console.error(chalk22.red("\u274C No backup file selected"));
|
|
3168
3240
|
process.exit(1);
|
|
3169
3241
|
}
|
|
3170
3242
|
const metadata = await loadBackupMetadata(file);
|
|
3171
3243
|
if (metadata) {
|
|
3172
|
-
console.log(
|
|
3173
|
-
console.log(
|
|
3174
|
-
console.log(
|
|
3244
|
+
console.log(chalk22.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
3245
|
+
console.log(chalk22.dim(` Database: ${metadata.database}`));
|
|
3246
|
+
console.log(chalk22.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
3175
3247
|
if (metadata.environment) {
|
|
3176
|
-
console.log(
|
|
3248
|
+
console.log(chalk22.dim(` Environment: ${metadata.environment}`));
|
|
3177
3249
|
}
|
|
3178
3250
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3179
|
-
console.log(
|
|
3251
|
+
console.log(chalk22.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
3180
3252
|
}
|
|
3181
3253
|
if (metadata.backup.dataOnly) {
|
|
3182
|
-
console.log(
|
|
3254
|
+
console.log(chalk22.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
3183
3255
|
}
|
|
3184
3256
|
if (metadata.backup.schemaOnly) {
|
|
3185
|
-
console.log(
|
|
3257
|
+
console.log(chalk22.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
3186
3258
|
}
|
|
3187
3259
|
const warnings2 = [];
|
|
3188
3260
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
@@ -3205,26 +3277,27 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3205
3277
|
}
|
|
3206
3278
|
}
|
|
3207
3279
|
if (warnings2.length > 0) {
|
|
3208
|
-
console.log(
|
|
3209
|
-
warnings2.forEach((warning) => console.log(
|
|
3280
|
+
console.log(chalk22.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
3281
|
+
warnings2.forEach((warning) => console.log(chalk22.yellow(` - ${warning}`)));
|
|
3210
3282
|
console.log("");
|
|
3211
3283
|
}
|
|
3212
3284
|
}
|
|
3213
|
-
const
|
|
3285
|
+
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
3286
|
+
const { confirm } = await prompts6({
|
|
3214
3287
|
type: "confirm",
|
|
3215
3288
|
name: "confirm",
|
|
3216
|
-
message:
|
|
3289
|
+
message: chalk22.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
3217
3290
|
initial: false
|
|
3218
3291
|
});
|
|
3219
3292
|
if (!confirm) {
|
|
3220
|
-
console.log(
|
|
3293
|
+
console.log(chalk22.gray("Cancelled"));
|
|
3221
3294
|
process.exit(0);
|
|
3222
3295
|
}
|
|
3296
|
+
await confirmDangerousTarget(dbInfo);
|
|
3223
3297
|
if (options.dataOnly && options.schemaOnly) {
|
|
3224
|
-
console.error(
|
|
3298
|
+
console.error(chalk22.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3225
3299
|
process.exit(1);
|
|
3226
3300
|
}
|
|
3227
|
-
const dbInfo = parseDatabaseUrl(dbUrl);
|
|
3228
3301
|
const ext = path4.extname(file);
|
|
3229
3302
|
const isCustomFormat = ext === ".dump";
|
|
3230
3303
|
const command = isCustomFormat ? "pg_restore" : "psql";
|
|
@@ -3236,7 +3309,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3236
3309
|
args.push("-d", dbInfo.database);
|
|
3237
3310
|
args.push("--verbose");
|
|
3238
3311
|
if (options.drop) {
|
|
3239
|
-
args.push("--clean");
|
|
3312
|
+
args.push("--clean", "--if-exists");
|
|
3240
3313
|
}
|
|
3241
3314
|
if (options.schema) {
|
|
3242
3315
|
args.push("-n", options.schema);
|
|
@@ -3249,9 +3322,12 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3249
3322
|
}
|
|
3250
3323
|
args.push(file);
|
|
3251
3324
|
} else {
|
|
3325
|
+
if (options.drop) {
|
|
3326
|
+
console.log(chalk22.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
|
|
3327
|
+
}
|
|
3252
3328
|
if (options.dataOnly || options.schemaOnly) {
|
|
3253
|
-
console.log(
|
|
3254
|
-
console.log(
|
|
3329
|
+
console.log(chalk22.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
3330
|
+
console.log(chalk22.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
3255
3331
|
}
|
|
3256
3332
|
args.push("-h", dbInfo.host);
|
|
3257
3333
|
args.push("-p", dbInfo.port);
|
|
@@ -3272,7 +3348,6 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3272
3348
|
const warnings = [];
|
|
3273
3349
|
const errors = [];
|
|
3274
3350
|
let objectCount = 0;
|
|
3275
|
-
let lastObject = "";
|
|
3276
3351
|
restoreProcess.stderr?.on("data", (data) => {
|
|
3277
3352
|
const lines = data.toString().split("\n").filter((l) => l.trim());
|
|
3278
3353
|
for (const line of lines) {
|
|
@@ -3286,18 +3361,16 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3286
3361
|
objectCount = Number(objectMatch[2]);
|
|
3287
3362
|
const current = Number(objectMatch[1]);
|
|
3288
3363
|
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3289
|
-
lastObject = desc;
|
|
3290
3364
|
spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
|
|
3291
3365
|
} else if (isCustomFormat) {
|
|
3292
3366
|
const desc = line.replace(/^pg_restore:\s*/, "").trim();
|
|
3293
3367
|
if (desc && !/warning:|error:/i.test(desc)) {
|
|
3294
|
-
lastObject = desc;
|
|
3295
3368
|
spinner.text = `Restoring backup... ${desc}`;
|
|
3296
3369
|
}
|
|
3297
3370
|
}
|
|
3298
3371
|
if (verbose) {
|
|
3299
3372
|
spinner.stop();
|
|
3300
|
-
console.log(
|
|
3373
|
+
console.log(chalk22.dim(` ${line.trim()}`));
|
|
3301
3374
|
spinner.start();
|
|
3302
3375
|
}
|
|
3303
3376
|
}
|
|
@@ -3305,7 +3378,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3305
3378
|
restoreProcess.stdout?.on("data", (data) => {
|
|
3306
3379
|
if (verbose) {
|
|
3307
3380
|
spinner.stop();
|
|
3308
|
-
console.log(
|
|
3381
|
+
console.log(chalk22.dim(` ${data.toString().trim()}`));
|
|
3309
3382
|
spinner.start();
|
|
3310
3383
|
}
|
|
3311
3384
|
});
|
|
@@ -3315,31 +3388,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3315
3388
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
3316
3389
|
spinner.succeed(`Restore completed${summary}`);
|
|
3317
3390
|
if (warnings.length > 0) {
|
|
3318
|
-
console.log(
|
|
3391
|
+
console.log(chalk22.yellow(`
|
|
3319
3392
|
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
3320
3393
|
`));
|
|
3321
3394
|
for (const w of warnings) {
|
|
3322
|
-
console.log(
|
|
3395
|
+
console.log(chalk22.yellow(` - ${w}`));
|
|
3323
3396
|
}
|
|
3324
3397
|
}
|
|
3325
|
-
console.log(
|
|
3398
|
+
console.log(chalk22.green("\n\u2705 Database restored successfully"));
|
|
3326
3399
|
resolve3();
|
|
3327
3400
|
} else {
|
|
3328
3401
|
spinner.fail("Restore failed");
|
|
3329
3402
|
if (errors.length > 0) {
|
|
3330
|
-
console.error(
|
|
3403
|
+
console.error(chalk22.red(`
|
|
3331
3404
|
\u274C Errors (${errors.length}):
|
|
3332
3405
|
`));
|
|
3333
3406
|
for (const e of errors) {
|
|
3334
|
-
console.error(
|
|
3407
|
+
console.error(chalk22.red(` - ${e}`));
|
|
3335
3408
|
}
|
|
3336
3409
|
}
|
|
3337
3410
|
if (warnings.length > 0) {
|
|
3338
|
-
console.log(
|
|
3411
|
+
console.log(chalk22.yellow(`
|
|
3339
3412
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
3340
3413
|
`));
|
|
3341
3414
|
for (const w of warnings) {
|
|
3342
|
-
console.log(
|
|
3415
|
+
console.log(chalk22.yellow(` - ${w}`));
|
|
3343
3416
|
}
|
|
3344
3417
|
}
|
|
3345
3418
|
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
@@ -3353,7 +3426,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3353
3426
|
}).catch((error) => {
|
|
3354
3427
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
3355
3428
|
if (msg) {
|
|
3356
|
-
console.error(
|
|
3429
|
+
console.error(chalk22.red(`
|
|
3357
3430
|
\u274C ${msg}`));
|
|
3358
3431
|
}
|
|
3359
3432
|
process.exit(1);
|
|
@@ -3361,17 +3434,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3361
3434
|
}
|
|
3362
3435
|
|
|
3363
3436
|
// src/commands/db/list.ts
|
|
3364
|
-
import
|
|
3437
|
+
import chalk23 from "chalk";
|
|
3365
3438
|
async function dbBackupList() {
|
|
3366
|
-
console.log(
|
|
3439
|
+
console.log(chalk23.blue("\u{1F4CB} Database backups:\n"));
|
|
3367
3440
|
const backups = await listBackupFiles();
|
|
3368
3441
|
if (backups.length === 0) {
|
|
3369
|
-
console.log(
|
|
3370
|
-
console.log(
|
|
3442
|
+
console.log(chalk23.yellow("No backups found in ./backups directory"));
|
|
3443
|
+
console.log(chalk23.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
3371
3444
|
return;
|
|
3372
3445
|
}
|
|
3373
|
-
console.log(
|
|
3374
|
-
console.log(
|
|
3446
|
+
console.log(chalk23.bold(" Date Size File"));
|
|
3447
|
+
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
3448
|
backups.forEach((backup) => {
|
|
3376
3449
|
const date = backup.date.toLocaleString("en-US", {
|
|
3377
3450
|
year: "numeric",
|
|
@@ -3382,23 +3455,23 @@ async function dbBackupList() {
|
|
|
3382
3455
|
second: "2-digit"
|
|
3383
3456
|
});
|
|
3384
3457
|
const sizeStr = backup.size.padEnd(10);
|
|
3385
|
-
console.log(
|
|
3458
|
+
console.log(chalk23.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
3386
3459
|
});
|
|
3387
|
-
console.log(
|
|
3460
|
+
console.log(chalk23.gray(`
|
|
3388
3461
|
Total: ${backups.length} backup(s)
|
|
3389
3462
|
`));
|
|
3390
3463
|
}
|
|
3391
3464
|
|
|
3392
3465
|
// src/commands/db/clean.ts
|
|
3393
3466
|
import { promises as fs4 } from "fs";
|
|
3394
|
-
import
|
|
3467
|
+
import chalk24 from "chalk";
|
|
3395
3468
|
import ora10 from "ora";
|
|
3396
|
-
import
|
|
3469
|
+
import prompts7 from "prompts";
|
|
3397
3470
|
async function dbBackupClean(options) {
|
|
3398
|
-
console.log(
|
|
3471
|
+
console.log(chalk24.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
3399
3472
|
const backups = await listBackupFiles();
|
|
3400
3473
|
if (backups.length === 0) {
|
|
3401
|
-
console.log(
|
|
3474
|
+
console.log(chalk24.yellow("No backups found"));
|
|
3402
3475
|
return;
|
|
3403
3476
|
}
|
|
3404
3477
|
let toDelete = [];
|
|
@@ -3415,42 +3488,46 @@ async function dbBackupClean(options) {
|
|
|
3415
3488
|
toDelete = backups.slice(defaultKeep);
|
|
3416
3489
|
}
|
|
3417
3490
|
if (toDelete.length === 0) {
|
|
3418
|
-
console.log(
|
|
3491
|
+
console.log(chalk24.green("\u2705 No backups to clean"));
|
|
3419
3492
|
return;
|
|
3420
3493
|
}
|
|
3421
|
-
console.log(
|
|
3494
|
+
console.log(chalk24.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
3422
3495
|
`));
|
|
3423
3496
|
toDelete.forEach((backup) => {
|
|
3424
|
-
console.log(
|
|
3497
|
+
console.log(chalk24.gray(` - ${backup.name} (${backup.size})`));
|
|
3425
3498
|
});
|
|
3426
|
-
const { confirm } = await
|
|
3499
|
+
const { confirm } = await prompts7({
|
|
3427
3500
|
type: "confirm",
|
|
3428
3501
|
name: "confirm",
|
|
3429
3502
|
message: "\nProceed with deletion?",
|
|
3430
3503
|
initial: false
|
|
3431
3504
|
});
|
|
3432
3505
|
if (!confirm) {
|
|
3433
|
-
console.log(
|
|
3506
|
+
console.log(chalk24.gray("Cancelled"));
|
|
3434
3507
|
return;
|
|
3435
3508
|
}
|
|
3436
3509
|
const spinner = ora10("Deleting backups...").start();
|
|
3437
3510
|
try {
|
|
3438
|
-
await Promise.all(toDelete.
|
|
3511
|
+
await Promise.all(toDelete.flatMap((backup) => [
|
|
3512
|
+
fs4.unlink(backup.path),
|
|
3513
|
+
// Remove the .meta.json sidecar too (force: ok if it doesn't exist)
|
|
3514
|
+
fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
|
|
3515
|
+
]));
|
|
3439
3516
|
spinner.succeed("Backups deleted");
|
|
3440
|
-
console.log(
|
|
3517
|
+
console.log(chalk24.green(`
|
|
3441
3518
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
3442
3519
|
} catch (error) {
|
|
3443
3520
|
spinner.fail("Failed to delete backups");
|
|
3444
|
-
console.error(
|
|
3521
|
+
console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3445
3522
|
process.exit(1);
|
|
3446
3523
|
}
|
|
3447
3524
|
}
|
|
3448
3525
|
|
|
3449
3526
|
// src/commands/db/reindex.ts
|
|
3450
|
-
import { existsSync as existsSync20, readFileSync as
|
|
3527
|
+
import { existsSync as existsSync20, readFileSync as readFileSync7, writeFileSync as writeFileSync12, renameSync, copyFileSync } from "fs";
|
|
3451
3528
|
import { join as join17 } from "path";
|
|
3452
|
-
import
|
|
3453
|
-
import { loadEnv as
|
|
3529
|
+
import chalk25 from "chalk";
|
|
3530
|
+
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
3454
3531
|
function isTimestampPrefix(tag) {
|
|
3455
3532
|
const prefix = tag.split("_")[0];
|
|
3456
3533
|
return /^\d{5,}$/.test(prefix);
|
|
@@ -3466,19 +3543,19 @@ function parseTag(tag) {
|
|
|
3466
3543
|
};
|
|
3467
3544
|
}
|
|
3468
3545
|
async function dbReindex(options = {}) {
|
|
3469
|
-
|
|
3546
|
+
loadEnv8();
|
|
3470
3547
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3471
3548
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
3472
3549
|
const outDir = config.out;
|
|
3473
3550
|
const journalPath = join17(outDir, "meta", "_journal.json");
|
|
3474
3551
|
if (!existsSync20(journalPath)) {
|
|
3475
|
-
console.error(
|
|
3476
|
-
console.log(
|
|
3552
|
+
console.error(chalk25.red("\u274C No _journal.json found at:"), journalPath);
|
|
3553
|
+
console.log(chalk25.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
3477
3554
|
process.exit(1);
|
|
3478
3555
|
}
|
|
3479
|
-
const journal = JSON.parse(
|
|
3556
|
+
const journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
|
|
3480
3557
|
if (journal.entries.length === 0) {
|
|
3481
|
-
console.log(
|
|
3558
|
+
console.log(chalk25.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
3482
3559
|
return;
|
|
3483
3560
|
}
|
|
3484
3561
|
const renames = [];
|
|
@@ -3505,33 +3582,33 @@ async function dbReindex(options = {}) {
|
|
|
3505
3582
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
3506
3583
|
}
|
|
3507
3584
|
if (tagUpdates.length === 0) {
|
|
3508
|
-
console.log(
|
|
3585
|
+
console.log(chalk25.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
3509
3586
|
if (skipped > 0) {
|
|
3510
|
-
console.log(
|
|
3587
|
+
console.log(chalk25.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
3511
3588
|
}
|
|
3512
3589
|
return;
|
|
3513
3590
|
}
|
|
3514
|
-
console.log(
|
|
3591
|
+
console.log(chalk25.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
3515
3592
|
for (const update of tagUpdates) {
|
|
3516
3593
|
console.log(
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3594
|
+
chalk25.dim(` [${update.idx}]`),
|
|
3595
|
+
chalk25.red(update.oldTag),
|
|
3596
|
+
chalk25.dim("\u2192"),
|
|
3597
|
+
chalk25.green(update.newTag)
|
|
3521
3598
|
);
|
|
3522
3599
|
}
|
|
3523
|
-
console.log(
|
|
3600
|
+
console.log(chalk25.dim(`
|
|
3524
3601
|
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
3525
3602
|
if (skipped > 0) {
|
|
3526
|
-
console.log(
|
|
3603
|
+
console.log(chalk25.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
3527
3604
|
}
|
|
3528
3605
|
if (options.dryRun) {
|
|
3529
|
-
console.log(
|
|
3606
|
+
console.log(chalk25.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
3530
3607
|
return;
|
|
3531
3608
|
}
|
|
3532
3609
|
const backupPath = journalPath + ".bak";
|
|
3533
3610
|
copyFileSync(journalPath, backupPath);
|
|
3534
|
-
console.log(
|
|
3611
|
+
console.log(chalk25.dim(`
|
|
3535
3612
|
Backed up journal \u2192 ${backupPath}`));
|
|
3536
3613
|
for (const rename of renames) {
|
|
3537
3614
|
renameSync(rename.from, rename.to);
|
|
@@ -3542,8 +3619,8 @@ async function dbReindex(options = {}) {
|
|
|
3542
3619
|
entry.tag = update.newTag;
|
|
3543
3620
|
}
|
|
3544
3621
|
}
|
|
3545
|
-
|
|
3546
|
-
console.log(
|
|
3622
|
+
writeFileSync12(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
3623
|
+
console.log(chalk25.green(`
|
|
3547
3624
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
3548
3625
|
}
|
|
3549
3626
|
|
|
@@ -3562,1415 +3639,96 @@ dbCommand.command("backup:clean").description("Clean old database backups").opti
|
|
|
3562
3639
|
dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
|
|
3563
3640
|
|
|
3564
3641
|
// src/commands/add.ts
|
|
3642
|
+
init_package_manager();
|
|
3565
3643
|
import { Command as Command10 } from "commander";
|
|
3566
|
-
import { existsSync as existsSync21, readFileSync as
|
|
3644
|
+
import { existsSync as existsSync21, readFileSync as readFileSync8 } from "fs";
|
|
3567
3645
|
import { join as join18 } from "path";
|
|
3568
|
-
import {
|
|
3569
|
-
import
|
|
3570
|
-
import chalk25 from "chalk";
|
|
3646
|
+
import { execa as execa7 } from "execa";
|
|
3647
|
+
import chalk26 from "chalk";
|
|
3571
3648
|
import ora11 from "ora";
|
|
3572
|
-
var execAsync2 = promisify2(exec2);
|
|
3573
3649
|
async function addPackage(packageName) {
|
|
3574
3650
|
if (!packageName.includes("/")) {
|
|
3575
|
-
console.error(
|
|
3576
|
-
console.log(
|
|
3577
|
-
console.log(
|
|
3578
|
-
console.log(
|
|
3651
|
+
console.error(chalk26.red("\u274C Please specify full package name"));
|
|
3652
|
+
console.log(chalk26.yellow("\n\u{1F4A1} Examples:"));
|
|
3653
|
+
console.log(chalk26.gray(" pnpm spfn add @spfn/cms"));
|
|
3654
|
+
console.log(chalk26.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
3579
3655
|
process.exit(1);
|
|
3580
3656
|
}
|
|
3581
|
-
console.log(
|
|
3657
|
+
console.log(chalk26.blue(`
|
|
3582
3658
|
\u{1F4E6} Setting up ${packageName}...
|
|
3583
3659
|
`));
|
|
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`);
|
|
3660
|
+
const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
3661
|
+
const pkgJsonPath = join18(pkgPath, "package.json");
|
|
3662
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
3663
|
+
const pm = detectPackageManager(process.cwd());
|
|
3664
|
+
const installSpinner = ora11("Installing package...").start();
|
|
3665
|
+
try {
|
|
3666
|
+
await execa7(pm, ["add", packageName]);
|
|
3667
|
+
installSpinner.succeed("Package installed");
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
installSpinner.fail("Failed to install package");
|
|
3670
|
+
console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3671
|
+
process.exit(1);
|
|
3601
3672
|
}
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3673
|
+
} else {
|
|
3674
|
+
console.log(chalk26.gray("\u2713 Package already installed (using local version)\n"));
|
|
3675
|
+
}
|
|
3676
|
+
if (!existsSync21(pkgJsonPath)) {
|
|
3677
|
+
console.error(chalk26.red(`\u274C Package ${packageName} not found after installation`));
|
|
3678
|
+
process.exit(1);
|
|
3679
|
+
}
|
|
3680
|
+
const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
|
|
3681
|
+
if (pkgJson.spfn?.migrations) {
|
|
3682
|
+
console.log(chalk26.blue(`
|
|
3605
3683
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
3606
3684
|
`));
|
|
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"));
|
|
3685
|
+
const { env: env8 } = await import("@spfn/core/config");
|
|
3686
|
+
if (!env8.DATABASE_URL) {
|
|
3687
|
+
console.log(chalk26.yellow("\u26A0\uFE0F DATABASE_URL not found"));
|
|
3688
|
+
console.log(chalk26.gray("Skipping database setup. Run migrations manually when ready:\n"));
|
|
3689
|
+
console.log(chalk26.gray(" pnpm spfn db push\n"));
|
|
3690
|
+
} else {
|
|
3691
|
+
const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
3692
|
+
const functions = discoverFunctionMigrations2(process.cwd());
|
|
3693
|
+
const targetFunction = functions.find((f) => f.packageName === packageName);
|
|
3694
|
+
if (targetFunction) {
|
|
3695
|
+
const spinner = ora11("Applying migrations...").start();
|
|
3696
|
+
try {
|
|
3697
|
+
await executeFunctionMigrations2([targetFunction]);
|
|
3698
|
+
spinner.succeed("Migrations applied");
|
|
3699
|
+
} catch (error) {
|
|
3700
|
+
spinner.fail("Failed to apply migrations");
|
|
3701
|
+
console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3702
|
+
process.exit(1);
|
|
3628
3703
|
}
|
|
3704
|
+
} else {
|
|
3705
|
+
console.log(chalk26.gray("\u2139\uFE0F No migrations found for this package"));
|
|
3629
3706
|
}
|
|
3630
|
-
} else {
|
|
3631
|
-
console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
3632
3707
|
}
|
|
3633
|
-
|
|
3708
|
+
} else {
|
|
3709
|
+
console.log(chalk26.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
3710
|
+
}
|
|
3711
|
+
console.log(chalk26.green(`
|
|
3634
3712
|
\u2705 ${packageName} installed successfully!
|
|
3635
3713
|
`));
|
|
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);
|
|
3714
|
+
if (pkgJson.spfn?.setupMessage) {
|
|
3715
|
+
console.log(chalk26.cyan("\u{1F4DA} Setup Guide:"));
|
|
3716
|
+
console.log(pkgJson.spfn.setupMessage);
|
|
3717
|
+
console.log();
|
|
3647
3718
|
}
|
|
3648
3719
|
}
|
|
3649
3720
|
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
3721
|
|
|
3651
|
-
// src/commands/
|
|
3652
|
-
init_logger();
|
|
3722
|
+
// src/commands/env.ts
|
|
3653
3723
|
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
3724
|
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";
|
|
3725
|
+
import { existsSync as existsSync22, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
|
|
4968
3726
|
import { resolve as resolve2 } from "path";
|
|
4969
3727
|
import { parse } from "dotenv";
|
|
4970
3728
|
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
4971
3729
|
var BASE_ENV_FILES = {
|
|
4972
3730
|
nextjs: [".env", ".env.local"],
|
|
4973
|
-
server: [".env.server"
|
|
3731
|
+
server: [".env.server"]
|
|
4974
3732
|
};
|
|
4975
3733
|
function getEnvFilesForEnvironment(nodeEnv) {
|
|
4976
3734
|
const files = [".env"];
|
|
@@ -4984,7 +3742,6 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4984
3742
|
files.push(`.env.${nodeEnv}.local`);
|
|
4985
3743
|
}
|
|
4986
3744
|
files.push(".env.server");
|
|
4987
|
-
files.push(".env.server.local");
|
|
4988
3745
|
return files;
|
|
4989
3746
|
}
|
|
4990
3747
|
function getTargetFile(schema) {
|
|
@@ -4992,7 +3749,7 @@ function getTargetFile(schema) {
|
|
|
4992
3749
|
if (isNextjs) {
|
|
4993
3750
|
return schema.sensitive ? ".env.local" : ".env";
|
|
4994
3751
|
}
|
|
4995
|
-
return
|
|
3752
|
+
return ".env.server";
|
|
4996
3753
|
}
|
|
4997
3754
|
async function loadEnvSchema(packageName) {
|
|
4998
3755
|
try {
|
|
@@ -5012,26 +3769,26 @@ async function loadEnvSchema(packageName) {
|
|
|
5012
3769
|
}
|
|
5013
3770
|
function formatType(type) {
|
|
5014
3771
|
const typeColors = {
|
|
5015
|
-
string:
|
|
5016
|
-
number:
|
|
5017
|
-
boolean:
|
|
5018
|
-
url:
|
|
5019
|
-
enum:
|
|
5020
|
-
json:
|
|
3772
|
+
string: chalk27.green,
|
|
3773
|
+
number: chalk27.blue,
|
|
3774
|
+
boolean: chalk27.yellow,
|
|
3775
|
+
url: chalk27.cyan,
|
|
3776
|
+
enum: chalk27.magenta,
|
|
3777
|
+
json: chalk27.red
|
|
5021
3778
|
};
|
|
5022
|
-
return (typeColors[type] ||
|
|
3779
|
+
return (typeColors[type] || chalk27.white)(type);
|
|
5023
3780
|
}
|
|
5024
3781
|
function formatDefault(value, type) {
|
|
5025
3782
|
if (value === void 0) {
|
|
5026
|
-
return
|
|
3783
|
+
return chalk27.dim("(none)");
|
|
5027
3784
|
}
|
|
5028
3785
|
if (type === "string" || type === "url") {
|
|
5029
|
-
return
|
|
3786
|
+
return chalk27.green(`"${value}"`);
|
|
5030
3787
|
}
|
|
5031
3788
|
if (type === "boolean") {
|
|
5032
|
-
return value ?
|
|
3789
|
+
return value ? chalk27.green("true") : chalk27.red("false");
|
|
5033
3790
|
}
|
|
5034
|
-
return
|
|
3791
|
+
return chalk27.cyan(String(value));
|
|
5035
3792
|
}
|
|
5036
3793
|
async function listEnvVars(options) {
|
|
5037
3794
|
const packageName = options.package || "@spfn/core";
|
|
@@ -5045,28 +3802,28 @@ async function listEnvVars(options) {
|
|
|
5045
3802
|
acc[target].push([key, schema]);
|
|
5046
3803
|
return acc;
|
|
5047
3804
|
}, {});
|
|
5048
|
-
console.log(
|
|
3805
|
+
console.log(chalk27.blue.bold(`
|
|
5049
3806
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
5050
3807
|
`));
|
|
5051
3808
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5052
|
-
console.log(
|
|
3809
|
+
console.log(chalk27.bold.magenta(`
|
|
5053
3810
|
${file}`));
|
|
5054
|
-
console.log(
|
|
3811
|
+
console.log(chalk27.dim("\u2500".repeat(50)));
|
|
5055
3812
|
for (const [key, schema] of vars) {
|
|
5056
3813
|
printEnvVar(key, schema);
|
|
5057
3814
|
}
|
|
5058
3815
|
}
|
|
5059
3816
|
} else {
|
|
5060
|
-
console.log(
|
|
3817
|
+
console.log(chalk27.blue.bold(`
|
|
5061
3818
|
\u{1F4CB} Environment Variables (${packageName})
|
|
5062
3819
|
`));
|
|
5063
3820
|
for (const [key, schema] of allVars) {
|
|
5064
3821
|
printEnvVar(key, schema, true);
|
|
5065
3822
|
}
|
|
5066
3823
|
}
|
|
5067
|
-
console.log(
|
|
3824
|
+
console.log(chalk27.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
5068
3825
|
} catch (error) {
|
|
5069
|
-
console.error(
|
|
3826
|
+
console.error(chalk27.red(`
|
|
5070
3827
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5071
3828
|
`));
|
|
5072
3829
|
process.exit(1);
|
|
@@ -5074,17 +3831,17 @@ ${file}`));
|
|
|
5074
3831
|
}
|
|
5075
3832
|
function printEnvVar(key, schema, showFile = false) {
|
|
5076
3833
|
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(` ${
|
|
3834
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
|
|
3835
|
+
const sensitiveStr = schema.sensitive ? chalk27.yellow(" [sensitive]") : "";
|
|
3836
|
+
const fileStr = showFile ? chalk27.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
3837
|
+
console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
3838
|
+
console.log(` ${chalk27.dim(schema.description)}`);
|
|
5082
3839
|
if (schema.default !== void 0) {
|
|
5083
|
-
console.log(` ${
|
|
3840
|
+
console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5084
3841
|
}
|
|
5085
3842
|
if (schema.examples && schema.examples.length > 0) {
|
|
5086
3843
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
5087
|
-
console.log(` ${
|
|
3844
|
+
console.log(` ${chalk27.dim("Examples:")} ${exampleStr}`);
|
|
5088
3845
|
}
|
|
5089
3846
|
console.log();
|
|
5090
3847
|
}
|
|
@@ -5092,7 +3849,7 @@ async function showEnvStats(options) {
|
|
|
5092
3849
|
const packageName = options.package || "@spfn/core";
|
|
5093
3850
|
try {
|
|
5094
3851
|
const envSchema = await loadEnvSchema(packageName);
|
|
5095
|
-
console.log(
|
|
3852
|
+
console.log(chalk27.blue.bold(`
|
|
5096
3853
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
5097
3854
|
`));
|
|
5098
3855
|
const allVars = Object.entries(envSchema);
|
|
@@ -5114,24 +3871,24 @@ async function showEnvStats(options) {
|
|
|
5114
3871
|
acc[file] = (acc[file] || 0) + 1;
|
|
5115
3872
|
return acc;
|
|
5116
3873
|
}, {});
|
|
5117
|
-
console.log(`${
|
|
5118
|
-
console.log(`${
|
|
5119
|
-
console.log(`${
|
|
5120
|
-
console.log(`${
|
|
5121
|
-
console.log(
|
|
5122
|
-
console.log(` ${
|
|
5123
|
-
console.log(` ${
|
|
5124
|
-
console.log(
|
|
3874
|
+
console.log(`${chalk27.bold("Total variables:")} ${chalk27.cyan(allVars.length)}`);
|
|
3875
|
+
console.log(`${chalk27.bold("Required:")} ${chalk27.red(required.length)}`);
|
|
3876
|
+
console.log(`${chalk27.bold("Optional:")} ${chalk27.dim(optional.length)}`);
|
|
3877
|
+
console.log(`${chalk27.bold("Sensitive:")} ${chalk27.yellow(sensitive.length)}`);
|
|
3878
|
+
console.log(chalk27.bold("\nBy Target:"));
|
|
3879
|
+
console.log(` ${chalk27.blue("Next.js accessible:")} ${chalk27.cyan(nextjsVars.length)}`);
|
|
3880
|
+
console.log(` ${chalk27.magenta("SPFN server only:")} ${chalk27.cyan(serverOnlyVars.length)}`);
|
|
3881
|
+
console.log(chalk27.bold("\nBy File:"));
|
|
5125
3882
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
5126
|
-
console.log(` ${
|
|
3883
|
+
console.log(` ${chalk27.dim(file)}: ${chalk27.cyan(count)}`);
|
|
5127
3884
|
}
|
|
5128
|
-
console.log(
|
|
3885
|
+
console.log(chalk27.bold("\nBy Type:"));
|
|
5129
3886
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
5130
|
-
console.log(` ${formatType(type)}: ${
|
|
3887
|
+
console.log(` ${formatType(type)}: ${chalk27.cyan(count)}`);
|
|
5131
3888
|
}
|
|
5132
3889
|
console.log();
|
|
5133
3890
|
} catch (error) {
|
|
5134
|
-
console.error(
|
|
3891
|
+
console.error(chalk27.red(`
|
|
5135
3892
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5136
3893
|
`));
|
|
5137
3894
|
process.exit(1);
|
|
@@ -5151,40 +3908,40 @@ async function searchEnvVars(query, options) {
|
|
|
5151
3908
|
}
|
|
5152
3909
|
}
|
|
5153
3910
|
if (results.length === 0) {
|
|
5154
|
-
console.log(
|
|
3911
|
+
console.log(chalk27.yellow(`
|
|
5155
3912
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
5156
3913
|
`));
|
|
5157
3914
|
return;
|
|
5158
3915
|
}
|
|
5159
|
-
console.log(
|
|
3916
|
+
console.log(chalk27.blue.bold(`
|
|
5160
3917
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
5161
3918
|
`));
|
|
5162
3919
|
for (const [key, schema] of results) {
|
|
5163
3920
|
const typeStr = formatType(schema.type);
|
|
5164
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
5165
|
-
console.log(`${
|
|
5166
|
-
console.log(` ${
|
|
3921
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
|
|
3922
|
+
console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}`);
|
|
3923
|
+
console.log(` ${chalk27.dim(schema.description)}`);
|
|
5167
3924
|
if (schema.default !== void 0) {
|
|
5168
|
-
console.log(` ${
|
|
3925
|
+
console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5169
3926
|
}
|
|
5170
3927
|
console.log();
|
|
5171
3928
|
}
|
|
5172
3929
|
} catch (error) {
|
|
5173
|
-
console.error(
|
|
3930
|
+
console.error(chalk27.red(`
|
|
5174
3931
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5175
3932
|
`));
|
|
5176
3933
|
process.exit(1);
|
|
5177
3934
|
}
|
|
5178
3935
|
}
|
|
5179
|
-
var envCommand = new
|
|
3936
|
+
var envCommand = new Command11("env").description("Manage environment variables");
|
|
5180
3937
|
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
3938
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
5182
3939
|
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
3940
|
function validateEnvOption(envValue) {
|
|
5184
3941
|
if (!VALID_ENVS.includes(envValue)) {
|
|
5185
|
-
console.error(
|
|
3942
|
+
console.error(chalk27.red(`
|
|
5186
3943
|
\u274C Invalid environment: "${envValue}"`));
|
|
5187
|
-
console.log(
|
|
3944
|
+
console.log(chalk27.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
5188
3945
|
`));
|
|
5189
3946
|
process.exit(1);
|
|
5190
3947
|
}
|
|
@@ -5205,8 +3962,8 @@ async function initEnvFiles(options) {
|
|
|
5205
3962
|
return acc;
|
|
5206
3963
|
}, {});
|
|
5207
3964
|
if (targetEnv) {
|
|
5208
|
-
console.log(
|
|
5209
|
-
\u{1F680} Generating .env template files for ${
|
|
3965
|
+
console.log(chalk27.blue.bold(`
|
|
3966
|
+
\u{1F680} Generating .env template files for ${chalk27.cyan(targetEnv)} environment
|
|
5210
3967
|
`));
|
|
5211
3968
|
const envSpecificFiles = {};
|
|
5212
3969
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -5222,25 +3979,24 @@ async function initEnvFiles(options) {
|
|
|
5222
3979
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5223
3980
|
}
|
|
5224
3981
|
} else {
|
|
5225
|
-
console.log(
|
|
3982
|
+
console.log(chalk27.blue.bold(`
|
|
5226
3983
|
\u{1F680} Generating .env template files
|
|
5227
3984
|
`));
|
|
5228
3985
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5229
3986
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5230
3987
|
}
|
|
5231
3988
|
}
|
|
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"));
|
|
3989
|
+
console.log(chalk27.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
3990
|
+
console.log(chalk27.dim(" cp .env.example .env"));
|
|
3991
|
+
console.log(chalk27.dim(" cp .env.local.example .env.local"));
|
|
3992
|
+
console.log(chalk27.dim(" cp .env.server.example .env.server"));
|
|
5237
3993
|
if (targetEnv) {
|
|
5238
|
-
console.log(
|
|
5239
|
-
console.log(
|
|
3994
|
+
console.log(chalk27.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
3995
|
+
console.log(chalk27.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
5240
3996
|
}
|
|
5241
3997
|
console.log("");
|
|
5242
3998
|
} catch (error) {
|
|
5243
|
-
console.error(
|
|
3999
|
+
console.error(chalk27.red(`
|
|
5244
4000
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5245
4001
|
`));
|
|
5246
4002
|
process.exit(1);
|
|
@@ -5248,12 +4004,12 @@ async function initEnvFiles(options) {
|
|
|
5248
4004
|
}
|
|
5249
4005
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5250
4006
|
const filePath = resolve2(cwd, file);
|
|
5251
|
-
if (
|
|
5252
|
-
console.log(
|
|
4007
|
+
if (existsSync22(filePath) && !force) {
|
|
4008
|
+
console.log(chalk27.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5253
4009
|
return;
|
|
5254
4010
|
}
|
|
5255
|
-
|
|
5256
|
-
console.log(
|
|
4011
|
+
writeFileSync13(filePath, generateEnvFileContent(vars), "utf-8");
|
|
4012
|
+
console.log(chalk27.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5257
4013
|
}
|
|
5258
4014
|
function generateEnvFileContent(vars) {
|
|
5259
4015
|
const lines = [
|
|
@@ -5288,7 +4044,7 @@ async function checkEnvFiles(options) {
|
|
|
5288
4044
|
const envSchema = await loadEnvSchema(packageName);
|
|
5289
4045
|
const allVars = Object.entries(envSchema);
|
|
5290
4046
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5291
|
-
console.log(
|
|
4047
|
+
console.log(chalk27.blue.bold(`
|
|
5292
4048
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
5293
4049
|
`));
|
|
5294
4050
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -5297,7 +4053,7 @@ async function checkEnvFiles(options) {
|
|
|
5297
4053
|
const warnings = [];
|
|
5298
4054
|
for (const file of filesToCheck) {
|
|
5299
4055
|
const filePath = resolve2(cwd, file);
|
|
5300
|
-
if (!
|
|
4056
|
+
if (!existsSync22(filePath)) {
|
|
5301
4057
|
continue;
|
|
5302
4058
|
}
|
|
5303
4059
|
const content = readFileSync9(filePath, "utf-8");
|
|
@@ -5305,7 +4061,7 @@ async function checkEnvFiles(options) {
|
|
|
5305
4061
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5306
4062
|
loadedEnv[key] = { value: value || "", file };
|
|
5307
4063
|
}
|
|
5308
|
-
console.log(
|
|
4064
|
+
console.log(chalk27.dim(` \u{1F4C4} ${file} loaded`));
|
|
5309
4065
|
}
|
|
5310
4066
|
console.log("");
|
|
5311
4067
|
for (const [key, schema] of allVars) {
|
|
@@ -5313,7 +4069,7 @@ async function checkEnvFiles(options) {
|
|
|
5313
4069
|
const found = loadedEnv[key];
|
|
5314
4070
|
if (!found) {
|
|
5315
4071
|
if (schema.required && schema.default === void 0) {
|
|
5316
|
-
issues.push(`${
|
|
4072
|
+
issues.push(`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is required but not found in any .env file`);
|
|
5317
4073
|
}
|
|
5318
4074
|
continue;
|
|
5319
4075
|
}
|
|
@@ -5323,11 +4079,11 @@ async function checkEnvFiles(options) {
|
|
|
5323
4079
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5324
4080
|
if (schema.sensitive) {
|
|
5325
4081
|
issues.push(
|
|
5326
|
-
`${
|
|
4082
|
+
`${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
4083
|
);
|
|
5328
4084
|
} else {
|
|
5329
4085
|
warnings.push(
|
|
5330
|
-
`${
|
|
4086
|
+
`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.dim(found.file)}`
|
|
5331
4087
|
);
|
|
5332
4088
|
}
|
|
5333
4089
|
}
|
|
@@ -5335,34 +4091,34 @@ async function checkEnvFiles(options) {
|
|
|
5335
4091
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5336
4092
|
const inSchema = allVars.some(([k]) => k === key);
|
|
5337
4093
|
if (!inSchema) {
|
|
5338
|
-
warnings.push(`${
|
|
4094
|
+
warnings.push(`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} in ${chalk27.dim(file)} is not in schema`);
|
|
5339
4095
|
}
|
|
5340
4096
|
}
|
|
5341
4097
|
if (issues.length > 0) {
|
|
5342
|
-
console.log(
|
|
4098
|
+
console.log(chalk27.red.bold("Issues:"));
|
|
5343
4099
|
for (const issue of issues) {
|
|
5344
4100
|
console.log(` ${issue}`);
|
|
5345
4101
|
}
|
|
5346
4102
|
console.log("");
|
|
5347
4103
|
}
|
|
5348
4104
|
if (warnings.length > 0) {
|
|
5349
|
-
console.log(
|
|
4105
|
+
console.log(chalk27.yellow.bold("Warnings:"));
|
|
5350
4106
|
for (const warning of warnings) {
|
|
5351
4107
|
console.log(` ${warning}`);
|
|
5352
4108
|
}
|
|
5353
4109
|
console.log("");
|
|
5354
4110
|
}
|
|
5355
4111
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5356
|
-
console.log(
|
|
4112
|
+
console.log(chalk27.green("\u2705 All environment variables are correctly configured!\n"));
|
|
5357
4113
|
} else {
|
|
5358
|
-
console.log(
|
|
4114
|
+
console.log(chalk27.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
5359
4115
|
`));
|
|
5360
4116
|
if (issues.length > 0) {
|
|
5361
4117
|
process.exit(1);
|
|
5362
4118
|
}
|
|
5363
4119
|
}
|
|
5364
4120
|
} catch (error) {
|
|
5365
|
-
console.error(
|
|
4121
|
+
console.error(chalk27.red(`
|
|
5366
4122
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5367
4123
|
`));
|
|
5368
4124
|
process.exit(1);
|
|
@@ -5374,17 +4130,17 @@ async function validateEnvVars(options) {
|
|
|
5374
4130
|
const packages = options.packages || ["@spfn/core"];
|
|
5375
4131
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5376
4132
|
if (targetEnv) {
|
|
5377
|
-
const { loadEnv:
|
|
5378
|
-
const result =
|
|
5379
|
-
console.log(
|
|
5380
|
-
\u{1F50D} Validating environment variables for ${
|
|
4133
|
+
const { loadEnv: loadEnv9 } = await import("@spfn/core/env/loader");
|
|
4134
|
+
const result = loadEnv9({ nodeEnv: targetEnv });
|
|
4135
|
+
console.log(chalk27.blue.bold(`
|
|
4136
|
+
\u{1F50D} Validating environment variables for ${chalk27.cyan(targetEnv)}
|
|
5381
4137
|
`));
|
|
5382
4138
|
if (result.loadedFiles.length > 0) {
|
|
5383
|
-
console.log(
|
|
4139
|
+
console.log(chalk27.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5384
4140
|
}
|
|
5385
4141
|
console.log("");
|
|
5386
4142
|
} else {
|
|
5387
|
-
console.log(
|
|
4143
|
+
console.log(chalk27.blue.bold(`
|
|
5388
4144
|
\u{1F50D} Validating environment variables
|
|
5389
4145
|
`));
|
|
5390
4146
|
}
|
|
@@ -5392,7 +4148,7 @@ async function validateEnvVars(options) {
|
|
|
5392
4148
|
const allWarnings = [];
|
|
5393
4149
|
for (const packageName of packages) {
|
|
5394
4150
|
try {
|
|
5395
|
-
console.log(
|
|
4151
|
+
console.log(chalk27.dim(` \u{1F4E6} ${packageName}`));
|
|
5396
4152
|
const envSchema = await loadEnvSchema(packageName);
|
|
5397
4153
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5398
4154
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -5405,10 +4161,10 @@ async function validateEnvVars(options) {
|
|
|
5405
4161
|
}
|
|
5406
4162
|
} catch (error) {
|
|
5407
4163
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5408
|
-
console.log(
|
|
4164
|
+
console.log(chalk27.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5409
4165
|
continue;
|
|
5410
4166
|
}
|
|
5411
|
-
console.error(
|
|
4167
|
+
console.error(chalk27.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5412
4168
|
if (options.strict) {
|
|
5413
4169
|
process.exit(1);
|
|
5414
4170
|
}
|
|
@@ -5416,32 +4172,32 @@ async function validateEnvVars(options) {
|
|
|
5416
4172
|
}
|
|
5417
4173
|
console.log("");
|
|
5418
4174
|
if (allErrors.length > 0) {
|
|
5419
|
-
console.log(
|
|
4175
|
+
console.log(chalk27.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
5420
4176
|
`));
|
|
5421
4177
|
for (const error of allErrors) {
|
|
5422
|
-
console.log(` ${
|
|
5423
|
-
console.log(` ${
|
|
5424
|
-
console.log(` ${
|
|
4178
|
+
console.log(` ${chalk27.red("\u2717")} ${chalk27.cyan(error.key)}`);
|
|
4179
|
+
console.log(` ${chalk27.dim(error.message)}`);
|
|
4180
|
+
console.log(` ${chalk27.dim(`from ${error.package}`)}`);
|
|
5425
4181
|
console.log("");
|
|
5426
4182
|
}
|
|
5427
4183
|
}
|
|
5428
4184
|
if (allWarnings.length > 0) {
|
|
5429
|
-
console.log(
|
|
4185
|
+
console.log(chalk27.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
5430
4186
|
`));
|
|
5431
4187
|
for (const warning of allWarnings) {
|
|
5432
|
-
console.log(` ${
|
|
5433
|
-
console.log(` ${
|
|
4188
|
+
console.log(` ${chalk27.yellow("\u26A0")} ${chalk27.cyan(warning.key)}`);
|
|
4189
|
+
console.log(` ${chalk27.dim(warning.message)}`);
|
|
5434
4190
|
console.log("");
|
|
5435
4191
|
}
|
|
5436
4192
|
}
|
|
5437
4193
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
5438
|
-
console.log(
|
|
4194
|
+
console.log(chalk27.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5439
4195
|
} else if (allErrors.length === 0) {
|
|
5440
|
-
console.log(
|
|
5441
|
-
console.log(
|
|
4196
|
+
console.log(chalk27.green("\u2705 No errors found."));
|
|
4197
|
+
console.log(chalk27.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
5442
4198
|
`));
|
|
5443
4199
|
} else {
|
|
5444
|
-
console.log(
|
|
4200
|
+
console.log(chalk27.red(`
|
|
5445
4201
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
5446
4202
|
`));
|
|
5447
4203
|
process.exit(1);
|
|
@@ -5451,12 +4207,11 @@ envCommand.command("validate").description("Validate environment variables again
|
|
|
5451
4207
|
|
|
5452
4208
|
// src/index.ts
|
|
5453
4209
|
init_version();
|
|
5454
|
-
var program = new
|
|
4210
|
+
var program = new Command12();
|
|
5455
4211
|
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
5456
4212
|
program.addCommand(createCommand);
|
|
5457
4213
|
program.addCommand(initCommand);
|
|
5458
4214
|
program.addCommand(addCommand);
|
|
5459
|
-
program.addCommand(generateCommand);
|
|
5460
4215
|
program.addCommand(devCommand);
|
|
5461
4216
|
program.addCommand(buildCommand);
|
|
5462
4217
|
program.addCommand(startCommand);
|