syncorejs 0.1.0
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 +30 -0
- package/dist/_vendor/core/_virtual/_rolldown/runtime.mjs +27 -0
- package/dist/_vendor/core/cli.d.mts +5 -0
- package/dist/_vendor/core/cli.d.mts.map +1 -0
- package/dist/_vendor/core/cli.mjs +1196 -0
- package/dist/_vendor/core/cli.mjs.map +1 -0
- package/dist/_vendor/core/index.d.mts +7 -0
- package/dist/_vendor/core/index.mjs +25 -0
- package/dist/_vendor/core/index.mjs.map +1 -0
- package/dist/_vendor/core/runtime/devtools.d.mts +15 -0
- package/dist/_vendor/core/runtime/devtools.d.mts.map +1 -0
- package/dist/_vendor/core/runtime/devtools.mjs +300 -0
- package/dist/_vendor/core/runtime/devtools.mjs.map +1 -0
- package/dist/_vendor/core/runtime/functions.d.mts +123 -0
- package/dist/_vendor/core/runtime/functions.d.mts.map +1 -0
- package/dist/_vendor/core/runtime/functions.mjs +71 -0
- package/dist/_vendor/core/runtime/functions.mjs.map +1 -0
- package/dist/_vendor/core/runtime/id.d.mts +13 -0
- package/dist/_vendor/core/runtime/id.d.mts.map +1 -0
- package/dist/_vendor/core/runtime/id.mjs +28 -0
- package/dist/_vendor/core/runtime/id.mjs.map +1 -0
- package/dist/_vendor/core/runtime/runtime.d.mts +370 -0
- package/dist/_vendor/core/runtime/runtime.d.mts.map +1 -0
- package/dist/_vendor/core/runtime/runtime.mjs +1143 -0
- package/dist/_vendor/core/runtime/runtime.mjs.map +1 -0
- package/dist/_vendor/devtools-protocol/index.d.ts +230 -0
- package/dist/_vendor/devtools-protocol/index.d.ts.map +1 -0
- package/dist/_vendor/devtools-protocol/index.js +0 -0
- package/dist/_vendor/next/config.d.ts +17 -0
- package/dist/_vendor/next/config.d.ts.map +1 -0
- package/dist/_vendor/next/config.js +73 -0
- package/dist/_vendor/next/config.js.map +1 -0
- package/dist/_vendor/next/index.d.ts +80 -0
- package/dist/_vendor/next/index.d.ts.map +1 -0
- package/dist/_vendor/next/index.js +81 -0
- package/dist/_vendor/next/index.js.map +1 -0
- package/dist/_vendor/platform-expo/index.d.ts +97 -0
- package/dist/_vendor/platform-expo/index.d.ts.map +1 -0
- package/dist/_vendor/platform-expo/index.js +197 -0
- package/dist/_vendor/platform-expo/index.js.map +1 -0
- package/dist/_vendor/platform-expo/react.d.ts +26 -0
- package/dist/_vendor/platform-expo/react.d.ts.map +1 -0
- package/dist/_vendor/platform-expo/react.js +30 -0
- package/dist/_vendor/platform-expo/react.js.map +1 -0
- package/dist/_vendor/platform-node/index.d.mts +145 -0
- package/dist/_vendor/platform-node/index.d.mts.map +1 -0
- package/dist/_vendor/platform-node/index.mjs +405 -0
- package/dist/_vendor/platform-node/index.mjs.map +1 -0
- package/dist/_vendor/platform-node/ipc-react.d.mts +25 -0
- package/dist/_vendor/platform-node/ipc-react.d.mts.map +1 -0
- package/dist/_vendor/platform-node/ipc-react.mjs +21 -0
- package/dist/_vendor/platform-node/ipc-react.mjs.map +1 -0
- package/dist/_vendor/platform-node/ipc.d.mts +75 -0
- package/dist/_vendor/platform-node/ipc.d.mts.map +1 -0
- package/dist/_vendor/platform-node/ipc.mjs +343 -0
- package/dist/_vendor/platform-node/ipc.mjs.map +1 -0
- package/dist/_vendor/platform-web/index.d.ts +123 -0
- package/dist/_vendor/platform-web/index.d.ts.map +1 -0
- package/dist/_vendor/platform-web/index.js +309 -0
- package/dist/_vendor/platform-web/index.js.map +1 -0
- package/dist/_vendor/platform-web/indexeddb.d.ts +25 -0
- package/dist/_vendor/platform-web/indexeddb.d.ts.map +1 -0
- package/dist/_vendor/platform-web/indexeddb.js +125 -0
- package/dist/_vendor/platform-web/indexeddb.js.map +1 -0
- package/dist/_vendor/platform-web/opfs.d.ts +27 -0
- package/dist/_vendor/platform-web/opfs.d.ts.map +1 -0
- package/dist/_vendor/platform-web/opfs.js +146 -0
- package/dist/_vendor/platform-web/opfs.js.map +1 -0
- package/dist/_vendor/platform-web/persistence.d.ts +27 -0
- package/dist/_vendor/platform-web/persistence.d.ts.map +1 -0
- package/dist/_vendor/platform-web/persistence.js +23 -0
- package/dist/_vendor/platform-web/persistence.js.map +1 -0
- package/dist/_vendor/platform-web/react.d.ts +35 -0
- package/dist/_vendor/platform-web/react.d.ts.map +1 -0
- package/dist/_vendor/platform-web/react.js +42 -0
- package/dist/_vendor/platform-web/react.js.map +1 -0
- package/dist/_vendor/platform-web/sqljs.js +133 -0
- package/dist/_vendor/platform-web/sqljs.js.map +1 -0
- package/dist/_vendor/platform-web/worker.d.ts +78 -0
- package/dist/_vendor/platform-web/worker.d.ts.map +1 -0
- package/dist/_vendor/platform-web/worker.js +307 -0
- package/dist/_vendor/platform-web/worker.js.map +1 -0
- package/dist/_vendor/react/index.d.ts +58 -0
- package/dist/_vendor/react/index.d.ts.map +1 -0
- package/dist/_vendor/react/index.js +151 -0
- package/dist/_vendor/react/index.js.map +1 -0
- package/dist/_vendor/schema/definition.d.ts +98 -0
- package/dist/_vendor/schema/definition.d.ts.map +1 -0
- package/dist/_vendor/schema/definition.js +84 -0
- package/dist/_vendor/schema/definition.js.map +1 -0
- package/dist/_vendor/schema/index.d.ts +4 -0
- package/dist/_vendor/schema/index.js +4 -0
- package/dist/_vendor/schema/planner.d.ts +42 -0
- package/dist/_vendor/schema/planner.d.ts.map +1 -0
- package/dist/_vendor/schema/planner.js +131 -0
- package/dist/_vendor/schema/planner.js.map +1 -0
- package/dist/_vendor/schema/validators.d.ts +194 -0
- package/dist/_vendor/schema/validators.d.ts.map +1 -0
- package/dist/_vendor/schema/validators.js +158 -0
- package/dist/_vendor/schema/validators.js.map +1 -0
- package/dist/_vendor/svelte/index.d.ts +43 -0
- package/dist/_vendor/svelte/index.d.ts.map +1 -0
- package/dist/_vendor/svelte/index.js +75 -0
- package/dist/_vendor/svelte/index.js.map +1 -0
- package/dist/browser-react.d.ts +2 -0
- package/dist/browser-react.js +2 -0
- package/dist/browser.d.ts +12 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +10 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/src/cli.d.ts +5 -0
- package/dist/core/src/cli.d.ts.map +1 -0
- package/dist/core/src/cli.js +1196 -0
- package/dist/core/src/cli.js.map +1 -0
- package/dist/core/src/index.js +7 -0
- package/dist/core/src/runtime/devtools.d.ts +7 -0
- package/dist/core/src/runtime/devtools.d.ts.map +1 -0
- package/dist/core/src/runtime/devtools.js +300 -0
- package/dist/core/src/runtime/devtools.js.map +1 -0
- package/dist/core/src/runtime/functions.d.ts +123 -0
- package/dist/core/src/runtime/functions.d.ts.map +1 -0
- package/dist/core/src/runtime/functions.js +71 -0
- package/dist/core/src/runtime/functions.js.map +1 -0
- package/dist/core/src/runtime/id.d.ts +13 -0
- package/dist/core/src/runtime/id.d.ts.map +1 -0
- package/dist/core/src/runtime/id.js +28 -0
- package/dist/core/src/runtime/id.js.map +1 -0
- package/dist/core/src/runtime/runtime.d.ts +371 -0
- package/dist/core/src/runtime/runtime.d.ts.map +1 -0
- package/dist/core/src/runtime/runtime.js +1143 -0
- package/dist/core/src/runtime/runtime.js.map +1 -0
- package/dist/devtools-protocol/src/index.d.ts +201 -0
- package/dist/devtools-protocol/src/index.d.ts.map +1 -0
- package/dist/expo-react.d.ts +2 -0
- package/dist/expo-react.js +2 -0
- package/dist/expo.d.ts +2 -0
- package/dist/expo.js +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/next/src/config.d.ts +17 -0
- package/dist/next/src/config.d.ts.map +1 -0
- package/dist/next/src/config.js +73 -0
- package/dist/next/src/config.js.map +1 -0
- package/dist/next/src/index.d.ts +80 -0
- package/dist/next/src/index.d.ts.map +1 -0
- package/dist/next/src/index.js +82 -0
- package/dist/next/src/index.js.map +1 -0
- package/dist/next-config.d.ts +2 -0
- package/dist/next-config.js +2 -0
- package/dist/next.d.ts +3 -0
- package/dist/next.js +3 -0
- package/dist/node-ipc-react.d.ts +2 -0
- package/dist/node-ipc-react.js +2 -0
- package/dist/node-ipc.d.ts +2 -0
- package/dist/node-ipc.js +2 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.js +3 -0
- package/dist/platform-expo/src/index.d.ts +96 -0
- package/dist/platform-expo/src/index.d.ts.map +1 -0
- package/dist/platform-expo/src/index.js +198 -0
- package/dist/platform-expo/src/index.js.map +1 -0
- package/dist/platform-expo/src/react.d.ts +26 -0
- package/dist/platform-expo/src/react.d.ts.map +1 -0
- package/dist/platform-expo/src/react.js +30 -0
- package/dist/platform-expo/src/react.js.map +1 -0
- package/dist/platform-node/src/index.d.ts +145 -0
- package/dist/platform-node/src/index.d.ts.map +1 -0
- package/dist/platform-node/src/index.js +407 -0
- package/dist/platform-node/src/index.js.map +1 -0
- package/dist/platform-node/src/ipc-react.d.ts +25 -0
- package/dist/platform-node/src/ipc-react.d.ts.map +1 -0
- package/dist/platform-node/src/ipc-react.js +21 -0
- package/dist/platform-node/src/ipc-react.js.map +1 -0
- package/dist/platform-node/src/ipc.d.ts +76 -0
- package/dist/platform-node/src/ipc.d.ts.map +1 -0
- package/dist/platform-node/src/ipc.js +344 -0
- package/dist/platform-node/src/ipc.js.map +1 -0
- package/dist/platform-web/src/index.d.ts +106 -0
- package/dist/platform-web/src/index.d.ts.map +1 -0
- package/dist/platform-web/src/index.js +311 -0
- package/dist/platform-web/src/index.js.map +1 -0
- package/dist/platform-web/src/indexeddb.js +125 -0
- package/dist/platform-web/src/indexeddb.js.map +1 -0
- package/dist/platform-web/src/opfs.js +146 -0
- package/dist/platform-web/src/opfs.js.map +1 -0
- package/dist/platform-web/src/persistence.d.ts +20 -0
- package/dist/platform-web/src/persistence.d.ts.map +1 -0
- package/dist/platform-web/src/persistence.js +23 -0
- package/dist/platform-web/src/persistence.js.map +1 -0
- package/dist/platform-web/src/react.d.ts +35 -0
- package/dist/platform-web/src/react.d.ts.map +1 -0
- package/dist/platform-web/src/react.js +42 -0
- package/dist/platform-web/src/react.js.map +1 -0
- package/dist/platform-web/src/sqljs.js +133 -0
- package/dist/platform-web/src/sqljs.js.map +1 -0
- package/dist/platform-web/src/worker.d.ts +79 -0
- package/dist/platform-web/src/worker.d.ts.map +1 -0
- package/dist/platform-web/src/worker.js +308 -0
- package/dist/platform-web/src/worker.js.map +1 -0
- package/dist/react/src/index.d.ts +59 -0
- package/dist/react/src/index.d.ts.map +1 -0
- package/dist/react/src/index.js +151 -0
- package/dist/react/src/index.js.map +1 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +2 -0
- package/dist/schema/src/definition.d.ts +98 -0
- package/dist/schema/src/definition.d.ts.map +1 -0
- package/dist/schema/src/definition.js +84 -0
- package/dist/schema/src/definition.js.map +1 -0
- package/dist/schema/src/planner.d.ts +42 -0
- package/dist/schema/src/planner.d.ts.map +1 -0
- package/dist/schema/src/planner.js +131 -0
- package/dist/schema/src/planner.js.map +1 -0
- package/dist/schema/src/validators.d.ts +194 -0
- package/dist/schema/src/validators.d.ts.map +1 -0
- package/dist/schema/src/validators.js +158 -0
- package/dist/schema/src/validators.js.map +1 -0
- package/dist/svelte/src/index.d.ts +44 -0
- package/dist/svelte/src/index.d.ts.map +1 -0
- package/dist/svelte/src/index.js +75 -0
- package/dist/svelte/src/index.js.map +1 -0
- package/dist/svelte.d.ts +2 -0
- package/dist/svelte.js +2 -0
- package/package.json +152 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
import { createSchemaSnapshot, diffSchemaSnapshots, parseSchemaSnapshot, renderCreateIndexStatement, renderCreateSearchIndexStatement, renderCreateTableStatement, renderMigrationSql, searchIndexTableName } from "../../schema/src/planner.js";
|
|
2
|
+
import { generateId } from "./runtime/id.js";
|
|
3
|
+
import "./index.js";
|
|
4
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { connect } from "node:net";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { DatabaseSync } from "node:sqlite";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { tsImport } from "tsx/esm/api";
|
|
12
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
13
|
+
//#region ../core/src/cli.ts
|
|
14
|
+
const COMBINED_DEV_COMMAND = "concurrently --kill-others-on-fail --names syncore,app --prefix-colors yellow,cyan \"bun run syncorejs:dev\" \"bun run dev:app\"";
|
|
15
|
+
const program = new Command();
|
|
16
|
+
const CORE_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
const migrationSnapshotFileName = "_schema_snapshot.json";
|
|
18
|
+
const validTemplates = [
|
|
19
|
+
"minimal",
|
|
20
|
+
"node",
|
|
21
|
+
"react-web",
|
|
22
|
+
"expo",
|
|
23
|
+
"electron",
|
|
24
|
+
"next"
|
|
25
|
+
];
|
|
26
|
+
let pendingDevBootstrap;
|
|
27
|
+
let devBootstrapInFlight = false;
|
|
28
|
+
program.name("syncorejs").description("Syncore local-first toolkit CLI").version("0.1.0");
|
|
29
|
+
program.command("init").description("Scaffold Syncore in the current directory").option("--template <template>", `Template to scaffold (${validTemplates.join(", ")}, or auto)`, "auto").option("--force", "Overwrite Syncore-managed files when they already exist").action(async (options) => {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const result = await scaffoldProject(cwd, {
|
|
32
|
+
template: await resolveRequestedTemplate(cwd, options.template),
|
|
33
|
+
...options.force ? { force: true } : {}
|
|
34
|
+
});
|
|
35
|
+
await runCodegen(cwd);
|
|
36
|
+
logScaffoldResult(result, "Syncore project scaffolded.");
|
|
37
|
+
console.log("Next: run `npx syncorejs dev` to keep codegen and local migrations in sync.");
|
|
38
|
+
});
|
|
39
|
+
program.command("codegen").description("Generate typed Syncore references from syncore/functions").action(async () => {
|
|
40
|
+
await runCodegen(process.cwd());
|
|
41
|
+
console.log("Generated syncore/_generated files.");
|
|
42
|
+
});
|
|
43
|
+
program.command("doctor").description("Check Syncore project structure and inferred template").action(async () => {
|
|
44
|
+
const cwd = process.cwd();
|
|
45
|
+
const checks = [
|
|
46
|
+
"syncore.config.ts",
|
|
47
|
+
path.join("syncore", "schema.ts"),
|
|
48
|
+
path.join("syncore", "functions"),
|
|
49
|
+
path.join("syncore", "migrations")
|
|
50
|
+
];
|
|
51
|
+
console.log(`Detected template: ${await detectProjectTemplate(cwd)}`);
|
|
52
|
+
for (const check of checks) {
|
|
53
|
+
const exists = await fileExists(path.join(cwd, check));
|
|
54
|
+
console.log(`${exists ? "OK" : "MISSING"} ${check}`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
program.command("import").description("Import JSONL sample data into a local Syncore table").requiredOption("--table <table>", "Table name to import into").argument("<file>", "Path to a JSONL file").action(async (filePath, options) => {
|
|
58
|
+
const importedCount = await importJsonlIntoProject(process.cwd(), options.table, filePath);
|
|
59
|
+
console.log(`Imported ${importedCount} row(s) into ${JSON.stringify(options.table)}.`);
|
|
60
|
+
});
|
|
61
|
+
program.command("seed").description("Import seed data from syncore/seed.jsonl or syncore/seed/<table>.jsonl").requiredOption("--table <table>", "Table name to seed").option("--file <file>", "Explicit JSONL file path").action(async (options) => {
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const seedFile = options.file ?? await resolveDefaultSeedFile(cwd, options.table) ?? path.join("syncore", "seed", `${options.table}.jsonl`);
|
|
64
|
+
const importedCount = await importJsonlIntoProject(cwd, options.table, seedFile);
|
|
65
|
+
console.log(`Seeded ${importedCount} row(s) into ${JSON.stringify(options.table)} from ${seedFile}.`);
|
|
66
|
+
});
|
|
67
|
+
program.command("migrate:status").description("Compare the current schema against the last saved migration snapshot").action(async () => {
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
const currentSnapshot = createSchemaSnapshot(await loadProjectSchema(cwd));
|
|
70
|
+
const storedSnapshot = await readStoredSnapshot(cwd);
|
|
71
|
+
const plan = diffSchemaSnapshots(storedSnapshot, currentSnapshot);
|
|
72
|
+
console.log(`Current schema hash: ${currentSnapshot.hash}`);
|
|
73
|
+
console.log(`Stored snapshot: ${storedSnapshot?.hash ?? "none"}`);
|
|
74
|
+
console.log(`Statements to generate: ${plan.statements.length}`);
|
|
75
|
+
console.log(`Warnings: ${plan.warnings.length}`);
|
|
76
|
+
console.log(`Destructive changes: ${plan.destructiveChanges.length}`);
|
|
77
|
+
for (const warning of plan.warnings) console.log(`WARN ${warning}`);
|
|
78
|
+
for (const destructiveChange of plan.destructiveChanges) console.log(`BLOCK ${destructiveChange}`);
|
|
79
|
+
});
|
|
80
|
+
program.command("migrate:generate").argument("[name]", "Optional migration name", "auto").description("Generate a SQL migration file from the current schema diff").action(async (name) => {
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const currentSnapshot = createSchemaSnapshot(await loadProjectSchema(cwd));
|
|
83
|
+
const plan = diffSchemaSnapshots(await readStoredSnapshot(cwd), currentSnapshot);
|
|
84
|
+
if (plan.destructiveChanges.length > 0) {
|
|
85
|
+
console.error("Destructive schema changes require a manual migration:");
|
|
86
|
+
for (const destructiveChange of plan.destructiveChanges) console.error(`- ${destructiveChange}`);
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (plan.statements.length === 0 && plan.warnings.length === 0) {
|
|
91
|
+
console.log("No schema changes detected.");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const migrationsDirectory = path.join(cwd, "syncore", "migrations");
|
|
95
|
+
await mkdir(migrationsDirectory, { recursive: true });
|
|
96
|
+
const migrationNumber = await getNextMigrationNumber(migrationsDirectory);
|
|
97
|
+
const slug = slugify(name);
|
|
98
|
+
const migrationFileName = `${String(migrationNumber).padStart(4, "0")}_${slug}.sql`;
|
|
99
|
+
const migrationSql = renderMigrationSql(plan, { title: `Syncore migration ${migrationFileName}` });
|
|
100
|
+
await writeFile(path.join(migrationsDirectory, migrationFileName), migrationSql);
|
|
101
|
+
await writeStoredSnapshot(cwd, currentSnapshot);
|
|
102
|
+
console.log(`Generated ${path.join("syncore", "migrations", migrationFileName)}`);
|
|
103
|
+
});
|
|
104
|
+
program.command("migrate:apply").description("Apply SQL migrations from syncore/migrations to the configured database").action(async () => {
|
|
105
|
+
const appliedCount = await applyProjectMigrations(process.cwd());
|
|
106
|
+
console.log(`Applied ${appliedCount} migration(s).`);
|
|
107
|
+
});
|
|
108
|
+
program.command("dev").description("Start the Syncore dev loop and devtools hub").option("--template <template>", `Template to scaffold when Syncore is missing (${validTemplates.join(", ")}, or auto)`, "auto").action(async (options) => {
|
|
109
|
+
const cwd = process.cwd();
|
|
110
|
+
await startDevHub({
|
|
111
|
+
cwd,
|
|
112
|
+
template: await resolveRequestedTemplate(cwd, options.template)
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
async function runSyncoreCli(argv = process.argv) {
|
|
116
|
+
await program.parseAsync(argv);
|
|
117
|
+
}
|
|
118
|
+
if (isCliEntryPoint()) await runSyncoreCli();
|
|
119
|
+
async function runCodegen(cwd) {
|
|
120
|
+
const functionsDir = path.join(cwd, "syncore", "functions");
|
|
121
|
+
const generatedDir = path.join(cwd, "syncore", "_generated");
|
|
122
|
+
await mkdir(generatedDir, { recursive: true });
|
|
123
|
+
const functionImportExtension = await resolveFunctionImportExtension(cwd);
|
|
124
|
+
const files = await listTypeScriptFiles(functionsDir);
|
|
125
|
+
const functionEntries = [];
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
const content = await readFile(file, "utf8");
|
|
128
|
+
const pathParts = path.relative(functionsDir, file).replaceAll("\\", "/").replace(/\.tsx?$/, "").split("/");
|
|
129
|
+
for (const match of content.matchAll(/export const (\w+)\s*=\s*(query|mutation|action)\(/g)) functionEntries.push({
|
|
130
|
+
pathParts,
|
|
131
|
+
exportName: match[1],
|
|
132
|
+
kind: match[2]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const apiSource = [
|
|
136
|
+
`/**`,
|
|
137
|
+
` * Generated \`api\` utility for referencing Syncore functions.`,
|
|
138
|
+
` *`,
|
|
139
|
+
` * THIS CODE IS AUTOMATICALLY GENERATED.`,
|
|
140
|
+
` *`,
|
|
141
|
+
` * To regenerate, run \`npx syncorejs dev\` or \`npx syncorejs codegen\`.`,
|
|
142
|
+
` * @module`,
|
|
143
|
+
` */`,
|
|
144
|
+
``,
|
|
145
|
+
`import { createFunctionReferenceFor } from "syncorejs";`,
|
|
146
|
+
`import type { FunctionReferenceFor } from "syncorejs";`,
|
|
147
|
+
...renderFunctionTypeImports(functionEntries, functionImportExtension),
|
|
148
|
+
``,
|
|
149
|
+
...renderGeneratedApiInterfaces(functionEntries),
|
|
150
|
+
``,
|
|
151
|
+
`/**`,
|
|
152
|
+
` * A utility for referencing Syncore functions in your app's public API.`,
|
|
153
|
+
` *`,
|
|
154
|
+
` * Usage:`,
|
|
155
|
+
` * \`\`\`ts`,
|
|
156
|
+
` * const listTasks = api.tasks.list;`,
|
|
157
|
+
` * \`\`\``,
|
|
158
|
+
` */`,
|
|
159
|
+
`export const api: SyncoreApi = ${renderApiObject(functionEntries)} as const;`,
|
|
160
|
+
``
|
|
161
|
+
].join("\n");
|
|
162
|
+
const functionsSource = [
|
|
163
|
+
`/**`,
|
|
164
|
+
` * Generated Syncore function registry.`,
|
|
165
|
+
` *`,
|
|
166
|
+
` * THIS CODE IS AUTOMATICALLY GENERATED.`,
|
|
167
|
+
` *`,
|
|
168
|
+
` * To regenerate, run \`npx syncorejs dev\` or \`npx syncorejs codegen\`.`,
|
|
169
|
+
` * @module`,
|
|
170
|
+
` */`,
|
|
171
|
+
``,
|
|
172
|
+
`import type { SyncoreFunctionRegistry } from "syncorejs";`,
|
|
173
|
+
``,
|
|
174
|
+
...renderFunctionImports(functionEntries, functionImportExtension),
|
|
175
|
+
``,
|
|
176
|
+
...renderGeneratedFunctionsInterface(functionEntries),
|
|
177
|
+
``,
|
|
178
|
+
`/**`,
|
|
179
|
+
` * The runtime registry for every function exported from \`syncore/functions\`.`,
|
|
180
|
+
` *`,
|
|
181
|
+
` * Most application code should import from \`./api\` instead of using this map directly.`,
|
|
182
|
+
` */`,
|
|
183
|
+
`export const functions: SyncoreFunctionsRegistry = {`,
|
|
184
|
+
...functionEntries.map((entry) => ` ${JSON.stringify(`${entry.pathParts.join("/")}/${entry.exportName}`)}: ${renderFunctionImportName(entry)},`),
|
|
185
|
+
`} as const;`,
|
|
186
|
+
``
|
|
187
|
+
].join("\n");
|
|
188
|
+
const serverSource = [
|
|
189
|
+
`/**`,
|
|
190
|
+
` * Generated utilities for implementing Syncore query, mutation, and action functions.`,
|
|
191
|
+
` *`,
|
|
192
|
+
` * THIS CODE IS AUTOMATICALLY GENERATED.`,
|
|
193
|
+
` *`,
|
|
194
|
+
` * To regenerate, run \`npx syncorejs dev\` or \`npx syncorejs codegen\`.`,
|
|
195
|
+
` * @module`,
|
|
196
|
+
` */`,
|
|
197
|
+
``,
|
|
198
|
+
`import type schema from "../schema${functionImportExtension}";`,
|
|
199
|
+
`import { action as baseAction, mutation as baseMutation, query as baseQuery } from "syncorejs";`,
|
|
200
|
+
`import type {`,
|
|
201
|
+
` ActionCtx as BaseActionCtx,`,
|
|
202
|
+
` FunctionConfig,`,
|
|
203
|
+
` Infer,`,
|
|
204
|
+
` InferArgs,`,
|
|
205
|
+
` MutationCtx as BaseMutationCtx,`,
|
|
206
|
+
` QueryCtx as BaseQueryCtx,`,
|
|
207
|
+
` SyncoreFunctionDefinition,`,
|
|
208
|
+
` Validator,`,
|
|
209
|
+
` ValidatorMap`,
|
|
210
|
+
`} from "syncorejs";`,
|
|
211
|
+
``,
|
|
212
|
+
`export { createFunctionReference, createFunctionReferenceFor, v } from "syncorejs";`,
|
|
213
|
+
``,
|
|
214
|
+
`/**`,
|
|
215
|
+
` * The context object available inside Syncore query handlers in this app.`,
|
|
216
|
+
` */`,
|
|
217
|
+
`export type QueryCtx = BaseQueryCtx<typeof schema>;`,
|
|
218
|
+
``,
|
|
219
|
+
`/**`,
|
|
220
|
+
` * The context object available inside Syncore mutation handlers in this app.`,
|
|
221
|
+
` */`,
|
|
222
|
+
`export type MutationCtx = BaseMutationCtx<typeof schema>;`,
|
|
223
|
+
``,
|
|
224
|
+
`/**`,
|
|
225
|
+
` * The context object available inside Syncore action handlers in this app.`,
|
|
226
|
+
` */`,
|
|
227
|
+
`export type ActionCtx = BaseActionCtx<typeof schema>;`,
|
|
228
|
+
``,
|
|
229
|
+
`export type { FunctionReference } from "syncorejs";`,
|
|
230
|
+
``,
|
|
231
|
+
`/**`,
|
|
232
|
+
` * Define a query in this Syncore app's public API.`,
|
|
233
|
+
` *`,
|
|
234
|
+
` * Queries can read from your local Syncore database and can be called from clients.`,
|
|
235
|
+
` *`,
|
|
236
|
+
` * @param config - The query definition, including args and a handler.`,
|
|
237
|
+
` * @returns The wrapped query. Export it from \`syncore/functions\` to add it to the generated API.`,
|
|
238
|
+
` */`,
|
|
239
|
+
`export function query<TValidator extends Validator<unknown>, TResult>(`,
|
|
240
|
+
` config: FunctionConfig<QueryCtx, Infer<TValidator>, TResult> & { args: TValidator }`,
|
|
241
|
+
`): SyncoreFunctionDefinition<"query", QueryCtx, Infer<TValidator>, TResult>;`,
|
|
242
|
+
`export function query<TArgsShape extends ValidatorMap, TResult>(`,
|
|
243
|
+
` config: FunctionConfig<QueryCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
244
|
+
`): SyncoreFunctionDefinition<"query", QueryCtx, InferArgs<TArgsShape>, TResult>;`,
|
|
245
|
+
`export function query<TArgsShape extends Validator<unknown> | ValidatorMap, TResult>(`,
|
|
246
|
+
` config: FunctionConfig<QueryCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
247
|
+
`) {`,
|
|
248
|
+
` return baseQuery(config as never) as SyncoreFunctionDefinition<`,
|
|
249
|
+
` "query",`,
|
|
250
|
+
` QueryCtx,`,
|
|
251
|
+
` InferArgs<TArgsShape>,`,
|
|
252
|
+
` TResult`,
|
|
253
|
+
` >;`,
|
|
254
|
+
`}`,
|
|
255
|
+
``,
|
|
256
|
+
`/**`,
|
|
257
|
+
` * Define a mutation in this Syncore app's public API.`,
|
|
258
|
+
` *`,
|
|
259
|
+
` * Mutations can write to your local Syncore database and can be called from clients.`,
|
|
260
|
+
` *`,
|
|
261
|
+
` * @param config - The mutation definition, including args and a handler.`,
|
|
262
|
+
` * @returns The wrapped mutation. Export it from \`syncore/functions\` to add it to the generated API.`,
|
|
263
|
+
` */`,
|
|
264
|
+
`export function mutation<TValidator extends Validator<unknown>, TResult>(`,
|
|
265
|
+
` config: FunctionConfig<MutationCtx, Infer<TValidator>, TResult> & { args: TValidator }`,
|
|
266
|
+
`): SyncoreFunctionDefinition<"mutation", MutationCtx, Infer<TValidator>, TResult>;`,
|
|
267
|
+
`export function mutation<TArgsShape extends ValidatorMap, TResult>(`,
|
|
268
|
+
` config: FunctionConfig<MutationCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
269
|
+
`): SyncoreFunctionDefinition<"mutation", MutationCtx, InferArgs<TArgsShape>, TResult>;`,
|
|
270
|
+
`export function mutation<TArgsShape extends Validator<unknown> | ValidatorMap, TResult>(`,
|
|
271
|
+
` config: FunctionConfig<MutationCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
272
|
+
`) {`,
|
|
273
|
+
` return baseMutation(config as never) as SyncoreFunctionDefinition<`,
|
|
274
|
+
` "mutation",`,
|
|
275
|
+
` MutationCtx,`,
|
|
276
|
+
` InferArgs<TArgsShape>,`,
|
|
277
|
+
` TResult`,
|
|
278
|
+
` >;`,
|
|
279
|
+
`}`,
|
|
280
|
+
``,
|
|
281
|
+
`/**`,
|
|
282
|
+
` * Define an action in this Syncore app's public API.`,
|
|
283
|
+
` *`,
|
|
284
|
+
` * Actions can run arbitrary JavaScript and may call queries or mutations.`,
|
|
285
|
+
` *`,
|
|
286
|
+
` * @param config - The action definition, including args and a handler.`,
|
|
287
|
+
` * @returns The wrapped action. Export it from \`syncore/functions\` to add it to the generated API.`,
|
|
288
|
+
` */`,
|
|
289
|
+
`export function action<TValidator extends Validator<unknown>, TResult>(`,
|
|
290
|
+
` config: FunctionConfig<ActionCtx, Infer<TValidator>, TResult> & { args: TValidator }`,
|
|
291
|
+
`): SyncoreFunctionDefinition<"action", ActionCtx, Infer<TValidator>, TResult>;`,
|
|
292
|
+
`export function action<TArgsShape extends ValidatorMap, TResult>(`,
|
|
293
|
+
` config: FunctionConfig<ActionCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
294
|
+
`): SyncoreFunctionDefinition<"action", ActionCtx, InferArgs<TArgsShape>, TResult>;`,
|
|
295
|
+
`export function action<TArgsShape extends Validator<unknown> | ValidatorMap, TResult>(`,
|
|
296
|
+
` config: FunctionConfig<ActionCtx, InferArgs<TArgsShape>, TResult> & { args: TArgsShape }`,
|
|
297
|
+
`) {`,
|
|
298
|
+
` return baseAction(config as never) as SyncoreFunctionDefinition<`,
|
|
299
|
+
` "action",`,
|
|
300
|
+
` ActionCtx,`,
|
|
301
|
+
` InferArgs<TArgsShape>,`,
|
|
302
|
+
` TResult`,
|
|
303
|
+
` >;`,
|
|
304
|
+
`}`,
|
|
305
|
+
``
|
|
306
|
+
].join("\n");
|
|
307
|
+
await writeFile(path.join(generatedDir, "api.ts"), apiSource);
|
|
308
|
+
await writeFile(path.join(generatedDir, "functions.ts"), functionsSource);
|
|
309
|
+
await writeFile(path.join(generatedDir, "server.ts"), serverSource);
|
|
310
|
+
}
|
|
311
|
+
async function scaffoldProject(cwd, options) {
|
|
312
|
+
const files = buildTemplateFiles(options.template);
|
|
313
|
+
const created = [];
|
|
314
|
+
const updated = [];
|
|
315
|
+
const skipped = [];
|
|
316
|
+
await mkdir(path.join(cwd, "syncore", "functions"), { recursive: true });
|
|
317
|
+
await mkdir(path.join(cwd, "syncore", "_generated"), { recursive: true });
|
|
318
|
+
await mkdir(path.join(cwd, "syncore", "migrations"), { recursive: true });
|
|
319
|
+
for (const file of files) {
|
|
320
|
+
const targetPath = path.join(cwd, file.path);
|
|
321
|
+
const result = await writeManagedFile(targetPath, file.content, options.force);
|
|
322
|
+
const relative = path.relative(cwd, targetPath).replaceAll("\\", "/");
|
|
323
|
+
if (result === "created") created.push(relative);
|
|
324
|
+
else if (result === "updated") updated.push(relative);
|
|
325
|
+
else skipped.push(relative);
|
|
326
|
+
}
|
|
327
|
+
await ensurePackageScripts(cwd, options.template);
|
|
328
|
+
await ensureGitignoreEntries(cwd, [".syncore/"]);
|
|
329
|
+
return {
|
|
330
|
+
template: options.template,
|
|
331
|
+
created,
|
|
332
|
+
updated,
|
|
333
|
+
skipped
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function buildTemplateFiles(template) {
|
|
337
|
+
const files = [
|
|
338
|
+
{
|
|
339
|
+
path: "syncore.config.ts",
|
|
340
|
+
content: `export default {
|
|
341
|
+
databasePath: ".syncore/syncore.db",
|
|
342
|
+
storageDirectory: ".syncore/storage"
|
|
343
|
+
};
|
|
344
|
+
`
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
path: path.join("syncore", "schema.ts"),
|
|
348
|
+
content: `import { defineSchema, defineTable, v } from "syncorejs";
|
|
349
|
+
|
|
350
|
+
export default defineSchema({
|
|
351
|
+
tasks: defineTable({
|
|
352
|
+
text: v.string(),
|
|
353
|
+
done: v.boolean()
|
|
354
|
+
}).index("by_done", ["done"])
|
|
355
|
+
});
|
|
356
|
+
`
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
path: path.join("syncore", "functions", "tasks.ts"),
|
|
360
|
+
content: `import { mutation, query, v } from "../_generated/server";
|
|
361
|
+
|
|
362
|
+
export const list = query({
|
|
363
|
+
args: {},
|
|
364
|
+
handler: async (ctx) =>
|
|
365
|
+
ctx.db.query("tasks").withIndex("by_done").order("asc").collect()
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
export const create = mutation({
|
|
369
|
+
args: { text: v.string() },
|
|
370
|
+
handler: async (ctx, args) =>
|
|
371
|
+
ctx.db.insert("tasks", { text: args.text, done: false })
|
|
372
|
+
});
|
|
373
|
+
`
|
|
374
|
+
}
|
|
375
|
+
];
|
|
376
|
+
switch (template) {
|
|
377
|
+
case "react-web":
|
|
378
|
+
files.push({
|
|
379
|
+
path: path.join("src", "syncore.worker.ts"),
|
|
380
|
+
content: `/// <reference lib="webworker" />
|
|
381
|
+
|
|
382
|
+
import { createBrowserWorkerRuntime } from "syncorejs/browser";
|
|
383
|
+
import schema from "../syncore/schema";
|
|
384
|
+
import { functions } from "../syncore/_generated/functions";
|
|
385
|
+
|
|
386
|
+
void createBrowserWorkerRuntime({
|
|
387
|
+
endpoint: self,
|
|
388
|
+
schema,
|
|
389
|
+
functions,
|
|
390
|
+
databaseName: "syncore-app",
|
|
391
|
+
persistenceMode: "opfs"
|
|
392
|
+
});
|
|
393
|
+
`
|
|
394
|
+
}, {
|
|
395
|
+
path: path.join("src", "syncore-provider.tsx"),
|
|
396
|
+
content: `import type { ReactNode } from "react";
|
|
397
|
+
import { SyncoreBrowserProvider } from "syncorejs/browser/react";
|
|
398
|
+
|
|
399
|
+
export function AppSyncoreProvider({ children }: { children: ReactNode }) {
|
|
400
|
+
return (
|
|
401
|
+
<SyncoreBrowserProvider workerUrl={new URL("./syncore.worker.ts", import.meta.url)}>
|
|
402
|
+
{children}
|
|
403
|
+
</SyncoreBrowserProvider>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
`
|
|
407
|
+
});
|
|
408
|
+
break;
|
|
409
|
+
case "expo":
|
|
410
|
+
files.push({
|
|
411
|
+
path: path.join("lib", "syncore.ts"),
|
|
412
|
+
content: `import { createExpoSyncoreBootstrap } from "syncorejs/expo";
|
|
413
|
+
import schema from "../syncore/schema";
|
|
414
|
+
import { functions } from "../syncore/_generated/functions";
|
|
415
|
+
|
|
416
|
+
export const syncore = createExpoSyncoreBootstrap({
|
|
417
|
+
schema,
|
|
418
|
+
functions,
|
|
419
|
+
databaseName: "syncore-app.db",
|
|
420
|
+
storageDirectoryName: "syncore-app-storage"
|
|
421
|
+
});
|
|
422
|
+
`
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
case "next":
|
|
426
|
+
files.push({
|
|
427
|
+
path: path.join("app", "syncore.worker.js"),
|
|
428
|
+
content: `/* eslint-disable */
|
|
429
|
+
|
|
430
|
+
import { createBrowserWorkerRuntime } from "syncorejs/browser";
|
|
431
|
+
import schema from "../syncore/schema";
|
|
432
|
+
import { functions } from "../syncore/_generated/functions";
|
|
433
|
+
|
|
434
|
+
void createBrowserWorkerRuntime({
|
|
435
|
+
endpoint: self,
|
|
436
|
+
schema,
|
|
437
|
+
functions,
|
|
438
|
+
databaseName: "syncore-app",
|
|
439
|
+
persistenceDatabaseName: "syncore-app",
|
|
440
|
+
locateFile: () => "/sql-wasm.wasm",
|
|
441
|
+
platform: "browser-worker"
|
|
442
|
+
});
|
|
443
|
+
`
|
|
444
|
+
}, {
|
|
445
|
+
path: path.join("app", "syncore-provider.tsx"),
|
|
446
|
+
content: `"use client";
|
|
447
|
+
|
|
448
|
+
import type { ReactNode } from "react";
|
|
449
|
+
import { SyncoreNextProvider } from "syncorejs/next";
|
|
450
|
+
|
|
451
|
+
const createWorker = () =>
|
|
452
|
+
new Worker(new URL("./syncore.worker.js", import.meta.url), {
|
|
453
|
+
type: "module"
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
export function AppSyncoreProvider({ children }: { children: ReactNode }) {
|
|
457
|
+
return (
|
|
458
|
+
<SyncoreNextProvider createWorker={createWorker}>
|
|
459
|
+
{children}
|
|
460
|
+
</SyncoreNextProvider>
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
`
|
|
464
|
+
});
|
|
465
|
+
break;
|
|
466
|
+
case "node":
|
|
467
|
+
files.push({
|
|
468
|
+
path: "script.mjs",
|
|
469
|
+
content: `import path from "node:path";
|
|
470
|
+
import { withNodeSyncoreClient } from "syncorejs/node";
|
|
471
|
+
import { api } from "./syncore/_generated/api.ts";
|
|
472
|
+
import schema from "./syncore/schema.ts";
|
|
473
|
+
import { functions } from "./syncore/_generated/functions.ts";
|
|
474
|
+
|
|
475
|
+
await withNodeSyncoreClient(
|
|
476
|
+
{
|
|
477
|
+
databasePath: path.join(process.cwd(), ".syncore", "syncore.db"),
|
|
478
|
+
storageDirectory: path.join(process.cwd(), ".syncore", "storage"),
|
|
479
|
+
schema,
|
|
480
|
+
functions
|
|
481
|
+
},
|
|
482
|
+
async (client) => {
|
|
483
|
+
await client.mutation(api.tasks.create, { text: "Run locally" });
|
|
484
|
+
console.log(await client.query(api.tasks.list));
|
|
485
|
+
}
|
|
486
|
+
);
|
|
487
|
+
`
|
|
488
|
+
});
|
|
489
|
+
break;
|
|
490
|
+
case "electron":
|
|
491
|
+
files.push({
|
|
492
|
+
path: path.join("src", "syncore-runtime.ts"),
|
|
493
|
+
content: `import path from "node:path";
|
|
494
|
+
import { app } from "electron";
|
|
495
|
+
import { createNodeSyncoreRuntime } from "syncorejs/node";
|
|
496
|
+
import schema from "../syncore/schema.js";
|
|
497
|
+
import { functions } from "../syncore/_generated/functions.js";
|
|
498
|
+
|
|
499
|
+
export function createAppSyncoreRuntime() {
|
|
500
|
+
const userDataDirectory = app.getPath("userData");
|
|
501
|
+
return createNodeSyncoreRuntime({
|
|
502
|
+
databasePath: path.join(userDataDirectory, "syncore.db"),
|
|
503
|
+
storageDirectory: path.join(userDataDirectory, "storage"),
|
|
504
|
+
schema,
|
|
505
|
+
functions,
|
|
506
|
+
platform: "electron-main"
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
`
|
|
510
|
+
});
|
|
511
|
+
break;
|
|
512
|
+
case "minimal": break;
|
|
513
|
+
}
|
|
514
|
+
return files;
|
|
515
|
+
}
|
|
516
|
+
function logScaffoldResult(result, heading) {
|
|
517
|
+
console.log(heading);
|
|
518
|
+
console.log(`Using template: ${result.template}`);
|
|
519
|
+
if (result.created.length > 0) console.log(`Created: ${result.created.join(", ")}`);
|
|
520
|
+
if (result.updated.length > 0) console.log(`Updated: ${result.updated.join(", ")}`);
|
|
521
|
+
if (result.skipped.length > 0) console.log(`Kept existing: ${result.skipped.join(", ")}`);
|
|
522
|
+
}
|
|
523
|
+
async function ensureProjectScaffolded(cwd, template) {
|
|
524
|
+
if (await hasSyncoreProject(cwd)) return;
|
|
525
|
+
logScaffoldResult(await scaffoldProject(cwd, { template }), "Syncore dev did not find a Syncore project, so it scaffolded one for you.");
|
|
526
|
+
}
|
|
527
|
+
async function hasSyncoreProject(cwd) {
|
|
528
|
+
return await fileExists(path.join(cwd, "syncore.config.ts")) && await fileExists(path.join(cwd, "syncore", "schema.ts")) && await fileExists(path.join(cwd, "syncore", "functions"));
|
|
529
|
+
}
|
|
530
|
+
async function resolveRequestedTemplate(cwd, requestedTemplate) {
|
|
531
|
+
if (requestedTemplate !== "auto") {
|
|
532
|
+
if (!validTemplates.includes(requestedTemplate)) throw new Error(`Unknown template ${JSON.stringify(requestedTemplate)}. Expected one of ${validTemplates.join(", ")} or auto.`);
|
|
533
|
+
return requestedTemplate;
|
|
534
|
+
}
|
|
535
|
+
return detectProjectTemplate(cwd);
|
|
536
|
+
}
|
|
537
|
+
async function detectProjectTemplate(cwd) {
|
|
538
|
+
const packageJson = await readPackageJson(cwd);
|
|
539
|
+
const dependencies = {
|
|
540
|
+
...packageJson?.dependencies ?? {},
|
|
541
|
+
...packageJson?.devDependencies ?? {}
|
|
542
|
+
};
|
|
543
|
+
if ("expo" in dependencies || "react-native" in dependencies) return "expo";
|
|
544
|
+
if ("electron" in dependencies) return "electron";
|
|
545
|
+
if ("next" in dependencies) return "next";
|
|
546
|
+
if ("vite" in dependencies || "@vitejs/plugin-react" in dependencies || await fileExists(path.join(cwd, "src", "main.tsx")) && "react" in dependencies) return "react-web";
|
|
547
|
+
if (packageJson) return "node";
|
|
548
|
+
return "minimal";
|
|
549
|
+
}
|
|
550
|
+
async function readPackageJson(cwd) {
|
|
551
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
552
|
+
if (!await fileExists(packageJsonPath)) return null;
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async function ensurePackageScripts(cwd, template) {
|
|
560
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
561
|
+
if (!await fileExists(packageJsonPath)) return;
|
|
562
|
+
const packageJson = await readPackageJson(cwd);
|
|
563
|
+
if (!packageJson) return;
|
|
564
|
+
const nextPackageJson = {
|
|
565
|
+
...packageJson,
|
|
566
|
+
scripts: { ...packageJson.scripts ?? {} }
|
|
567
|
+
};
|
|
568
|
+
nextPackageJson.scripts ??= {};
|
|
569
|
+
nextPackageJson.scripts["syncorejs:dev"] ??= "syncorejs dev";
|
|
570
|
+
nextPackageJson.scripts["syncorejs:codegen"] ??= "syncorejs codegen";
|
|
571
|
+
maybeAddManagedDevScripts(nextPackageJson, template);
|
|
572
|
+
if (stableStringify(nextPackageJson) === stableStringify(packageJson)) return;
|
|
573
|
+
await writeFile(packageJsonPath, `${JSON.stringify(nextPackageJson, null, 2)}\n`);
|
|
574
|
+
}
|
|
575
|
+
function maybeAddManagedDevScripts(packageJson, template) {
|
|
576
|
+
if (!supportsManagedCombinedDev(template)) return;
|
|
577
|
+
packageJson.scripts ??= {};
|
|
578
|
+
const scripts = packageJson.scripts;
|
|
579
|
+
const currentDev = scripts.dev;
|
|
580
|
+
if (!currentDev) return;
|
|
581
|
+
if (scripts["dev:app"] === currentDev && scripts.dev === combinedDevCommand()) {
|
|
582
|
+
packageJson.devDependencies ??= {};
|
|
583
|
+
packageJson.devDependencies.concurrently ??= "^9.1.2";
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (scripts["dev:app"] && scripts["dev:app"] !== currentDev) return;
|
|
587
|
+
if (currentDev.includes("syncorejs:dev") || currentDev.includes("dev:app")) return;
|
|
588
|
+
packageJson.devDependencies ??= {};
|
|
589
|
+
packageJson.devDependencies.concurrently ??= "^9.1.2";
|
|
590
|
+
scripts["dev:app"] ??= currentDev;
|
|
591
|
+
scripts.dev = combinedDevCommand();
|
|
592
|
+
}
|
|
593
|
+
function supportsManagedCombinedDev(template) {
|
|
594
|
+
return template === "next" || template === "react-web" || template === "electron";
|
|
595
|
+
}
|
|
596
|
+
function combinedDevCommand() {
|
|
597
|
+
return COMBINED_DEV_COMMAND;
|
|
598
|
+
}
|
|
599
|
+
async function ensureGitignoreEntries(cwd, entries) {
|
|
600
|
+
const gitignorePath = path.join(cwd, ".gitignore");
|
|
601
|
+
const existing = await fileExists(gitignorePath) ? await readFile(gitignorePath, "utf8") : "";
|
|
602
|
+
const lines = new Set(existing.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
603
|
+
let changed = false;
|
|
604
|
+
for (const entry of entries) if (!lines.has(entry)) {
|
|
605
|
+
lines.add(entry);
|
|
606
|
+
changed = true;
|
|
607
|
+
}
|
|
608
|
+
if (!changed) return;
|
|
609
|
+
await writeFile(gitignorePath, `${[...lines].sort((left, right) => left.localeCompare(right)).join("\n")}\n`);
|
|
610
|
+
}
|
|
611
|
+
async function writeManagedFile(filePath, content, force = false) {
|
|
612
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
613
|
+
if (!await fileExists(filePath)) {
|
|
614
|
+
await writeFile(filePath, content);
|
|
615
|
+
return "created";
|
|
616
|
+
}
|
|
617
|
+
if (!force) return "skipped";
|
|
618
|
+
if (await readFile(filePath, "utf8") === content) return "skipped";
|
|
619
|
+
await writeFile(filePath, content);
|
|
620
|
+
return "updated";
|
|
621
|
+
}
|
|
622
|
+
async function importJsonlIntoProject(cwd, tableName, sourcePath) {
|
|
623
|
+
const schema = await loadProjectSchema(cwd);
|
|
624
|
+
const table = schema.getTable(tableName);
|
|
625
|
+
const config = await loadProjectConfig(cwd);
|
|
626
|
+
const databasePath = path.resolve(cwd, config.databasePath);
|
|
627
|
+
const storageDirectory = path.resolve(cwd, config.storageDirectory);
|
|
628
|
+
const sourceFilePath = path.resolve(cwd, sourcePath);
|
|
629
|
+
await mkdir(path.dirname(databasePath), { recursive: true });
|
|
630
|
+
await mkdir(storageDirectory, { recursive: true });
|
|
631
|
+
const rows = (await readFile(sourceFilePath, "utf8")).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
632
|
+
const database = new DatabaseSync(databasePath);
|
|
633
|
+
try {
|
|
634
|
+
ensureDatabaseReadyForImport(database, schema);
|
|
635
|
+
let importedCount = 0;
|
|
636
|
+
let lineNumber = 0;
|
|
637
|
+
for (const line of rows) {
|
|
638
|
+
lineNumber += 1;
|
|
639
|
+
let parsed;
|
|
640
|
+
try {
|
|
641
|
+
parsed = JSON.parse(line);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
throw new Error(`Invalid JSON on line ${lineNumber} of ${sourcePath}: ${formatError(error)}`);
|
|
644
|
+
}
|
|
645
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`Line ${lineNumber} of ${sourcePath} must contain a JSON object.`);
|
|
646
|
+
const payload = { ...parsed };
|
|
647
|
+
delete payload._id;
|
|
648
|
+
delete payload._creationTime;
|
|
649
|
+
const validated = table.validator.parse(payload);
|
|
650
|
+
const id = generateId();
|
|
651
|
+
const creationTime = Date.now() + importedCount;
|
|
652
|
+
const json = stableStringify(validated);
|
|
653
|
+
database.prepare(`INSERT INTO ${quoteIdentifier(tableName)} (_id, _creationTime, _json) VALUES (?, ?, ?)`).run(id, creationTime, json);
|
|
654
|
+
syncSearchIndexesForImport(database, tableName, table, {
|
|
655
|
+
_id: id,
|
|
656
|
+
_creationTime: creationTime,
|
|
657
|
+
_json: json
|
|
658
|
+
});
|
|
659
|
+
importedCount += 1;
|
|
660
|
+
}
|
|
661
|
+
return importedCount;
|
|
662
|
+
} finally {
|
|
663
|
+
database.close();
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function resolveDefaultSeedFile(cwd, tableName) {
|
|
667
|
+
const candidates = [path.join(cwd, "syncore", "seed", `${tableName}.jsonl`), path.join(cwd, "syncore", "seed.jsonl")];
|
|
668
|
+
for (const candidate of candidates) if (await fileExists(candidate)) return path.relative(cwd, candidate).replaceAll("\\", "/");
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
function ensureDatabaseReadyForImport(database, schema) {
|
|
672
|
+
for (const tableName of schema.tableNames()) {
|
|
673
|
+
const table = schema.getTable(tableName);
|
|
674
|
+
database.exec(renderCreateTableStatement(tableName));
|
|
675
|
+
for (const index of table.indexes) database.exec(renderCreateIndexStatement(tableName, index.name, index.fields));
|
|
676
|
+
for (const searchIndex of table.searchIndexes) try {
|
|
677
|
+
database.exec(renderCreateSearchIndexStatement(tableName, searchIndex));
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (!isUnsupportedFts5Statement(renderCreateSearchIndexStatement(tableName, searchIndex), error)) throw error;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function syncSearchIndexesForImport(database, tableName, table, row) {
|
|
684
|
+
if (table.searchIndexes.length === 0) return;
|
|
685
|
+
const payload = JSON.parse(row._json);
|
|
686
|
+
for (const searchIndex of table.searchIndexes) {
|
|
687
|
+
const searchTable = searchIndexTableName(tableName, searchIndex.name);
|
|
688
|
+
try {
|
|
689
|
+
database.prepare(`DELETE FROM ${quoteIdentifier(searchTable)} WHERE _id = ?`).run(row._id);
|
|
690
|
+
database.prepare(`INSERT INTO ${quoteIdentifier(searchTable)} (_id, search_value) VALUES (?, ?)`).run(row._id, toSearchValue(payload[searchIndex.searchField]));
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (!isUnsupportedFts5Statement(renderCreateSearchIndexStatement(tableName, searchIndex), error)) throw error;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function listTypeScriptFiles(directory) {
|
|
697
|
+
if (!await fileExists(directory)) return [];
|
|
698
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
699
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
700
|
+
const fullPath = path.join(directory, entry.name);
|
|
701
|
+
if (entry.isDirectory()) return listTypeScriptFiles(fullPath);
|
|
702
|
+
if (entry.isFile() && /\.tsx?$/.test(entry.name)) return [fullPath];
|
|
703
|
+
return [];
|
|
704
|
+
}))).flat().sort((left, right) => left.localeCompare(right));
|
|
705
|
+
}
|
|
706
|
+
function renderGeneratedApiInterfaces(functionEntries) {
|
|
707
|
+
return renderGeneratedApiInterfaceNode(buildApiModuleTree(functionEntries));
|
|
708
|
+
}
|
|
709
|
+
function renderGeneratedApiInterfaceNode(node) {
|
|
710
|
+
const childInterfaces = [...node.children.values()].sort((left, right) => left.pathParts.join("/").localeCompare(right.pathParts.join("/"))).flatMap((child) => renderGeneratedApiInterfaceNode(child));
|
|
711
|
+
const lines = [
|
|
712
|
+
`/**`,
|
|
713
|
+
` * ${renderApiInterfaceDescription(node)}`,
|
|
714
|
+
` */`,
|
|
715
|
+
`export interface ${renderApiInterfaceName(node)} {`
|
|
716
|
+
];
|
|
717
|
+
for (const child of [...node.children.values()].sort((left, right) => left.pathParts.join("/").localeCompare(right.pathParts.join("/")))) lines.push(` /**`, ` * ${renderApiModulePropertyDescription(child)}`, ` */`, ` readonly ${renderPropertyKey(child.pathParts.at(-1) ?? "")}: ${renderApiInterfaceName(child)};`);
|
|
718
|
+
for (const entry of [...node.functions].sort((left, right) => left.exportName.localeCompare(right.exportName))) lines.push(` /**`, ` * ${renderApiFunctionPropertyDescription(entry)}`, ` */`, ` readonly ${renderPropertyKey(entry.exportName)}: FunctionReferenceFor<typeof ${renderFunctionImportName(entry)}>;`);
|
|
719
|
+
lines.push(`}`);
|
|
720
|
+
return [...childInterfaces, lines.join("\n")];
|
|
721
|
+
}
|
|
722
|
+
function renderApiObject(functionEntries) {
|
|
723
|
+
return renderApiObjectNode(buildApiModuleTree(functionEntries));
|
|
724
|
+
}
|
|
725
|
+
function renderApiObjectNode(node) {
|
|
726
|
+
const properties = [];
|
|
727
|
+
for (const child of [...node.children.values()].sort((left, right) => left.pathParts.join("/").localeCompare(right.pathParts.join("/")))) properties.push(`${renderPropertyKey(child.pathParts.at(-1) ?? "")}: ${renderApiObjectNode(child)}`);
|
|
728
|
+
for (const entry of [...node.functions].sort((left, right) => left.exportName.localeCompare(right.exportName))) properties.push(`${renderPropertyKey(entry.exportName)}: createFunctionReferenceFor<typeof ${renderFunctionImportName(entry)}>("${entry.kind}", "${entry.pathParts.join("/")}/${entry.exportName}")`);
|
|
729
|
+
return `{ ${properties.join(", ")} }`;
|
|
730
|
+
}
|
|
731
|
+
function buildApiModuleTree(functionEntries) {
|
|
732
|
+
const root = {
|
|
733
|
+
pathParts: [],
|
|
734
|
+
children: /* @__PURE__ */ new Map(),
|
|
735
|
+
functions: []
|
|
736
|
+
};
|
|
737
|
+
for (const entry of functionEntries) {
|
|
738
|
+
let cursor = root;
|
|
739
|
+
for (const segment of entry.pathParts) {
|
|
740
|
+
let child = cursor.children.get(segment);
|
|
741
|
+
if (!child) {
|
|
742
|
+
child = {
|
|
743
|
+
pathParts: [...cursor.pathParts, segment],
|
|
744
|
+
children: /* @__PURE__ */ new Map(),
|
|
745
|
+
functions: []
|
|
746
|
+
};
|
|
747
|
+
cursor.children.set(segment, child);
|
|
748
|
+
}
|
|
749
|
+
cursor = child;
|
|
750
|
+
}
|
|
751
|
+
cursor.functions.push(entry);
|
|
752
|
+
}
|
|
753
|
+
return root;
|
|
754
|
+
}
|
|
755
|
+
function renderGeneratedFunctionsInterface(functionEntries) {
|
|
756
|
+
const lines = [
|
|
757
|
+
`/**`,
|
|
758
|
+
` * Type-safe runtime definitions for every function exported from \`syncore/functions\`.`,
|
|
759
|
+
` */`,
|
|
760
|
+
`export interface SyncoreFunctionsRegistry extends SyncoreFunctionRegistry {`
|
|
761
|
+
];
|
|
762
|
+
for (const entry of functionEntries.slice().sort((left, right) => `${left.pathParts.join("/")}/${left.exportName}`.localeCompare(`${right.pathParts.join("/")}/${right.exportName}`))) lines.push(` /**`, ` * Runtime definition for the public Syncore ${entry.kind} \`${entry.pathParts.join("/")}/${entry.exportName}\`.`, ` */`, ` readonly ${JSON.stringify(`${entry.pathParts.join("/")}/${entry.exportName}`)}: typeof ${renderFunctionImportName(entry)};`);
|
|
763
|
+
lines.push(`}`);
|
|
764
|
+
return [lines.join("\n")];
|
|
765
|
+
}
|
|
766
|
+
function renderApiInterfaceName(node) {
|
|
767
|
+
if (node.pathParts.length === 0) return "SyncoreApi";
|
|
768
|
+
return `SyncoreApi__${node.pathParts.map(toTypeNamePart).join("__")}`;
|
|
769
|
+
}
|
|
770
|
+
function renderApiInterfaceDescription(node) {
|
|
771
|
+
if (node.pathParts.length === 0) return "Type-safe references to every public Syncore function in this app.";
|
|
772
|
+
if (node.children.size === 0) return `Type-safe references to functions exported from \`syncore/functions/${node.pathParts.join("/")}.ts\`.`;
|
|
773
|
+
return `Type-safe references to functions grouped under \`syncore/functions/${node.pathParts.join("/")}/*\`.`;
|
|
774
|
+
}
|
|
775
|
+
function renderApiModulePropertyDescription(node) {
|
|
776
|
+
if (node.children.size === 0) return `Functions exported from \`syncore/functions/${node.pathParts.join("/")}.ts\`.`;
|
|
777
|
+
return `Functions grouped under \`syncore/functions/${node.pathParts.join("/")}/*\`.`;
|
|
778
|
+
}
|
|
779
|
+
function renderApiFunctionPropertyDescription(entry) {
|
|
780
|
+
return `Reference to the public Syncore ${entry.kind} \`${entry.pathParts.join("/")}/${entry.exportName}\`.`;
|
|
781
|
+
}
|
|
782
|
+
function renderPropertyKey(key) {
|
|
783
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key) ? key : JSON.stringify(key);
|
|
784
|
+
}
|
|
785
|
+
function toTypeNamePart(value) {
|
|
786
|
+
const sanitized = value.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
787
|
+
return /^[0-9]/u.test(sanitized) ? `_${sanitized}` : sanitized;
|
|
788
|
+
}
|
|
789
|
+
function renderFunctionImports(functionEntries, extension) {
|
|
790
|
+
return functionEntries.slice().sort((left, right) => `${left.pathParts.join("/")}/${left.exportName}`.localeCompare(`${right.pathParts.join("/")}/${right.exportName}`)).map((entry) => {
|
|
791
|
+
const relativePath = `../functions/${entry.pathParts.join("/")}${extension}`;
|
|
792
|
+
return `import { ${entry.exportName} as ${renderFunctionImportName(entry)} } from ${JSON.stringify(relativePath)};`;
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
function renderFunctionTypeImports(functionEntries, extension) {
|
|
796
|
+
return renderFunctionImports(functionEntries, extension).map((line) => line.replace(/^import \{/, "import type {"));
|
|
797
|
+
}
|
|
798
|
+
function renderFunctionImportName(entry) {
|
|
799
|
+
return [...entry.pathParts.map((segment) => segment.replace(/[^a-zA-Z0-9_$]/g, "_")), entry.exportName].join("__");
|
|
800
|
+
}
|
|
801
|
+
async function loadProjectConfig(cwd) {
|
|
802
|
+
const config = await loadDefaultExport(path.join(cwd, "syncore.config.ts"));
|
|
803
|
+
if (!config || typeof config !== "object" || typeof config.databasePath !== "string" || typeof config.storageDirectory !== "string") throw new Error("syncore.config.ts must export { databasePath, storageDirectory }.");
|
|
804
|
+
return config;
|
|
805
|
+
}
|
|
806
|
+
async function loadProjectSchema(cwd) {
|
|
807
|
+
const schema = await loadDefaultExport(path.join(cwd, "syncore", "schema.ts"));
|
|
808
|
+
if (!schema || typeof schema !== "object" || typeof schema.tableNames !== "function") throw new Error("syncore/schema.ts must default export defineSchema(...).");
|
|
809
|
+
return schema;
|
|
810
|
+
}
|
|
811
|
+
async function loadDefaultExport(filePath) {
|
|
812
|
+
if (!await fileExists(filePath)) throw new Error(`Missing file: ${path.relative(process.cwd(), filePath)}`);
|
|
813
|
+
const moduleUrl = pathToFileURL(filePath).href;
|
|
814
|
+
const loaded = await tsImport(moduleUrl, { parentURL: import.meta.url });
|
|
815
|
+
if (!("default" in loaded)) throw new Error(`File ${path.relative(process.cwd(), filePath)} must have a default export.`);
|
|
816
|
+
const resolvedDefault = unwrapDefaultExport(loaded.default);
|
|
817
|
+
if (resolvedDefault === void 0) throw new Error(`File ${path.relative(process.cwd(), filePath)} exported undefined.`);
|
|
818
|
+
return resolvedDefault;
|
|
819
|
+
}
|
|
820
|
+
async function readStoredSnapshot(cwd) {
|
|
821
|
+
const snapshotPath = path.join(cwd, "syncore", "migrations", migrationSnapshotFileName);
|
|
822
|
+
if (!await fileExists(snapshotPath)) return null;
|
|
823
|
+
return parseSchemaSnapshot(await readFile(snapshotPath, "utf8"));
|
|
824
|
+
}
|
|
825
|
+
async function writeStoredSnapshot(cwd, snapshot) {
|
|
826
|
+
const migrationsDirectory = path.join(cwd, "syncore", "migrations");
|
|
827
|
+
await mkdir(migrationsDirectory, { recursive: true });
|
|
828
|
+
await writeFile(path.join(migrationsDirectory, migrationSnapshotFileName), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
829
|
+
}
|
|
830
|
+
async function getNextMigrationNumber(directory) {
|
|
831
|
+
if (!await fileExists(directory)) return 1;
|
|
832
|
+
const migrationNumbers = (await readdir(directory)).map((name) => name.match(/^(\d+)_.*\.sql$/)?.[1]).filter((value) => value !== void 0).map((value) => Number.parseInt(value, 10));
|
|
833
|
+
if (migrationNumbers.length === 0) return 1;
|
|
834
|
+
return Math.max(...migrationNumbers) + 1;
|
|
835
|
+
}
|
|
836
|
+
async function applyProjectMigrations(cwd) {
|
|
837
|
+
const config = await loadProjectConfig(cwd);
|
|
838
|
+
const databasePath = path.resolve(cwd, config.databasePath);
|
|
839
|
+
const storageDirectory = path.resolve(cwd, config.storageDirectory);
|
|
840
|
+
await mkdir(path.dirname(databasePath), { recursive: true });
|
|
841
|
+
await mkdir(storageDirectory, { recursive: true });
|
|
842
|
+
const database = new DatabaseSync(databasePath);
|
|
843
|
+
database.exec(`
|
|
844
|
+
CREATE TABLE IF NOT EXISTS "_syncore_migrations" (
|
|
845
|
+
name TEXT PRIMARY KEY,
|
|
846
|
+
applied_at TEXT NOT NULL
|
|
847
|
+
);
|
|
848
|
+
`);
|
|
849
|
+
const migrationsDirectory = path.join(cwd, "syncore", "migrations");
|
|
850
|
+
if (!await fileExists(migrationsDirectory)) {
|
|
851
|
+
database.close();
|
|
852
|
+
return 0;
|
|
853
|
+
}
|
|
854
|
+
const appliedRows = database.prepare(`SELECT name FROM "_syncore_migrations" ORDER BY name ASC`).all();
|
|
855
|
+
const appliedNames = new Set(appliedRows.map((row) => row.name));
|
|
856
|
+
const migrationFiles = (await readdir(migrationsDirectory)).filter((name) => /\.sql$/i.test(name)).sort((left, right) => left.localeCompare(right));
|
|
857
|
+
let appliedCount = 0;
|
|
858
|
+
for (const fileName of migrationFiles) {
|
|
859
|
+
if (appliedNames.has(fileName)) continue;
|
|
860
|
+
const sql = await readFile(path.join(migrationsDirectory, fileName), "utf8");
|
|
861
|
+
database.exec("BEGIN");
|
|
862
|
+
try {
|
|
863
|
+
applyMigrationSql(database, sql, fileName);
|
|
864
|
+
database.prepare(`INSERT INTO "_syncore_migrations" (name, applied_at) VALUES (?, ?)`).run(fileName, (/* @__PURE__ */ new Date()).toISOString());
|
|
865
|
+
database.exec("COMMIT");
|
|
866
|
+
appliedCount += 1;
|
|
867
|
+
} catch (error) {
|
|
868
|
+
database.exec("ROLLBACK");
|
|
869
|
+
database.close();
|
|
870
|
+
throw error;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
database.close();
|
|
874
|
+
return appliedCount;
|
|
875
|
+
}
|
|
876
|
+
async function fileExists(filePath) {
|
|
877
|
+
try {
|
|
878
|
+
await stat(filePath);
|
|
879
|
+
return true;
|
|
880
|
+
} catch {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async function resolveFunctionImportExtension(cwd) {
|
|
885
|
+
const candidateConfigFiles = (await readdir(cwd, { withFileTypes: true })).filter((entry) => entry.isFile() && /^tsconfig.*\.json$/i.test(entry.name)).map((entry) => path.join(cwd, entry.name)).sort((left, right) => left.localeCompare(right));
|
|
886
|
+
for (const filePath of candidateConfigFiles) try {
|
|
887
|
+
const rawConfig = JSON.parse(await readFile(filePath, "utf8"));
|
|
888
|
+
const moduleResolution = rawConfig.compilerOptions?.moduleResolution?.toLowerCase();
|
|
889
|
+
const moduleTarget = rawConfig.compilerOptions?.module?.toLowerCase();
|
|
890
|
+
if (moduleResolution === "nodenext" || moduleTarget === "nodenext") return ".js";
|
|
891
|
+
} catch {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
return "";
|
|
895
|
+
}
|
|
896
|
+
function formatError(error) {
|
|
897
|
+
if (error instanceof Error) return error.message;
|
|
898
|
+
return String(error);
|
|
899
|
+
}
|
|
900
|
+
async function isLocalPortInUse(port) {
|
|
901
|
+
return await new Promise((resolve) => {
|
|
902
|
+
const socket = connect({
|
|
903
|
+
host: "127.0.0.1",
|
|
904
|
+
port
|
|
905
|
+
});
|
|
906
|
+
socket.once("connect", () => {
|
|
907
|
+
socket.end();
|
|
908
|
+
resolve(true);
|
|
909
|
+
});
|
|
910
|
+
socket.once("error", () => {
|
|
911
|
+
resolve(false);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function slugify(value) {
|
|
916
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "auto";
|
|
917
|
+
}
|
|
918
|
+
function applyMigrationSql(database, sql, fileName) {
|
|
919
|
+
const statements = sql.split(/;\s*(?:\r?\n|$)/).map((statement) => statement.trim()).filter(Boolean);
|
|
920
|
+
for (const statement of statements) try {
|
|
921
|
+
database.exec(statement);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (isUnsupportedFts5Statement(statement, error)) {
|
|
924
|
+
console.warn(`Skipping FTS5 statement in ${fileName} because this SQLite build does not include FTS5 support.`);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async function startDevHub(options) {
|
|
931
|
+
const dashboardPort = resolvePortFromEnv("SYNCORE_DASHBOARD_PORT", 4310);
|
|
932
|
+
const devtoolsPort = resolvePortFromEnv("SYNCORE_DEVTOOLS_PORT", 4311);
|
|
933
|
+
await runDevProjectBootstrap(options.cwd, options.template);
|
|
934
|
+
await setupDevProjectWatch(options.cwd, options.template);
|
|
935
|
+
if (await isLocalPortInUse(devtoolsPort)) {
|
|
936
|
+
console.log(`Syncore devtools hub already running at ws://127.0.0.1:${devtoolsPort}. Reusing existing hub/dashboard.`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const httpServer = createServer((_request, response) => {
|
|
940
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
941
|
+
response.end(JSON.stringify({
|
|
942
|
+
ok: true,
|
|
943
|
+
wsPort: devtoolsPort
|
|
944
|
+
}));
|
|
945
|
+
});
|
|
946
|
+
const websocketServer = new WebSocketServer({ server: httpServer });
|
|
947
|
+
const latestSnapshots = /* @__PURE__ */ new Map();
|
|
948
|
+
const runtimeSockets = /* @__PURE__ */ new Map();
|
|
949
|
+
const socketRuntimeIds = /* @__PURE__ */ new Map();
|
|
950
|
+
const dashboardSockets = /* @__PURE__ */ new Set();
|
|
951
|
+
const hello = {
|
|
952
|
+
type: "hello",
|
|
953
|
+
runtimeId: "syncore-dev-hub",
|
|
954
|
+
platform: "dev"
|
|
955
|
+
};
|
|
956
|
+
websocketServer.on("connection", (socket) => {
|
|
957
|
+
dashboardSockets.add(socket);
|
|
958
|
+
socket.send(JSON.stringify(hello));
|
|
959
|
+
for (const snapshot of latestSnapshots.values()) socket.send(JSON.stringify(snapshot));
|
|
960
|
+
socket.on("message", (payload) => {
|
|
961
|
+
const rawPayload = decodeWebSocketPayload(payload);
|
|
962
|
+
if (rawPayload.length === 0) return;
|
|
963
|
+
const message = JSON.parse(rawPayload);
|
|
964
|
+
if (message.type === "ping") {
|
|
965
|
+
socket.send(JSON.stringify({ type: "pong" }));
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (message.type === "request") {
|
|
969
|
+
const targetRuntimeId = message.targetRuntimeId;
|
|
970
|
+
if (!targetRuntimeId) return;
|
|
971
|
+
const target = runtimeSockets.get(targetRuntimeId);
|
|
972
|
+
if (target && target.readyState === WebSocket.OPEN) target.send(JSON.stringify(message));
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (message.type === "hello") {
|
|
976
|
+
dashboardSockets.delete(socket);
|
|
977
|
+
runtimeSockets.set(message.runtimeId, socket);
|
|
978
|
+
const runtimeIds = socketRuntimeIds.get(socket) ?? /* @__PURE__ */ new Set();
|
|
979
|
+
runtimeIds.add(message.runtimeId);
|
|
980
|
+
socketRuntimeIds.set(socket, runtimeIds);
|
|
981
|
+
for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(JSON.stringify(message));
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
if (message.type === "snapshot") {
|
|
985
|
+
latestSnapshots.set(message.snapshot.runtimeId, message);
|
|
986
|
+
dashboardSockets.delete(socket);
|
|
987
|
+
runtimeSockets.set(message.snapshot.runtimeId, socket);
|
|
988
|
+
const runtimeIds = socketRuntimeIds.get(socket) ?? /* @__PURE__ */ new Set();
|
|
989
|
+
runtimeIds.add(message.snapshot.runtimeId);
|
|
990
|
+
socketRuntimeIds.set(socket, runtimeIds);
|
|
991
|
+
}
|
|
992
|
+
const encoded = JSON.stringify(message);
|
|
993
|
+
if (message.type === "response") {
|
|
994
|
+
for (const client of dashboardSockets) if (client.readyState === WebSocket.OPEN) client.send(encoded);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
for (const client of dashboardSockets) {
|
|
998
|
+
if (client === socket || client.readyState !== WebSocket.OPEN) continue;
|
|
999
|
+
client.send(encoded);
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
socket.on("close", () => {
|
|
1003
|
+
dashboardSockets.delete(socket);
|
|
1004
|
+
const runtimeIds = socketRuntimeIds.get(socket);
|
|
1005
|
+
if (!runtimeIds) return;
|
|
1006
|
+
for (const runtimeId of runtimeIds) {
|
|
1007
|
+
latestSnapshots.delete(runtimeId);
|
|
1008
|
+
if (runtimeSockets.get(runtimeId) === socket) runtimeSockets.delete(runtimeId);
|
|
1009
|
+
}
|
|
1010
|
+
socketRuntimeIds.delete(socket);
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
const heartbeat = setInterval(() => {
|
|
1014
|
+
const event = {
|
|
1015
|
+
type: "event",
|
|
1016
|
+
event: {
|
|
1017
|
+
type: "log",
|
|
1018
|
+
runtimeId: "syncore-dev-hub",
|
|
1019
|
+
level: "info",
|
|
1020
|
+
message: "Syncore devtools hub is alive.",
|
|
1021
|
+
timestamp: Date.now()
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
const payload = JSON.stringify(event);
|
|
1025
|
+
for (const client of websocketServer.clients) client.send(payload);
|
|
1026
|
+
}, 4e3);
|
|
1027
|
+
httpServer.on("error", (error) => {
|
|
1028
|
+
console.error(`Syncore devtools hub failed: ${formatError(error)}`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
});
|
|
1031
|
+
httpServer.listen(devtoolsPort, "127.0.0.1", () => {
|
|
1032
|
+
(async () => {
|
|
1033
|
+
console.log(`Syncore devtools hub: ws://127.0.0.1:${devtoolsPort}`);
|
|
1034
|
+
console.log(`Electron/Node runtimes: set devtoolsUrl to ws://127.0.0.1:${devtoolsPort}.`);
|
|
1035
|
+
console.log(`Web/Next apps: connect the dashboard or worker bridge to ws://127.0.0.1:${devtoolsPort}.`);
|
|
1036
|
+
console.log("Expo apps: use the same hub URL through LAN or adb reverse while developing.");
|
|
1037
|
+
const dashboardRoot = path.resolve(CORE_PACKAGE_ROOT, "..", "..", "apps", "dashboard");
|
|
1038
|
+
if (await fileExists(path.join(dashboardRoot, "vite.config.ts"))) try {
|
|
1039
|
+
await (await (await import("vite")).createServer({
|
|
1040
|
+
configFile: path.join(dashboardRoot, "vite.config.ts"),
|
|
1041
|
+
root: dashboardRoot,
|
|
1042
|
+
server: { port: dashboardPort }
|
|
1043
|
+
})).listen();
|
|
1044
|
+
console.log(`Dashboard shell: http://127.0.0.1:${dashboardPort}`);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
console.log(`Dashboard source not started automatically: ${formatError(error)}`);
|
|
1047
|
+
}
|
|
1048
|
+
})();
|
|
1049
|
+
});
|
|
1050
|
+
const close = () => {
|
|
1051
|
+
clearInterval(heartbeat);
|
|
1052
|
+
websocketServer.close();
|
|
1053
|
+
httpServer.close();
|
|
1054
|
+
process.exit(0);
|
|
1055
|
+
};
|
|
1056
|
+
process.on("SIGINT", close);
|
|
1057
|
+
process.on("SIGTERM", close);
|
|
1058
|
+
}
|
|
1059
|
+
async function setupDevProjectWatch(cwd, template) {
|
|
1060
|
+
const snapshot = await createDevWatchSnapshot(cwd);
|
|
1061
|
+
if (snapshot.size === 0) return;
|
|
1062
|
+
console.log("Watching syncore/ for changes.");
|
|
1063
|
+
let lastSnapshot = snapshot;
|
|
1064
|
+
const interval = setInterval(() => {
|
|
1065
|
+
(async () => {
|
|
1066
|
+
const nextSnapshot = await createDevWatchSnapshot(cwd);
|
|
1067
|
+
if (!areDevWatchSnapshotsEqual(lastSnapshot, nextSnapshot)) {
|
|
1068
|
+
lastSnapshot = nextSnapshot;
|
|
1069
|
+
scheduleDevProjectBootstrap(cwd, template);
|
|
1070
|
+
}
|
|
1071
|
+
})();
|
|
1072
|
+
}, 500);
|
|
1073
|
+
const dispose = () => {
|
|
1074
|
+
clearInterval(interval);
|
|
1075
|
+
};
|
|
1076
|
+
process.once("SIGINT", dispose);
|
|
1077
|
+
process.once("SIGTERM", dispose);
|
|
1078
|
+
}
|
|
1079
|
+
function scheduleDevProjectBootstrap(cwd, template) {
|
|
1080
|
+
if (pendingDevBootstrap) clearTimeout(pendingDevBootstrap);
|
|
1081
|
+
pendingDevBootstrap = setTimeout(() => {
|
|
1082
|
+
runDevProjectBootstrap(cwd, template);
|
|
1083
|
+
}, 150);
|
|
1084
|
+
}
|
|
1085
|
+
async function runDevProjectBootstrap(cwd, template) {
|
|
1086
|
+
if (devBootstrapInFlight) {
|
|
1087
|
+
scheduleDevProjectBootstrap(cwd, template);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
devBootstrapInFlight = true;
|
|
1091
|
+
try {
|
|
1092
|
+
await ensureProjectScaffolded(cwd, template);
|
|
1093
|
+
await runCodegen(cwd);
|
|
1094
|
+
const currentSnapshot = createSchemaSnapshot(await loadProjectSchema(cwd));
|
|
1095
|
+
const storedSnapshot = await readStoredSnapshot(cwd);
|
|
1096
|
+
const plan = diffSchemaSnapshots(storedSnapshot, currentSnapshot);
|
|
1097
|
+
if (plan.destructiveChanges.length > 0) {
|
|
1098
|
+
console.error("Syncore dev blocked by destructive schema changes:");
|
|
1099
|
+
for (const destructiveChange of plan.destructiveChanges) console.error(`- ${destructiveChange}`);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (storedSnapshot?.hash !== currentSnapshot.hash) {
|
|
1103
|
+
await writeStoredSnapshot(cwd, currentSnapshot);
|
|
1104
|
+
if (plan.statements.length > 0 || plan.warnings.length > 0) console.log(`Schema snapshot updated (${plan.statements.length} statement(s), ${plan.warnings.length} warning(s)).`);
|
|
1105
|
+
else console.log("Schema snapshot updated.");
|
|
1106
|
+
}
|
|
1107
|
+
for (const warning of plan.warnings) console.warn(`Syncore dev warning: ${warning}`);
|
|
1108
|
+
const appliedCount = await applyProjectMigrations(cwd);
|
|
1109
|
+
console.log(`Syncore dev is ready. Codegen refreshed; ${appliedCount} migration(s) applied.`);
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.error(`Syncore dev bootstrap failed: ${formatError(error)}`);
|
|
1112
|
+
} finally {
|
|
1113
|
+
devBootstrapInFlight = false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function createDevWatchSnapshot(cwd) {
|
|
1117
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
1118
|
+
const filesToCheck = [
|
|
1119
|
+
path.join(cwd, "syncore.config.ts"),
|
|
1120
|
+
path.join(cwd, "syncore", "schema.ts"),
|
|
1121
|
+
path.join(cwd, ".gitignore"),
|
|
1122
|
+
path.join(cwd, "package.json")
|
|
1123
|
+
];
|
|
1124
|
+
for (const file of filesToCheck) if (await fileExists(file)) {
|
|
1125
|
+
const info = await stat(file);
|
|
1126
|
+
snapshot.set(file, info.mtimeMs);
|
|
1127
|
+
}
|
|
1128
|
+
for (const directory of [path.join(cwd, "syncore", "functions"), path.join(cwd, "syncore", "migrations")]) for (const file of await listFilesRecursively(directory)) {
|
|
1129
|
+
const info = await stat(file);
|
|
1130
|
+
snapshot.set(file, info.mtimeMs);
|
|
1131
|
+
}
|
|
1132
|
+
return snapshot;
|
|
1133
|
+
}
|
|
1134
|
+
async function listFilesRecursively(directory) {
|
|
1135
|
+
if (!await fileExists(directory)) return [];
|
|
1136
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1137
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
1138
|
+
const fullPath = path.join(directory, entry.name);
|
|
1139
|
+
if (entry.isDirectory()) return listFilesRecursively(fullPath);
|
|
1140
|
+
if (entry.isFile()) return [fullPath];
|
|
1141
|
+
return [];
|
|
1142
|
+
}))).flat().sort((left, right) => left.localeCompare(right));
|
|
1143
|
+
}
|
|
1144
|
+
function areDevWatchSnapshotsEqual(left, right) {
|
|
1145
|
+
if (left.size !== right.size) return false;
|
|
1146
|
+
for (const [filePath, leftTimestamp] of left) if (right.get(filePath) !== leftTimestamp) return false;
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
function decodeWebSocketPayload(payload) {
|
|
1150
|
+
if (typeof payload === "string") return payload;
|
|
1151
|
+
if (payload instanceof Buffer) return payload.toString("utf8");
|
|
1152
|
+
if (Array.isArray(payload)) return Buffer.concat(payload).toString("utf8");
|
|
1153
|
+
if (payload instanceof ArrayBuffer) return Buffer.from(payload).toString("utf8");
|
|
1154
|
+
return Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength).toString("utf8");
|
|
1155
|
+
}
|
|
1156
|
+
function isUnsupportedFts5Statement(statement, error) {
|
|
1157
|
+
if (!/using\s+fts5/i.test(statement)) return false;
|
|
1158
|
+
return error instanceof Error && /fts5/i.test(error.message);
|
|
1159
|
+
}
|
|
1160
|
+
function unwrapDefaultExport(value) {
|
|
1161
|
+
if (value && typeof value === "object" && "default" in value && value.default !== void 0) return unwrapDefaultExport(value.default);
|
|
1162
|
+
return value;
|
|
1163
|
+
}
|
|
1164
|
+
function resolvePortFromEnv(environmentVariable, fallback) {
|
|
1165
|
+
const rawValue = process.env[environmentVariable];
|
|
1166
|
+
if (!rawValue) return fallback;
|
|
1167
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
1168
|
+
if (Number.isNaN(parsed) || parsed <= 0) throw new Error(`${environmentVariable} must be a positive integer when provided.`);
|
|
1169
|
+
return parsed;
|
|
1170
|
+
}
|
|
1171
|
+
function isCliEntryPoint() {
|
|
1172
|
+
const entryPath = process.argv[1];
|
|
1173
|
+
if (!entryPath) return false;
|
|
1174
|
+
return path.resolve(entryPath) === path.resolve(fileURLToPath(import.meta.url));
|
|
1175
|
+
}
|
|
1176
|
+
function stableStringify(value) {
|
|
1177
|
+
return JSON.stringify(sortValue(value));
|
|
1178
|
+
}
|
|
1179
|
+
function sortValue(value) {
|
|
1180
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
1181
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortValue(nested)]));
|
|
1182
|
+
return value;
|
|
1183
|
+
}
|
|
1184
|
+
function quoteIdentifier(identifier) {
|
|
1185
|
+
return `"${identifier.replaceAll("\"", "\"\"")}"`;
|
|
1186
|
+
}
|
|
1187
|
+
function toSearchValue(value) {
|
|
1188
|
+
if (typeof value === "string") return value;
|
|
1189
|
+
if (value === null || value === void 0) return "";
|
|
1190
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
1191
|
+
return stableStringify(value);
|
|
1192
|
+
}
|
|
1193
|
+
//#endregion
|
|
1194
|
+
export { runSyncoreCli };
|
|
1195
|
+
|
|
1196
|
+
//# sourceMappingURL=cli.js.map
|