@checkstack/scripts 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +21 -0
- package/src/cli.ts +56 -0
- package/src/commands/create.ts +259 -0
- package/src/sync.ts +125 -0
- package/src/templates/backend/.changeset/initial.md.hbs +5 -0
- package/src/templates/backend/README.md.hbs +46 -0
- package/src/templates/backend/drizzle.config.ts.hbs +10 -0
- package/src/templates/backend/package.json.hbs +11 -0
- package/src/templates/backend/src/index.ts.hbs +35 -0
- package/src/templates/backend/src/router.ts.hbs +46 -0
- package/src/templates/backend/src/schema.ts.hbs +19 -0
- package/src/templates/backend/src/service.ts.hbs +69 -0
- package/src/templates/backend/tsconfig.json +6 -0
- package/src/templates/common/.changeset/initial.md.hbs +5 -0
- package/src/templates/common/README.md.hbs +11 -0
- package/src/templates/common/package.json.hbs +7 -0
- package/src/templates/common/src/index.ts.hbs +14 -0
- package/src/templates/common/src/permissions.ts.hbs +17 -0
- package/src/templates/common/src/plugin-metadata.ts.hbs +6 -0
- package/src/templates/common/src/routes.ts.hbs +6 -0
- package/src/templates/common/src/rpc-contract.ts.hbs +60 -0
- package/src/templates/common/src/schemas.ts.hbs +25 -0
- package/src/templates/common/tsconfig.json +6 -0
- package/src/templates/frontend/.changeset/initial.md.hbs +5 -0
- package/src/templates/frontend/README.md.hbs +29 -0
- package/src/templates/frontend/bunfig.toml.hbs +1 -0
- package/src/templates/frontend/package.json.hbs +12 -0
- package/src/templates/frontend/playwright.config.ts.hbs +3 -0
- package/src/templates/frontend/src/api.ts.hbs +15 -0
- package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +81 -0
- package/src/templates/frontend/src/index.tsx.hbs +33 -0
- package/src/templates/frontend/tsconfig.json.hbs +1 -0
- package/src/templates/node/.changeset/initial.md.hbs +5 -0
- package/src/templates/node/README.md.hbs +8 -0
- package/src/templates/node/package.json.hbs +3 -0
- package/src/templates/node/src/index.ts.hbs +6 -0
- package/src/templates/node/tsconfig.json +6 -0
- package/src/templates/react/.changeset/initial.md.hbs +5 -0
- package/src/templates/react/README.md.hbs +23 -0
- package/src/templates/react/package.json.hbs +4 -0
- package/src/templates/react/src/components/{{pluginNamePascal}}Component.tsx.hbs +12 -0
- package/src/templates/react/src/index.tsx.hbs +1 -0
- package/src/templates/react/tsconfig.json +6 -0
- package/src/templates.test.ts +134 -0
- package/src/utils/template.ts +154 -0
- package/src/utils/validation.ts +110 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/scripts",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"bin": {
|
|
5
|
+
"checkstack-scripts": "./src/cli.ts"
|
|
6
|
+
},
|
|
7
|
+
"scripts": {
|
|
8
|
+
"sync": "bun run src/sync.ts",
|
|
9
|
+
"typecheck": "tsc --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"inquirer": "^8.1.0",
|
|
13
|
+
"handlebars": "^4.7.8"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
17
|
+
"@types/inquirer": "^8.2.10",
|
|
18
|
+
"@types/handlebars": "^4.1.0",
|
|
19
|
+
"typescript": "^5.0.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const command = process.argv[2];
|
|
6
|
+
|
|
7
|
+
if (!command) {
|
|
8
|
+
console.log("Usage: checkstack-scripts <command>");
|
|
9
|
+
console.log("\nCommands:");
|
|
10
|
+
console.log(" create - Create a new plugin interactively");
|
|
11
|
+
console.log(" sync - Synchronize package configurations");
|
|
12
|
+
console.log(" generate - Generate migrations and strip public schema");
|
|
13
|
+
console.log(" typecheck - Run TypeScript type checking");
|
|
14
|
+
console.log(" lint - Run linting checks");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const rootDir = process.cwd();
|
|
19
|
+
|
|
20
|
+
if (command === "sync") {
|
|
21
|
+
const result = spawnSync(
|
|
22
|
+
"bun",
|
|
23
|
+
["run", path.join(rootDir, "core/scripts/src/sync.ts")],
|
|
24
|
+
{
|
|
25
|
+
stdio: "inherit",
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
process.exit(result.status ?? 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (command === "create") {
|
|
32
|
+
const result = spawnSync(
|
|
33
|
+
"bun",
|
|
34
|
+
["run", path.join(rootDir, "core/scripts/src/commands/create.ts")],
|
|
35
|
+
{
|
|
36
|
+
stdio: "inherit",
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
process.exit(result.status ?? 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command === "generate") {
|
|
43
|
+
const result = spawnSync(
|
|
44
|
+
"bun",
|
|
45
|
+
[path.join(rootDir, "core/scripts/src/commands/generate.ts")],
|
|
46
|
+
{
|
|
47
|
+
cwd: rootDir, // Keep cwd as root, script will handle paths
|
|
48
|
+
stdio: "inherit",
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
process.exit(result.status ?? 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Fallback for other scripts if we want to centralize their execution logic
|
|
55
|
+
console.error(`Unknown command: ${command}`);
|
|
56
|
+
process.exit(1);
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
validatePluginName,
|
|
6
|
+
pluginExists,
|
|
7
|
+
packageExists,
|
|
8
|
+
extractBaseName,
|
|
9
|
+
} from "../utils/validation";
|
|
10
|
+
import {
|
|
11
|
+
registerHelpers,
|
|
12
|
+
copyTemplate,
|
|
13
|
+
prepareTemplateData,
|
|
14
|
+
} from "../utils/template";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
interface PluginTypeChoice {
|
|
20
|
+
name: string;
|
|
21
|
+
value: string;
|
|
22
|
+
description: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LocationChoice {
|
|
26
|
+
name: string;
|
|
27
|
+
value: "core" | "plugins";
|
|
28
|
+
description: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PACKAGE_LOCATIONS: LocationChoice[] = [
|
|
32
|
+
{
|
|
33
|
+
name: "core/ - Core platform component (essential, non-replaceable)",
|
|
34
|
+
value: "core",
|
|
35
|
+
description: "Core platform component",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "plugins/ - Replaceable provider (optional, swappable)",
|
|
39
|
+
value: "plugins",
|
|
40
|
+
description: "Replaceable provider plugin",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const PLUGIN_TYPES: PluginTypeChoice[] = [
|
|
45
|
+
{
|
|
46
|
+
name: "backend - Backend plugin with oRPC router",
|
|
47
|
+
value: "backend",
|
|
48
|
+
description: "Backend plugin with oRPC router",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "frontend - Frontend plugin with React components",
|
|
52
|
+
value: "frontend",
|
|
53
|
+
description: "Frontend plugin with React components",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "common - Common plugin with contracts and types",
|
|
57
|
+
value: "common",
|
|
58
|
+
description: "Common plugin with contracts and types",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "node - Node.js utility plugin",
|
|
62
|
+
value: "node",
|
|
63
|
+
description: "Node.js utility plugin",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "react - React component library plugin",
|
|
67
|
+
value: "react",
|
|
68
|
+
description: "React component library plugin",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export async function createCommand() {
|
|
73
|
+
console.log("\n🚀 Checkstack Package Generator\n");
|
|
74
|
+
|
|
75
|
+
// Register Handlebars helpers
|
|
76
|
+
registerHelpers();
|
|
77
|
+
|
|
78
|
+
// Prompt for package location
|
|
79
|
+
const { packageLocation } = await inquirer.prompt<{
|
|
80
|
+
packageLocation: "core" | "plugins";
|
|
81
|
+
}>([
|
|
82
|
+
{
|
|
83
|
+
type: "list",
|
|
84
|
+
name: "packageLocation",
|
|
85
|
+
message: "Where should the package be created?",
|
|
86
|
+
choices: PACKAGE_LOCATIONS,
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// Show guidance based on choice
|
|
91
|
+
if (packageLocation === "core") {
|
|
92
|
+
console.log(
|
|
93
|
+
"\n📦 Core packages are essential platform components that cannot be removed."
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
" Examples: auth, catalog, notifications, queue, healthcheck, theme\n"
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
console.log(
|
|
100
|
+
"\n🔌 Plugins are replaceable providers that can be swapped or removed."
|
|
101
|
+
);
|
|
102
|
+
console.log(
|
|
103
|
+
" Examples: auth-github, auth-ldap, queue-bullmq, healthcheck-http\n"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Prompt for plugin type
|
|
108
|
+
const { pluginType } = await inquirer.prompt<{ pluginType: string }>([
|
|
109
|
+
{
|
|
110
|
+
type: "list",
|
|
111
|
+
name: "pluginType",
|
|
112
|
+
message: "What type of package do you want to create?",
|
|
113
|
+
choices: PLUGIN_TYPES,
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// Prompt for plugin name
|
|
118
|
+
const { pluginBaseName } = await inquirer.prompt<{ pluginBaseName: string }>([
|
|
119
|
+
{
|
|
120
|
+
type: "input",
|
|
121
|
+
name: "pluginBaseName",
|
|
122
|
+
message: `Package name (e.g., 'catalog' for 'catalog-${pluginType}'):`,
|
|
123
|
+
validate: (input: string) => {
|
|
124
|
+
const extracted = extractBaseName(input);
|
|
125
|
+
const validation = validatePluginName(extracted);
|
|
126
|
+
return validation.valid || validation.error || false;
|
|
127
|
+
},
|
|
128
|
+
filter: (input: string) => extractBaseName(input.trim()),
|
|
129
|
+
},
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// Check if plugin already exists
|
|
133
|
+
const rootDir = process.cwd();
|
|
134
|
+
const existsInPlugins = pluginExists({
|
|
135
|
+
baseName: pluginBaseName,
|
|
136
|
+
pluginType,
|
|
137
|
+
rootDir,
|
|
138
|
+
});
|
|
139
|
+
const existsInPackages = packageExists({
|
|
140
|
+
baseName: pluginBaseName,
|
|
141
|
+
pluginType,
|
|
142
|
+
rootDir,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (existsInPlugins || existsInPackages) {
|
|
146
|
+
const existingLocation = existsInPackages ? "core" : "plugins";
|
|
147
|
+
console.error(
|
|
148
|
+
`\n❌ Error: '${pluginBaseName}-${pluginType}' already exists in ${existingLocation}/!\n`
|
|
149
|
+
);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Prompt for description
|
|
154
|
+
const { description } = await inquirer.prompt<{ description: string }>([
|
|
155
|
+
{
|
|
156
|
+
type: "input",
|
|
157
|
+
name: "description",
|
|
158
|
+
message: "Package description (optional):",
|
|
159
|
+
default: `${
|
|
160
|
+
pluginBaseName.charAt(0).toUpperCase() + pluginBaseName.slice(1)
|
|
161
|
+
} ${packageLocation === "core" ? "package" : "plugin"}`,
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
// Prepare template data
|
|
166
|
+
const templateData = prepareTemplateData({
|
|
167
|
+
baseName: pluginBaseName,
|
|
168
|
+
pluginType,
|
|
169
|
+
description,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Confirm before generation
|
|
173
|
+
const locationLabel = packageLocation === "core" ? "package" : "plugin";
|
|
174
|
+
const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
|
|
175
|
+
{
|
|
176
|
+
type: "confirm",
|
|
177
|
+
name: "confirmed",
|
|
178
|
+
message: `Create ${pluginType} ${locationLabel} '${templateData.pluginName}' in ${packageLocation}/?`,
|
|
179
|
+
default: true,
|
|
180
|
+
},
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
if (!confirmed) {
|
|
184
|
+
console.log("\n❌ Creation cancelled.\n");
|
|
185
|
+
process.exit(0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Generate plugin from template
|
|
189
|
+
console.log(
|
|
190
|
+
`\n📦 Creating ${pluginType} ${locationLabel}: ${templateData.pluginName}`
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const templateDir = path.join(__dirname, "..", "templates", pluginType);
|
|
194
|
+
const targetDir = path.join(
|
|
195
|
+
rootDir,
|
|
196
|
+
packageLocation,
|
|
197
|
+
templateData.pluginName
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const createdFiles = copyTemplate({
|
|
202
|
+
templateDir,
|
|
203
|
+
targetDir,
|
|
204
|
+
data: templateData,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
console.log(
|
|
208
|
+
` ✓ Created directory: ${packageLocation}/${templateData.pluginName}`
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Show created files
|
|
212
|
+
const relativeFiles = createdFiles.map((file) =>
|
|
213
|
+
path.relative(targetDir, file)
|
|
214
|
+
);
|
|
215
|
+
for (const file of relativeFiles) {
|
|
216
|
+
console.log(` ✓ Generated ${file}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Success message with next steps
|
|
220
|
+
console.log(
|
|
221
|
+
`\n✅ ${
|
|
222
|
+
locationLabel.charAt(0).toUpperCase() + locationLabel.slice(1)
|
|
223
|
+
} created successfully!\n`
|
|
224
|
+
);
|
|
225
|
+
console.log("Next steps:");
|
|
226
|
+
console.log(` 1. cd ${packageLocation}/${templateData.pluginName}`);
|
|
227
|
+
console.log(` 2. bun install`);
|
|
228
|
+
|
|
229
|
+
// Type-specific instructions
|
|
230
|
+
switch (pluginType) {
|
|
231
|
+
case "backend": {
|
|
232
|
+
console.log(` 3. Update src/schema.ts with your database schema`);
|
|
233
|
+
console.log(` 4. Update src/router.ts with your RPC procedures`);
|
|
234
|
+
console.log(` 5. Generate migrations: bun run drizzle-kit generate`);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "frontend": {
|
|
238
|
+
console.log(` 3. Update src/api.ts with your API client`);
|
|
239
|
+
console.log(` 4. Create your page components in src/components/`);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "common": {
|
|
243
|
+
console.log(` 3. Define your permissions in src/permissions.ts`);
|
|
244
|
+
console.log(` 4. Define your schemas in src/schemas.ts`);
|
|
245
|
+
console.log(` 5. Define your contract in src/rpc-contract.ts`);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
// No additional steps for node and react types
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log(` 6. Review the initial changeset in .changeset/initial.md\n`);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(`\n❌ Error creating ${locationLabel}: ${error}\n`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Run the command when executed directly
|
|
259
|
+
await createCommand();
|
package/src/sync.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Glob } from "bun";
|
|
4
|
+
|
|
5
|
+
function stripComments(text: string) {
|
|
6
|
+
return text.replaceAll(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const rootDir = process.cwd();
|
|
10
|
+
|
|
11
|
+
// Standard scripts we want to share
|
|
12
|
+
const STANDARD_SCRIPTS: Record<string, string> = {
|
|
13
|
+
typecheck: "tsc --noEmit",
|
|
14
|
+
lint: "bun run lint:code",
|
|
15
|
+
"lint:code": "eslint . --max-warnings 0",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const packageGlob = new Glob("{packages,plugins}/*/package.json");
|
|
19
|
+
const packages = [...packageGlob.scanSync({ cwd: rootDir })];
|
|
20
|
+
|
|
21
|
+
console.log(`Checking ${packages.length} packages for synchronization...`);
|
|
22
|
+
|
|
23
|
+
for (const pkgPath of packages) {
|
|
24
|
+
const fullPkgPath = path.join(rootDir, pkgPath);
|
|
25
|
+
const pkgDir = path.join(rootDir, pkgPath.replaceAll("/package.json", ""));
|
|
26
|
+
const tsconfigPath = path.join(pkgDir, "tsconfig.json");
|
|
27
|
+
|
|
28
|
+
const pkgContent = readFileSync(fullPkgPath, "utf8");
|
|
29
|
+
let pkg;
|
|
30
|
+
try {
|
|
31
|
+
pkg = JSON.parse(pkgContent);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(`Failed to parse ${fullPkgPath}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
pkg.name === "@checkstack/scripts" ||
|
|
39
|
+
pkg.name === "@checkstack/tsconfig"
|
|
40
|
+
)
|
|
41
|
+
continue;
|
|
42
|
+
|
|
43
|
+
let pkgChanged = false;
|
|
44
|
+
pkg.scripts = pkg.scripts || {};
|
|
45
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
46
|
+
|
|
47
|
+
// Ensure @checkstack/scripts is present
|
|
48
|
+
if (pkg.devDependencies["@checkstack/scripts"] !== "workspace:*") {
|
|
49
|
+
console.log(
|
|
50
|
+
` [${pkg.name}] Adding @checkstack/scripts to devDependencies`
|
|
51
|
+
);
|
|
52
|
+
pkg.devDependencies["@checkstack/scripts"] = "workspace:*";
|
|
53
|
+
pkgChanged = true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Update scripts
|
|
57
|
+
for (const [name, script] of Object.entries(STANDARD_SCRIPTS)) {
|
|
58
|
+
if (pkg.scripts[name] !== script) {
|
|
59
|
+
console.log(` [${pkg.name}] Updating script: ${name}`);
|
|
60
|
+
pkg.scripts[name] = script;
|
|
61
|
+
pkgChanged = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (pkgChanged) {
|
|
66
|
+
writeFileSync(fullPkgPath, JSON.stringify(pkg, undefined, 2) + "\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Manage tsconfig.json
|
|
70
|
+
if (existsSync(tsconfigPath)) {
|
|
71
|
+
const tsconfigContent = readFileSync(tsconfigPath, "utf8");
|
|
72
|
+
let tsconfig;
|
|
73
|
+
try {
|
|
74
|
+
tsconfig = JSON.parse(stripComments(tsconfigContent));
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let tsconfigChanged = false;
|
|
80
|
+
|
|
81
|
+
// Determine correct configType
|
|
82
|
+
let configType = "backend.json";
|
|
83
|
+
const isFrontend =
|
|
84
|
+
pkgPath.includes("frontend") ||
|
|
85
|
+
pkg.name.match(/frontend|ui|dashboard/) ||
|
|
86
|
+
(pkg.dependencies &&
|
|
87
|
+
(pkg.dependencies["react"] || pkg.dependencies["vite"]));
|
|
88
|
+
const isCommon = pkgPath.includes("common") || pkg.name.includes("common");
|
|
89
|
+
|
|
90
|
+
if (isFrontend) {
|
|
91
|
+
configType = "frontend.json";
|
|
92
|
+
} else if (isCommon) {
|
|
93
|
+
configType = "common.json";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const expectedExtends = `@checkstack/tsconfig/${configType}`;
|
|
97
|
+
if (tsconfig.extends !== expectedExtends) {
|
|
98
|
+
console.log(
|
|
99
|
+
` [${pkg.name}] Updating tsconfig extends to ${expectedExtends}`
|
|
100
|
+
);
|
|
101
|
+
tsconfig.extends = expectedExtends;
|
|
102
|
+
tsconfigChanged = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Repair corrupted include path
|
|
106
|
+
if (Array.isArray(tsconfig.include) && tsconfig.include.includes("src*")) {
|
|
107
|
+
console.log(
|
|
108
|
+
` [${pkg.name}] Fixing corrupted include path in tsconfig.json`
|
|
109
|
+
);
|
|
110
|
+
tsconfig.include = tsconfig.include.map((i: string) =>
|
|
111
|
+
i === "src*" ? "src" : i
|
|
112
|
+
);
|
|
113
|
+
tsconfigChanged = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (tsconfigChanged) {
|
|
117
|
+
writeFileSync(
|
|
118
|
+
tsconfigPath,
|
|
119
|
+
JSON.stringify(tsconfig, undefined, 2) + "\n"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("Synchronization complete!");
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# {{pluginNamePascal}} Backend
|
|
2
|
+
|
|
3
|
+
Backend plugin for {{pluginNamePascal}}. Implements the oRPC contract with database persistence.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `src/index.ts` - Plugin entry point
|
|
8
|
+
- `src/router.ts` - oRPC router implementation
|
|
9
|
+
- `src/service.ts` - Business logic layer
|
|
10
|
+
- `src/schema.ts` - Drizzle database schema
|
|
11
|
+
- `drizzle.config.ts` - Drizzle Kit configuration
|
|
12
|
+
- `migrations/` - Database migrations (generated)
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
### Generate Migration
|
|
17
|
+
|
|
18
|
+
After modifying `src/schema.ts`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun run drizzle-kit generate
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Run Migration
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun run drizzle-kit migrate
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Type Check
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun run typecheck
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Lint
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun run lint
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Testing
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun test
|
|
46
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
schema: "./src/schema.ts",
|
|
5
|
+
out: "./migrations",
|
|
6
|
+
dialect: "postgresql",
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env.DATABASE_URL || "postgresql://localhost:5432/checkstack",
|
|
9
|
+
},
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{ "name": "@checkstack/{{pluginName}}", "version": "0.0.1",
|
|
2
|
+
"description": "{{pluginDescription}}", "type": "module", "exports": { ".": {
|
|
3
|
+
"import": "./src/index.ts" } }, "scripts": { "typecheck": "tsc --noEmit",
|
|
4
|
+
"lint": "bun run lint:code", "lint:code": "eslint . --max-warnings 0", "test":
|
|
5
|
+
"bun test" }, "dependencies": { "@checkstack/backend-api": "workspace:*",
|
|
6
|
+
"@checkstack/common": "workspace:*", "@checkstack/{{pluginBaseName}}-common":
|
|
7
|
+
"workspace:*", "@orpc/server": "^1.13.2", "drizzle-orm": "^0.45.1" },
|
|
8
|
+
"devDependencies": { "@checkstack/tsconfig": "workspace:*",
|
|
9
|
+
"drizzle-kit": "^0.28.1", "@checkstack/drizzle-helper": "workspace:*",
|
|
10
|
+
"@checkstack/test-utils-backend": "workspace:*", "typescript": "^5.7.2" }
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBackendPlugin,
|
|
3
|
+
coreServices,
|
|
4
|
+
} from "@checkstack/backend-api";
|
|
5
|
+
import {
|
|
6
|
+
permissionList,
|
|
7
|
+
pluginMetadata,
|
|
8
|
+
{{pluginNameCamel}}Contract,
|
|
9
|
+
} from "@checkstack/{{pluginBaseName}}-common";
|
|
10
|
+
import * as schema from "./schema";
|
|
11
|
+
import { create{{pluginNamePascal}}Router } from "./router";
|
|
12
|
+
|
|
13
|
+
export default createBackendPlugin({
|
|
14
|
+
metadata: pluginMetadata,
|
|
15
|
+
register(env) {
|
|
16
|
+
env.registerPermissions(permissionList);
|
|
17
|
+
|
|
18
|
+
env.registerInit({
|
|
19
|
+
schema,
|
|
20
|
+
deps: {
|
|
21
|
+
rpc: coreServices.rpc,
|
|
22
|
+
logger: coreServices.logger,
|
|
23
|
+
},
|
|
24
|
+
init: async ({ database, rpc, logger }) => {
|
|
25
|
+
logger.debug("Initializing {{pluginNamePascal}} Backend...");
|
|
26
|
+
|
|
27
|
+
// Create and register router with plugin-scoped database
|
|
28
|
+
const router = create{{pluginNamePascal}}Router({ database });
|
|
29
|
+
rpc.registerRouter(router, {{pluginNameCamel}}Contract);
|
|
30
|
+
|
|
31
|
+
logger.debug("✅ {{pluginNamePascal}} Backend initialized.");
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { implement } from "@orpc/server";
|
|
2
|
+
import { autoAuthMiddleware, type RpcContext } from "@checkstack/backend-api";
|
|
3
|
+
import { {{pluginNameCamel}}Contract } from "@checkstack/{{pluginBaseName}}-common";
|
|
4
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
5
|
+
import type * as schema from "./schema";
|
|
6
|
+
import { {{pluginNamePascal}}Service } from "./service";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates the {{pluginBaseName}} router using contract-based implementation.
|
|
10
|
+
*
|
|
11
|
+
* Auth and permissions are automatically enforced via autoAuthMiddleware
|
|
12
|
+
* based on the contract's meta.userType and meta.permissions.
|
|
13
|
+
*/
|
|
14
|
+
const os = implement({{pluginNameCamel}}Contract)
|
|
15
|
+
.$context<RpcContext>()
|
|
16
|
+
.use(autoAuthMiddleware);
|
|
17
|
+
|
|
18
|
+
export function create{{pluginNamePascal}}Router({
|
|
19
|
+
database,
|
|
20
|
+
}: {
|
|
21
|
+
database: NodePgDatabase<typeof schema>;
|
|
22
|
+
}) {
|
|
23
|
+
const service = new {{pluginNamePascal}}Service(database);
|
|
24
|
+
|
|
25
|
+
return os.router({
|
|
26
|
+
getItems: os.getItems.handler(async () => {
|
|
27
|
+
return await service.getItems();
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
getItem: os.getItem.handler(async ({ input }) => {
|
|
31
|
+
return await service.getItem(input);
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
createItem: os.createItem.handler(async ({ input }) => {
|
|
35
|
+
return await service.createItem(input);
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
updateItem: os.updateItem.handler(async ({ input }) => {
|
|
39
|
+
return await service.updateItem(input.id, input.data);
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
deleteItem: os.deleteItem.handler(async ({ input }) => {
|
|
43
|
+
await service.deleteItem(input);
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin database schema.
|
|
5
|
+
*
|
|
6
|
+
* Tables use `pgTable()` for schema-agnostic definitions.
|
|
7
|
+
* At runtime, the plugin's database connection uses `search_path`
|
|
8
|
+
* to route all queries to the plugin's isolated schema.
|
|
9
|
+
*/
|
|
10
|
+
export const {{pluginNameCamel}}Items = pgTable("items", {
|
|
11
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
12
|
+
name: text("name").notNull(),
|
|
13
|
+
description: text("description"),
|
|
14
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
15
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type {{pluginNamePascal}}Item = typeof {{pluginNameCamel}}Items.$inferSelect;
|
|
19
|
+
export type New{{pluginNamePascal}}Item = typeof {{pluginNameCamel}}Items.$inferInsert;
|