@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +21 -0
  3. package/src/cli.ts +56 -0
  4. package/src/commands/create.ts +259 -0
  5. package/src/sync.ts +125 -0
  6. package/src/templates/backend/.changeset/initial.md.hbs +5 -0
  7. package/src/templates/backend/README.md.hbs +46 -0
  8. package/src/templates/backend/drizzle.config.ts.hbs +10 -0
  9. package/src/templates/backend/package.json.hbs +11 -0
  10. package/src/templates/backend/src/index.ts.hbs +35 -0
  11. package/src/templates/backend/src/router.ts.hbs +46 -0
  12. package/src/templates/backend/src/schema.ts.hbs +19 -0
  13. package/src/templates/backend/src/service.ts.hbs +69 -0
  14. package/src/templates/backend/tsconfig.json +6 -0
  15. package/src/templates/common/.changeset/initial.md.hbs +5 -0
  16. package/src/templates/common/README.md.hbs +11 -0
  17. package/src/templates/common/package.json.hbs +7 -0
  18. package/src/templates/common/src/index.ts.hbs +14 -0
  19. package/src/templates/common/src/permissions.ts.hbs +17 -0
  20. package/src/templates/common/src/plugin-metadata.ts.hbs +6 -0
  21. package/src/templates/common/src/routes.ts.hbs +6 -0
  22. package/src/templates/common/src/rpc-contract.ts.hbs +60 -0
  23. package/src/templates/common/src/schemas.ts.hbs +25 -0
  24. package/src/templates/common/tsconfig.json +6 -0
  25. package/src/templates/frontend/.changeset/initial.md.hbs +5 -0
  26. package/src/templates/frontend/README.md.hbs +29 -0
  27. package/src/templates/frontend/bunfig.toml.hbs +1 -0
  28. package/src/templates/frontend/package.json.hbs +12 -0
  29. package/src/templates/frontend/playwright.config.ts.hbs +3 -0
  30. package/src/templates/frontend/src/api.ts.hbs +15 -0
  31. package/src/templates/frontend/src/components/{{pluginNamePascal}}ListPage.tsx.hbs +81 -0
  32. package/src/templates/frontend/src/index.tsx.hbs +33 -0
  33. package/src/templates/frontend/tsconfig.json.hbs +1 -0
  34. package/src/templates/node/.changeset/initial.md.hbs +5 -0
  35. package/src/templates/node/README.md.hbs +8 -0
  36. package/src/templates/node/package.json.hbs +3 -0
  37. package/src/templates/node/src/index.ts.hbs +6 -0
  38. package/src/templates/node/tsconfig.json +6 -0
  39. package/src/templates/react/.changeset/initial.md.hbs +5 -0
  40. package/src/templates/react/README.md.hbs +23 -0
  41. package/src/templates/react/package.json.hbs +4 -0
  42. package/src/templates/react/src/components/{{pluginNamePascal}}Component.tsx.hbs +12 -0
  43. package/src/templates/react/src/index.tsx.hbs +1 -0
  44. package/src/templates/react/tsconfig.json +6 -0
  45. package/src/templates.test.ts +134 -0
  46. package/src/utils/template.ts +154 -0
  47. package/src/utils/validation.ts +110 -0
  48. package/tsconfig.json +6 -0
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
2
+ import {
3
+ copyTemplate,
4
+ prepareTemplateData,
5
+ registerHelpers,
6
+ } from "./utils/template";
7
+ import { execSync } from "node:child_process";
8
+ import { rmSync, existsSync, mkdirSync } from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ const TEST_BASE_NAME = "scaffoldtest";
15
+ const TEST_SCAFFOLDS_DIR = "plugins/_test-scaffolds";
16
+
17
+ // Plugin types to test - these have the most template complexity
18
+ // Order matters: common must be scaffolded before frontend (frontend depends on common)
19
+ const PLUGIN_TYPES = ["common", "backend", "frontend"] as const;
20
+
21
+ describe("CLI Template Scaffolding", () => {
22
+ const rootDir = path.resolve(__dirname, "../../..");
23
+ const scaffoldsDir = path.join(rootDir, TEST_SCAFFOLDS_DIR);
24
+
25
+ beforeAll(() => {
26
+ registerHelpers();
27
+
28
+ // Clean up any previous test scaffolds
29
+ if (existsSync(scaffoldsDir)) {
30
+ rmSync(scaffoldsDir, { recursive: true });
31
+ }
32
+
33
+ // Ensure the test scaffolds directory exists
34
+ mkdirSync(scaffoldsDir, { recursive: true });
35
+
36
+ // Scaffold ALL plugins first (order matters for dependencies)
37
+ for (const pluginType of PLUGIN_TYPES) {
38
+ const templateData = prepareTemplateData({
39
+ baseName: TEST_BASE_NAME,
40
+ pluginType,
41
+ description: `Test ${pluginType} plugin for template validation`,
42
+ });
43
+
44
+ const templateDir = path.join(__dirname, "templates", pluginType);
45
+ const targetDir = path.join(scaffoldsDir, templateData.pluginName);
46
+
47
+ copyTemplate({
48
+ templateDir,
49
+ targetDir,
50
+ data: templateData,
51
+ });
52
+ }
53
+
54
+ // Run bun install ONCE after all plugins are scaffolded
55
+ // This ensures workspace dependencies are resolved correctly
56
+ execSync("bun install", {
57
+ cwd: rootDir,
58
+ stdio: "pipe",
59
+ timeout: 120_000,
60
+ });
61
+ });
62
+
63
+ afterAll(() => {
64
+ // Cleanup all test packages
65
+ if (existsSync(scaffoldsDir)) {
66
+ rmSync(scaffoldsDir, { recursive: true });
67
+ }
68
+
69
+ // Re-run bun install to remove stale entries from bun.lock
70
+ execSync("bun install", {
71
+ cwd: rootDir,
72
+ stdio: "pipe",
73
+ timeout: 60_000,
74
+ });
75
+ });
76
+
77
+ for (const pluginType of PLUGIN_TYPES) {
78
+ const pluginName = `${TEST_BASE_NAME}-${pluginType}`;
79
+ const targetDir = path.join(scaffoldsDir, pluginName);
80
+
81
+ describe(`${pluginType} plugin template`, () => {
82
+ it("should have scaffolded files", () => {
83
+ expect(existsSync(path.join(targetDir, "package.json"))).toBe(true);
84
+ expect(existsSync(path.join(targetDir, "src"))).toBe(true);
85
+ expect(existsSync(path.join(targetDir, "tsconfig.json"))).toBe(true);
86
+ });
87
+
88
+ it(
89
+ "should pass typecheck",
90
+ () => {
91
+ try {
92
+ execSync(
93
+ `bun run --filter '@checkstack/${pluginName}' typecheck`,
94
+ {
95
+ cwd: rootDir,
96
+ stdio: "pipe",
97
+ timeout: 60_000,
98
+ }
99
+ );
100
+ } catch (error) {
101
+ const execError = error as { stderr?: Buffer; stdout?: Buffer };
102
+ const stderr = execError.stderr?.toString() ?? "";
103
+ const stdout = execError.stdout?.toString() ?? "";
104
+ throw new Error(
105
+ `Typecheck failed for ${pluginName}:\n${stderr}\n${stdout}`
106
+ );
107
+ }
108
+ },
109
+ { timeout: 30_000 }
110
+ );
111
+
112
+ it(
113
+ "should pass lint",
114
+ () => {
115
+ try {
116
+ execSync(`bun run eslint ${TEST_SCAFFOLDS_DIR}/${pluginName}`, {
117
+ cwd: rootDir,
118
+ stdio: "pipe",
119
+ timeout: 60_000,
120
+ });
121
+ } catch (error) {
122
+ const execError = error as { stderr?: Buffer; stdout?: Buffer };
123
+ const stderr = execError.stderr?.toString() ?? "";
124
+ const stdout = execError.stdout?.toString() ?? "";
125
+ throw new Error(
126
+ `Lint failed for ${pluginName}:\n${stderr}\n${stdout}`
127
+ );
128
+ }
129
+ },
130
+ { timeout: 30_000 }
131
+ );
132
+ });
133
+ }
134
+ });
@@ -0,0 +1,154 @@
1
+ import Handlebars from "handlebars";
2
+ import {
3
+ readFileSync,
4
+ writeFileSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ statSync,
8
+ } from "node:fs";
9
+ import path from "node:path";
10
+
11
+ export interface TemplateData {
12
+ pluginName: string;
13
+ pluginBaseName: string;
14
+ pluginNamePascal: string;
15
+ pluginNameCamel: string;
16
+ pluginDescription: string;
17
+ pluginId: string;
18
+ pluginType: string;
19
+ currentYear: number;
20
+ }
21
+
22
+ /**
23
+ * Register custom Handlebars helpers for common case transformations
24
+ */
25
+ export function registerHelpers() {
26
+ Handlebars.registerHelper("pascalCase", (value: string) => {
27
+ return value
28
+ .split("-")
29
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
30
+ .join("");
31
+ });
32
+
33
+ Handlebars.registerHelper("camelCase", (value: string) => {
34
+ const pascal = value
35
+ .split("-")
36
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
37
+ .join("");
38
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
39
+ });
40
+
41
+ Handlebars.registerHelper("kebabCase", (value: string) => {
42
+ return value.toLowerCase().replaceAll(/\s+/g, "-");
43
+ });
44
+
45
+ Handlebars.registerHelper("year", () => {
46
+ return new Date().getFullYear();
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Process a single template file using Handlebars
52
+ */
53
+ export function processTemplate(content: string, data: TemplateData): string {
54
+ const template = Handlebars.compile(content);
55
+ return template(data);
56
+ }
57
+
58
+ /**
59
+ * Recursively copy template directory and process .hbs files
60
+ */
61
+ export function copyTemplate({
62
+ templateDir,
63
+ targetDir,
64
+ data,
65
+ }: {
66
+ templateDir: string;
67
+ targetDir: string;
68
+ data: TemplateData;
69
+ }): string[] {
70
+ const createdFiles: string[] = [];
71
+
72
+ // Create target directory if it doesn't exist
73
+ if (!statSync(targetDir, { throwIfNoEntry: false })) {
74
+ mkdirSync(targetDir, { recursive: true });
75
+ }
76
+
77
+ // Read all items in template directory
78
+ const items = readdirSync(templateDir);
79
+
80
+ for (const item of items) {
81
+ const templatePath = path.join(templateDir, item);
82
+ const stat = statSync(templatePath);
83
+
84
+ if (stat.isDirectory()) {
85
+ // Recursively copy directories
86
+ const newTargetDir = path.join(targetDir, item);
87
+ const subFiles = copyTemplate({
88
+ templateDir: templatePath,
89
+ targetDir: newTargetDir,
90
+ data,
91
+ });
92
+ createdFiles.push(...subFiles);
93
+ } else if (stat.isFile()) {
94
+ // Process files
95
+ const isTemplate = item.endsWith(".hbs");
96
+ let targetFileName = isTemplate ? item.slice(0, -4) : item; // Remove .hbs extension
97
+
98
+ // Process Handlebars patterns in filename
99
+ if (targetFileName.includes("{{")) {
100
+ targetFileName = processTemplate(targetFileName, data);
101
+ }
102
+
103
+ const targetPath = path.join(targetDir, targetFileName);
104
+
105
+ if (isTemplate) {
106
+ // Process Handlebars template
107
+ const content = readFileSync(templatePath, "utf8");
108
+ const processed = processTemplate(content, data);
109
+ writeFileSync(targetPath, processed, "utf8");
110
+ } else {
111
+ // Copy non-template files as-is
112
+ const content = readFileSync(templatePath);
113
+ writeFileSync(targetPath, content);
114
+ }
115
+
116
+ createdFiles.push(targetPath);
117
+ }
118
+ }
119
+
120
+ return createdFiles;
121
+ }
122
+
123
+ /**
124
+ * Prepare template data from user inputs
125
+ */
126
+ export function prepareTemplateData({
127
+ baseName,
128
+ pluginType,
129
+ description,
130
+ }: {
131
+ baseName: string;
132
+ pluginType: string;
133
+ description: string;
134
+ }): TemplateData {
135
+ const pluginName = `${baseName}-${pluginType}`;
136
+ const pluginNamePascal = baseName
137
+ .split("-")
138
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
139
+ .join("");
140
+
141
+ const pluginNameCamel =
142
+ pluginNamePascal.charAt(0).toLowerCase() + pluginNamePascal.slice(1);
143
+
144
+ return {
145
+ pluginName,
146
+ pluginBaseName: baseName,
147
+ pluginNamePascal,
148
+ pluginNameCamel,
149
+ pluginDescription: description,
150
+ pluginId: pluginName,
151
+ pluginType,
152
+ currentYear: new Date().getFullYear(),
153
+ };
154
+ }
@@ -0,0 +1,110 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const RESERVED_NAMES = new Set(["checkstack", "core", "api", "common"]);
5
+
6
+ /**
7
+ * Validate plugin name follows naming conventions
8
+ */
9
+ export function validatePluginName(name: string): {
10
+ valid: boolean;
11
+ error?: string;
12
+ } {
13
+ // Must not be empty
14
+ if (!name || name.trim().length === 0) {
15
+ return { valid: false, error: "Plugin name cannot be empty" };
16
+ }
17
+
18
+ // Must be lowercase
19
+ if (name !== name.toLowerCase()) {
20
+ return {
21
+ valid: false,
22
+ error: "Plugin name must be lowercase (use hyphens for word separation)",
23
+ };
24
+ }
25
+
26
+ // Can only contain lowercase letters, numbers, and hyphens
27
+ if (!/^[\da-z-]+$/.test(name)) {
28
+ return {
29
+ valid: false,
30
+ error:
31
+ "Plugin name can only contain lowercase letters, numbers, and hyphens",
32
+ };
33
+ }
34
+
35
+ // Cannot start or end with hyphen
36
+ if (name.startsWith("-") || name.endsWith("-")) {
37
+ return {
38
+ valid: false,
39
+ error: "Plugin name cannot start or end with a hyphen",
40
+ };
41
+ }
42
+
43
+ // Cannot have consecutive hyphens
44
+ if (name.includes("--")) {
45
+ return {
46
+ valid: false,
47
+ error: "Plugin name cannot contain consecutive hyphens",
48
+ };
49
+ }
50
+
51
+ // Check reserved names
52
+ if (RESERVED_NAMES.has(name)) {
53
+ return {
54
+ valid: false,
55
+ error: `'${name}' is a reserved name and cannot be used`,
56
+ };
57
+ }
58
+
59
+ return { valid: true };
60
+ }
61
+
62
+ /**
63
+ * Check if a plugin already exists
64
+ */
65
+ export function pluginExists({
66
+ baseName,
67
+ pluginType,
68
+ rootDir,
69
+ }: {
70
+ baseName: string;
71
+ pluginType: string;
72
+ rootDir: string;
73
+ }): boolean {
74
+ const pluginName = `${baseName}-${pluginType}`;
75
+ const pluginPath = path.join(rootDir, "plugins", pluginName);
76
+ return existsSync(pluginPath);
77
+ }
78
+
79
+ /**
80
+ * Check if a package already exists in core/
81
+ */
82
+ export function packageExists({
83
+ baseName,
84
+ pluginType,
85
+ rootDir,
86
+ }: {
87
+ baseName: string;
88
+ pluginType: string;
89
+ rootDir: string;
90
+ }): boolean {
91
+ const packageName = `${baseName}-${pluginType}`;
92
+ const packagePath = path.join(rootDir, "core", packageName);
93
+ return existsSync(packagePath);
94
+ }
95
+
96
+ /**
97
+ * Extract base name from full plugin name if provided
98
+ * e.g., "catalog-backend" -> "catalog"
99
+ */
100
+ export function extractBaseName(fullName: string): string {
101
+ const parts = fullName.split("-");
102
+ if (parts.length > 1) {
103
+ const lastPart = parts.at(-1);
104
+ const knownTypes = ["backend", "frontend", "common", "node", "react"];
105
+ if (lastPart && knownTypes.includes(lastPart)) {
106
+ return parts.slice(0, -1).join("-");
107
+ }
108
+ }
109
+ return fullName;
110
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }