@nakedev/go-scaffold 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 +223 -0
- package/bin/go-scaffold.js +2 -0
- package/dist/commands/create.js +57 -0
- package/dist/commands/generate.js +97 -0
- package/dist/commands/method.js +70 -0
- package/dist/commands/remove.js +72 -0
- package/dist/index.js +138 -0
- package/dist/prompts/create-wizard.js +43 -0
- package/dist/prompts/generate-wizard.js +68 -0
- package/dist/templates/create-manifest.js +110 -0
- package/dist/templates/module-manifest.js +28 -0
- package/dist/types.js +2 -0
- package/dist/utils/config.js +53 -0
- package/dist/utils/main-patcher.js +59 -0
- package/dist/utils/marker-patch.js +63 -0
- package/dist/utils/method-patcher.js +271 -0
- package/dist/utils/migrations.js +17 -0
- package/dist/utils/module-paths.js +33 -0
- package/dist/utils/naming.js +160 -0
- package/dist/utils/openapi-patcher.js +47 -0
- package/dist/utils/template-renderer.js +51 -0
- package/package.json +49 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +83 -0
- package/templates/create/base/.env.example.hbs +7 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +46 -0
- package/templates/create/base/.gitignore.hbs +5 -0
- package/templates/create/base/.golangci.yml.hbs +32 -0
- package/templates/create/base/.vscode/settings.json.hbs +11 -0
- package/templates/create/base/AGENTS.md.hbs +68 -0
- package/templates/create/base/CLAUDE.md.hbs +1 -0
- package/templates/create/base/Makefile.hbs +93 -0
- package/templates/create/base/README.md.hbs +143 -0
- package/templates/create/base/cmd/api/main.go.hbs +116 -0
- package/templates/create/base/go.mod.hbs +11 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +28 -0
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +36 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +46 -0
- package/templates/create/base/internal/shared/dberr/dberr.go.hbs +28 -0
- package/templates/create/base/internal/shared/httpx/httpx.go.hbs +37 -0
- package/templates/create/base/internal/shared/id/id.go.hbs +16 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +33 -0
- package/templates/create/base/internal/shared/middleware/logger.go.hbs +23 -0
- package/templates/create/base/internal/shared/middleware/requestid.go.hbs +36 -0
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +39 -0
- package/templates/create/base/migrations/.gitkeep.hbs +0 -0
- package/templates/create/features/docker-compose.yml.hbs +14 -0
- package/templates/create/features/docs/architecture.md.hbs +99 -0
- package/templates/create/features/docs/common/parameters.yaml.hbs +13 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +20 -0
- package/templates/create/features/docs/common/schemas.yaml.hbs +23 -0
- package/templates/create/features/docs/health/health-livez.yaml.hbs +13 -0
- package/templates/create/features/docs/health/health-readyz.yaml.hbs +21 -0
- package/templates/create/features/docs/openapi.yaml.hbs +33 -0
- package/templates/create/features/docs/patterns.md.hbs +119 -0
- package/templates/create/features/docs/techstack.md.hbs +38 -0
- package/templates/generate/module/docs/collection.yaml.hbs +36 -0
- package/templates/generate/module/docs/item.yaml.hbs +37 -0
- package/templates/generate/module/docs/schemas.yaml.hbs +13 -0
- package/templates/generate/module/dto.go.hbs +29 -0
- package/templates/generate/module/errors.go.hbs +28 -0
- package/templates/generate/module/handler.go.hbs +103 -0
- package/templates/generate/module/handler_test.go.hbs +105 -0
- package/templates/generate/module/migration.down.sql.hbs +1 -0
- package/templates/generate/module/migration.up.sql.hbs +5 -0
- package/templates/generate/module/minimal/dto.go.hbs +26 -0
- package/templates/generate/module/minimal/handler.go.hbs +24 -0
- package/templates/generate/module/minimal/handler_test.go.hbs +70 -0
- package/templates/generate/module/minimal/service.go.hbs +45 -0
- package/templates/generate/module/minimal/service_test.go.hbs +54 -0
- package/templates/generate/module/model/model.go.hbs +20 -0
- package/templates/generate/module/repository.go.hbs +49 -0
- package/templates/generate/module/service.go.hbs +97 -0
- package/templates/generate/module/service_test.go.hbs +65 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.patchMethod = patchMethod;
|
|
7
|
+
exports.markersPresent = markersPresent;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const naming_1 = require("./naming");
|
|
11
|
+
const DTO_MARKER = "// go-scaffold:dto";
|
|
12
|
+
const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
|
|
13
|
+
const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
|
|
14
|
+
const SERVICE_MARKER = "// go-scaffold:service-methods";
|
|
15
|
+
const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
|
|
16
|
+
const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
|
|
17
|
+
const FAKE_REPO_MARKER = "// go-scaffold:fake-repo-methods";
|
|
18
|
+
const UNUSED_G_LINE = "\t_ = g\n";
|
|
19
|
+
// writeHandler ensures whatever packages the new handler code references are
|
|
20
|
+
// imported (a minimal module starts with only "gin" imported) and drops the
|
|
21
|
+
// `_ = g` placeholder once a real route makes it unnecessary — a no-op on a
|
|
22
|
+
// full module, which already imports everything and never has that line.
|
|
23
|
+
function writeHandler(handlerPath, content, goModule, needs) {
|
|
24
|
+
for (const pkg of needs) {
|
|
25
|
+
const importPath = pkg.startsWith("net/") ? pkg : `${goModule}/internal/shared/${pkg}`;
|
|
26
|
+
content = (0, marker_patch_1.ensureImport)(content, importPath);
|
|
27
|
+
}
|
|
28
|
+
fs_extra_1.default.writeFileSync(handlerPath, content.replace(UNUSED_G_LINE, ""));
|
|
29
|
+
}
|
|
30
|
+
function assertNotDuplicate(content, needle, what) {
|
|
31
|
+
if (content.includes(needle)) {
|
|
32
|
+
throw new Error(`${what} already exists — pick a different method name`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function routeCall(type) {
|
|
36
|
+
return { get: "GET", post: "POST", put: "PUT", patch: "PATCH", delete: "DELETE" }[type];
|
|
37
|
+
}
|
|
38
|
+
function patchMethod(paths, naming, method, opts, goModule) {
|
|
39
|
+
const handlerSig = `func (h *Handler) ${method.handlerName}(`;
|
|
40
|
+
const serviceSig = `func (s *Service) ${method.pascalName}(`;
|
|
41
|
+
const handlerContent = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
42
|
+
const serviceContent = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
43
|
+
assertNotDuplicate(handlerContent, handlerSig, `handler method "${method.handlerName}"`);
|
|
44
|
+
assertNotDuplicate(serviceContent, serviceSig, `service method "${method.pascalName}"`);
|
|
45
|
+
if (opts.type === "get" && opts.getMode === "all") {
|
|
46
|
+
patchGetAll(paths, naming, method, goModule);
|
|
47
|
+
}
|
|
48
|
+
else if (opts.type === "get") {
|
|
49
|
+
if (!opts.field)
|
|
50
|
+
throw new Error("--field is required for --type get --get-mode one");
|
|
51
|
+
if (opts.field.toLowerCase() === "id") {
|
|
52
|
+
throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
|
|
53
|
+
}
|
|
54
|
+
patchGetOne(paths, naming, method, opts.field, goModule);
|
|
55
|
+
}
|
|
56
|
+
else if (opts.type === "post") {
|
|
57
|
+
patchPost(paths, naming, method, goModule);
|
|
58
|
+
}
|
|
59
|
+
else if (opts.type === "put" || opts.type === "patch") {
|
|
60
|
+
patchResourceAction(paths, naming, method, opts.type, goModule);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
patchDelete(paths, method, goModule);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function patchGetAll(paths, naming, method, goModule) {
|
|
67
|
+
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
68
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${method.pathSegment}", h.${method.handlerName})`);
|
|
69
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
70
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
71
|
+
`\tp := pagination.Parse(c)`,
|
|
72
|
+
`\titems, err := h.svc.${method.pascalName}(c.Request.Context(), p.Limit, p.Offset)`,
|
|
73
|
+
`\tif err != nil {`,
|
|
74
|
+
`\t\tc.Error(err)`,
|
|
75
|
+
`\t\treturn`,
|
|
76
|
+
`\t}`,
|
|
77
|
+
`\tout := make([]response, len(items))`,
|
|
78
|
+
`\tfor i := range items {`,
|
|
79
|
+
`\t\tout[i] = toResponse(&items[i])`,
|
|
80
|
+
`\t}`,
|
|
81
|
+
`\tc.JSON(http.StatusOK, p.Response(out))`,
|
|
82
|
+
`}`,
|
|
83
|
+
``,
|
|
84
|
+
].join("\n"));
|
|
85
|
+
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "pagination"]);
|
|
86
|
+
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
87
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
88
|
+
`func (s *Service) ${method.pascalName}(ctx context.Context, limit, offset int) ([]model.${naming.pascalName}, error) {`,
|
|
89
|
+
`\t// TODO: add real filtering for "${method.name}" — currently reuses FindAll`,
|
|
90
|
+
`\titems, err := s.repo.FindAll(ctx, limit, offset)`,
|
|
91
|
+
`\tif err != nil {`,
|
|
92
|
+
`\t\treturn nil, apperror.NewInternal()`,
|
|
93
|
+
`\t}`,
|
|
94
|
+
`\treturn items, nil`,
|
|
95
|
+
`}`,
|
|
96
|
+
``,
|
|
97
|
+
].join("\n"));
|
|
98
|
+
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
99
|
+
}
|
|
100
|
+
function patchGetOne(paths, naming, method, rawField, goModule) {
|
|
101
|
+
const fieldParam = (0, naming_1.toCamelCase)(rawField);
|
|
102
|
+
const fieldPascal = (0, naming_1.toPascalCase)(rawField);
|
|
103
|
+
const fieldColumn = (0, naming_1.toDbName)(rawField);
|
|
104
|
+
let repo = fs_extra_1.default.readFileSync(paths.repositoryPath, "utf8");
|
|
105
|
+
assertNotDuplicate(repo, `FindBy${fieldPascal}(`, `repository method "FindBy${fieldPascal}"`);
|
|
106
|
+
repo = (0, marker_patch_1.insertBeforeMarker)(repo, REPO_IMPL_MARKER, [
|
|
107
|
+
`func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
108
|
+
`\tvar m model.${naming.pascalName}`,
|
|
109
|
+
`\t// TODO: confirm "${fieldColumn}" is the real column name for ${fieldParam}`,
|
|
110
|
+
`\tif err := r.db.WithContext(ctx).First(&m, "${fieldColumn} = ?", ${fieldParam}).Error; err != nil {`,
|
|
111
|
+
`\t\treturn nil, err`,
|
|
112
|
+
`\t}`,
|
|
113
|
+
`\treturn &m, nil`,
|
|
114
|
+
`}`,
|
|
115
|
+
``,
|
|
116
|
+
].join("\n"));
|
|
117
|
+
fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
|
|
118
|
+
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
119
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
|
|
120
|
+
// the interface just grew, so the hand-written fakeRepo mock in
|
|
121
|
+
// service_test.go needs a matching stub or the test file stops compiling
|
|
122
|
+
let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
|
|
123
|
+
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, FAKE_REPO_MARKER, [
|
|
124
|
+
// nolint: only matters for a minimal module (fakeRepo never instantiated
|
|
125
|
+
// yet, so unused flags every one of its methods individually); harmless
|
|
126
|
+
// no-op on a full module where fakeRepo is already in use.
|
|
127
|
+
`//nolint:unused`,
|
|
128
|
+
`func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
|
|
129
|
+
`\tif f.err != nil {`,
|
|
130
|
+
`\t\treturn nil, f.err`,
|
|
131
|
+
`\t}`,
|
|
132
|
+
`\treturn f.m, nil`,
|
|
133
|
+
`}`,
|
|
134
|
+
``,
|
|
135
|
+
].join("\n"));
|
|
136
|
+
fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
|
|
137
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
138
|
+
`func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
139
|
+
`\tm, err := s.repo.FindBy${fieldPascal}(ctx, ${fieldParam})`,
|
|
140
|
+
`\tif err != nil {`,
|
|
141
|
+
`\t\treturn nil, wrapFindErr(err)`,
|
|
142
|
+
`\t}`,
|
|
143
|
+
`\treturn m, nil`,
|
|
144
|
+
`}`,
|
|
145
|
+
``,
|
|
146
|
+
].join("\n"));
|
|
147
|
+
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
148
|
+
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
149
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${fieldColumn}/:${fieldParam}", h.${method.handlerName})`);
|
|
150
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
151
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
152
|
+
`\t${fieldParam} := c.Param("${fieldParam}")`,
|
|
153
|
+
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), ${fieldParam})`,
|
|
154
|
+
`\tif err != nil {`,
|
|
155
|
+
`\t\tc.Error(err)`,
|
|
156
|
+
`\t\treturn`,
|
|
157
|
+
`\t}`,
|
|
158
|
+
`\tc.JSON(http.StatusOK, toResponse(m))`,
|
|
159
|
+
`}`,
|
|
160
|
+
``,
|
|
161
|
+
].join("\n"));
|
|
162
|
+
writeHandler(paths.handlerPath, handler, goModule, ["net/http"]);
|
|
163
|
+
}
|
|
164
|
+
function patchPost(paths, naming, method, goModule) {
|
|
165
|
+
const inputName = `${method.pascalName}Input`;
|
|
166
|
+
let dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
|
|
167
|
+
assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
|
|
168
|
+
dto = (0, marker_patch_1.insertBeforeMarker)(dto, DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ``].join("\n"));
|
|
169
|
+
fs_extra_1.default.writeFileSync(paths.dtoPath, dto);
|
|
170
|
+
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
171
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.POST("/${method.pathSegment}", h.${method.handlerName})`);
|
|
172
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
173
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
174
|
+
`\tvar in ${inputName}`,
|
|
175
|
+
`\tif err := c.ShouldBindJSON(&in); err != nil {`,
|
|
176
|
+
`\t\tc.Error(httpx.BindErr(err))`,
|
|
177
|
+
`\t\treturn`,
|
|
178
|
+
`\t}`,
|
|
179
|
+
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), in)`,
|
|
180
|
+
`\tif err != nil {`,
|
|
181
|
+
`\t\tc.Error(err)`,
|
|
182
|
+
`\t\treturn`,
|
|
183
|
+
`\t}`,
|
|
184
|
+
`\tc.JSON(http.StatusCreated, toResponse(m))`,
|
|
185
|
+
`}`,
|
|
186
|
+
``,
|
|
187
|
+
].join("\n"));
|
|
188
|
+
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
189
|
+
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
190
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
191
|
+
`func (s *Service) ${method.pascalName}(ctx context.Context, in ${inputName}) (*model.${naming.pascalName}, error) {`,
|
|
192
|
+
`\t// TODO: implement "${method.name}" — this stub does nothing yet`,
|
|
193
|
+
`\t_ = in`,
|
|
194
|
+
`\treturn nil, apperror.NewInternal()`,
|
|
195
|
+
`}`,
|
|
196
|
+
``,
|
|
197
|
+
].join("\n"));
|
|
198
|
+
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
199
|
+
}
|
|
200
|
+
function patchResourceAction(paths, naming, method, type, goModule) {
|
|
201
|
+
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
202
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.${routeCall(type)}("/:id/${method.pathSegment}", h.${method.handlerName})`);
|
|
203
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
204
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
205
|
+
`\tid, ok := httpx.ParseID(c)`,
|
|
206
|
+
`\tif !ok {`,
|
|
207
|
+
`\t\treturn`,
|
|
208
|
+
`\t}`,
|
|
209
|
+
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), id)`,
|
|
210
|
+
`\tif err != nil {`,
|
|
211
|
+
`\t\tc.Error(err)`,
|
|
212
|
+
`\t\treturn`,
|
|
213
|
+
`\t}`,
|
|
214
|
+
`\tc.JSON(http.StatusOK, toResponse(m))`,
|
|
215
|
+
`}`,
|
|
216
|
+
``,
|
|
217
|
+
].join("\n"));
|
|
218
|
+
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
219
|
+
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
220
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
221
|
+
`func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) (*model.${naming.pascalName}, error) {`,
|
|
222
|
+
`\tm, err := s.repo.FindByID(ctx, id)`,
|
|
223
|
+
`\tif err != nil {`,
|
|
224
|
+
`\t\treturn nil, wrapFindErr(err)`,
|
|
225
|
+
`\t}`,
|
|
226
|
+
`\t// TODO: implement "${method.name}" — currently a no-op save`,
|
|
227
|
+
`\tif err := s.repo.Update(ctx, m); err != nil {`,
|
|
228
|
+
`\t\treturn nil, apperror.NewInternal()`,
|
|
229
|
+
`\t}`,
|
|
230
|
+
`\treturn m, nil`,
|
|
231
|
+
`}`,
|
|
232
|
+
``,
|
|
233
|
+
].join("\n"));
|
|
234
|
+
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
235
|
+
}
|
|
236
|
+
function patchDelete(paths, method, goModule) {
|
|
237
|
+
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
238
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.DELETE("/:id/${method.pathSegment}", h.${method.handlerName})`);
|
|
239
|
+
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
240
|
+
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
241
|
+
`\tid, ok := httpx.ParseID(c)`,
|
|
242
|
+
`\tif !ok {`,
|
|
243
|
+
`\t\treturn`,
|
|
244
|
+
`\t}`,
|
|
245
|
+
`\tif err := h.svc.${method.pascalName}(c.Request.Context(), id); err != nil {`,
|
|
246
|
+
`\t\tc.Error(err)`,
|
|
247
|
+
`\t\treturn`,
|
|
248
|
+
`\t}`,
|
|
249
|
+
`\tc.Status(http.StatusNoContent)`,
|
|
250
|
+
`}`,
|
|
251
|
+
``,
|
|
252
|
+
].join("\n"));
|
|
253
|
+
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
254
|
+
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
255
|
+
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
256
|
+
`func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
|
|
257
|
+
`\t// TODO: implement "${method.name}" — this stub does nothing yet`,
|
|
258
|
+
`\treturn apperror.NewInternal()`,
|
|
259
|
+
`}`,
|
|
260
|
+
``,
|
|
261
|
+
].join("\n"));
|
|
262
|
+
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
263
|
+
}
|
|
264
|
+
function markersPresent(handlerPath, servicePath) {
|
|
265
|
+
const handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
|
|
266
|
+
const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
|
|
267
|
+
return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
|
|
268
|
+
(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
|
|
269
|
+
(0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
|
|
270
|
+
(0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
|
|
271
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.nextMigrationSeq = nextMigrationSeq;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
// golang-migrate sequential numbering: 000001, 000002, ...
|
|
9
|
+
function nextMigrationSeq(migrationsDir) {
|
|
10
|
+
const files = fs_extra_1.default.existsSync(migrationsDir) ? fs_extra_1.default.readdirSync(migrationsDir) : [];
|
|
11
|
+
const nums = files
|
|
12
|
+
.map((f) => f.match(/^(\d+)_/))
|
|
13
|
+
.filter((m) => m !== null)
|
|
14
|
+
.map((m) => parseInt(m[1], 10));
|
|
15
|
+
const next = (nums.length ? Math.max(...nums) : 0) + 1;
|
|
16
|
+
return String(next).padStart(6, "0");
|
|
17
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listVersionFolders = listVersionFolders;
|
|
7
|
+
exports.versionsContainingModule = versionsContainingModule;
|
|
8
|
+
exports.nextVersionName = nextVersionName;
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
// folder-based domain versioning lays modules out as internal/app/v<n>/<pkg>.
|
|
12
|
+
// listVersionFolders returns the existing v<n> dirs, sorted ascending.
|
|
13
|
+
function listVersionFolders(projectDir) {
|
|
14
|
+
const appDir = path_1.default.join(projectDir, "internal", "app");
|
|
15
|
+
if (!fs_extra_1.default.existsSync(appDir))
|
|
16
|
+
return [];
|
|
17
|
+
return fs_extra_1.default
|
|
18
|
+
.readdirSync(appDir, { withFileTypes: true })
|
|
19
|
+
.filter((e) => e.isDirectory() && /^v\d+$/.test(e.name))
|
|
20
|
+
.map((e) => e.name)
|
|
21
|
+
.sort((a, b) => parseInt(a.slice(1), 10) - parseInt(b.slice(1), 10));
|
|
22
|
+
}
|
|
23
|
+
// version folders that actually contain the given module (identified by its
|
|
24
|
+
// handler.go) — a module can legitimately live in more than one version at
|
|
25
|
+
// once (that's the point of versioning), so callers that need exactly one
|
|
26
|
+
// (generate method, remove module) must pick among the matches.
|
|
27
|
+
function versionsContainingModule(projectDir, pkg) {
|
|
28
|
+
return listVersionFolders(projectDir).filter((v) => fs_extra_1.default.existsSync(path_1.default.join(projectDir, "internal", "app", v, pkg, "handler.go")));
|
|
29
|
+
}
|
|
30
|
+
function nextVersionName(versions) {
|
|
31
|
+
const max = versions.reduce((m, v) => Math.max(m, parseInt(v.slice(1), 10) || 0), 0);
|
|
32
|
+
return `v${max + 1}`;
|
|
33
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pluralize = pluralize;
|
|
4
|
+
exports.toPascalCase = toPascalCase;
|
|
5
|
+
exports.toPackageName = toPackageName;
|
|
6
|
+
exports.toCamelCase = toCamelCase;
|
|
7
|
+
exports.toKebabCase = toKebabCase;
|
|
8
|
+
exports.toDbName = toDbName;
|
|
9
|
+
exports.validateGoModulePath = validateGoModulePath;
|
|
10
|
+
exports.assertValidGoModulePath = assertValidGoModulePath;
|
|
11
|
+
exports.assertNotGoKeyword = assertNotGoKeyword;
|
|
12
|
+
exports.validateModuleName = validateModuleName;
|
|
13
|
+
exports.normalizeApiPrefix = normalizeApiPrefix;
|
|
14
|
+
exports.validateApiPrefix = validateApiPrefix;
|
|
15
|
+
exports.resolveModuleNaming = resolveModuleNaming;
|
|
16
|
+
exports.resolveMethodNaming = resolveMethodNaming;
|
|
17
|
+
// ponytail: heuristic pluralizer, not a dependency — covers common English
|
|
18
|
+
// nouns (order/user/category/address); irregular plurals still need a manual
|
|
19
|
+
// rename in the generated file, add a dictionary if that becomes frequent.
|
|
20
|
+
function pluralize(word) {
|
|
21
|
+
if (/[sxz]$/.test(word) || /[^aeiou](ch|sh)$/.test(word))
|
|
22
|
+
return word + "es";
|
|
23
|
+
if (/[^aeiou]y$/.test(word))
|
|
24
|
+
return word.slice(0, -1) + "ies";
|
|
25
|
+
if (word.endsWith("s"))
|
|
26
|
+
return word;
|
|
27
|
+
return word + "s";
|
|
28
|
+
}
|
|
29
|
+
function toPascalCase(value) {
|
|
30
|
+
return value
|
|
31
|
+
.replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ""))
|
|
32
|
+
.replace(/^(.)/, (char) => char.toUpperCase());
|
|
33
|
+
}
|
|
34
|
+
// Go package names: lowercase, single word, no separators (effective Go).
|
|
35
|
+
function toPackageName(value) {
|
|
36
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
37
|
+
}
|
|
38
|
+
function toCamelCase(value) {
|
|
39
|
+
const pascal = toPascalCase(value);
|
|
40
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
41
|
+
}
|
|
42
|
+
// URL path segment: "findActive" -> "find-active", "reset_password" -> "reset-password".
|
|
43
|
+
function toKebabCase(value) {
|
|
44
|
+
return value
|
|
45
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
46
|
+
.replace(/[_\s]+/g, "-")
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
49
|
+
.replace(/^-+|-+$/g, "");
|
|
50
|
+
}
|
|
51
|
+
// Postgres database/identifier name: lowercase snake_case.
|
|
52
|
+
function toDbName(value) {
|
|
53
|
+
return value
|
|
54
|
+
.trim()
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
57
|
+
.replace(/^_+|_+$/g, "");
|
|
58
|
+
}
|
|
59
|
+
// Go module path: each '/'-separated segment is letters/digits, optionally
|
|
60
|
+
// with . _ - in the middle — this is what go.mod's `module` line accepts.
|
|
61
|
+
// Rejects spaces and other punctuation that would produce a go.mod that
|
|
62
|
+
// fails to parse (go: errors parsing go.mod: usage: module module/path).
|
|
63
|
+
const VALID_GO_MODULE_PATH = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?(\/[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)*$/;
|
|
64
|
+
function validateGoModulePath(name) {
|
|
65
|
+
return VALID_GO_MODULE_PATH.test(name)
|
|
66
|
+
? true
|
|
67
|
+
: `invalid project name "${name}" — go.mod module paths can't contain spaces; use letters, numbers, ., _, -, / only (e.g. "my-api" or "github.com/org/my-api")`;
|
|
68
|
+
}
|
|
69
|
+
function assertValidGoModulePath(name) {
|
|
70
|
+
const result = validateGoModulePath(name);
|
|
71
|
+
if (result !== true)
|
|
72
|
+
throw new Error(result);
|
|
73
|
+
}
|
|
74
|
+
// Go's 25 reserved words — a package/func/param named any of these produces
|
|
75
|
+
// code that won't parse (`package func`, `func (h *Handler) type(...)`).
|
|
76
|
+
const GO_KEYWORDS = new Set([
|
|
77
|
+
"break", "case", "chan", "const", "continue", "default", "defer", "else",
|
|
78
|
+
"fallthrough", "for", "func", "go", "goto", "if", "import", "interface",
|
|
79
|
+
"map", "package", "range", "return", "select", "struct", "switch", "type", "var",
|
|
80
|
+
]);
|
|
81
|
+
// predeclared type names — legal as identifiers, but a *package* named one of
|
|
82
|
+
// these shadows the builtin in generated code that uses it as a type (main.go
|
|
83
|
+
// has `s string`), so reject them for module names specifically.
|
|
84
|
+
const GO_PREDECLARED_TYPES = new Set([
|
|
85
|
+
"any", "bool", "byte", "comparable", "complex64", "complex128", "error",
|
|
86
|
+
"float32", "float64", "int", "int8", "int16", "int32", "int64", "rune",
|
|
87
|
+
"string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr",
|
|
88
|
+
]);
|
|
89
|
+
// method/handler/param identifier: only keywords are hard-illegal (a param or
|
|
90
|
+
// func named `string` is legal Go, just shadows the builtin locally).
|
|
91
|
+
function assertNotGoKeyword(ident, role) {
|
|
92
|
+
if (GO_KEYWORDS.has(ident.toLowerCase())) {
|
|
93
|
+
throw new Error(`"${ident}" is a Go keyword — can't use it as a ${role} name; pick another`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// module name becomes a Go package name; keywords and predeclared type names
|
|
97
|
+
// both produce code that won't compile (`package func`, or a `string` package
|
|
98
|
+
// shadowing the builtin in main.go). Returns true|message for inquirer, and
|
|
99
|
+
// backs the assert in resolveModuleNaming — one source of truth for both.
|
|
100
|
+
function validateModuleName(rawName) {
|
|
101
|
+
const pkg = toPackageName(rawName);
|
|
102
|
+
if (!pkg)
|
|
103
|
+
return `invalid module name: "${rawName}" (must contain letters/numbers)`;
|
|
104
|
+
if (/^[0-9]/.test(pkg)) {
|
|
105
|
+
return `"${pkg}" starts with a digit — a Go package name can't, so it won't compile; pick another module name`;
|
|
106
|
+
}
|
|
107
|
+
if (GO_KEYWORDS.has(pkg) || GO_PREDECLARED_TYPES.has(pkg)) {
|
|
108
|
+
return `"${pkg}" is a reserved Go word — a package named it won't compile; pick another module name`;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// strips whitespace and leading/trailing slashes so "/api/v1/" and "api/v1"
|
|
113
|
+
// store identically — callers should normalize once and use the result
|
|
114
|
+
// everywhere (config, templates), not just for validation.
|
|
115
|
+
function normalizeApiPrefix(raw) {
|
|
116
|
+
return raw.trim().replace(/^\/+|\/+$/g, "");
|
|
117
|
+
}
|
|
118
|
+
// apiPrefix becomes a URL path (/api/v1/orders) — multiple slash-separated
|
|
119
|
+
// segments are fine (gin's r.Group() joins paths natively, confirmed against
|
|
120
|
+
// gin directly: r.Group("/api/v1") produces /api/v1/orders as expected). The
|
|
121
|
+
// `api := r.Group(...)` variable is always named "api" regardless of the
|
|
122
|
+
// prefix's value, so the prefix itself never needs to be a valid Go
|
|
123
|
+
// identifier — just clean URL segments. Empty string is valid on purpose: it
|
|
124
|
+
// means "no prefix", routes register directly at /orders.
|
|
125
|
+
function validateApiPrefix(raw) {
|
|
126
|
+
const trimmed = normalizeApiPrefix(raw);
|
|
127
|
+
if (trimmed === "")
|
|
128
|
+
return true;
|
|
129
|
+
return /^[a-z][a-z0-9]*(\/[a-z][a-z0-9]*)*$/.test(trimmed)
|
|
130
|
+
? true
|
|
131
|
+
: `invalid API prefix "${trimmed}" — use lowercase letters/numbers per segment, separated by "/" (e.g. "v1", "api/v1"), or leave blank for none`;
|
|
132
|
+
}
|
|
133
|
+
function resolveModuleNaming(rawName) {
|
|
134
|
+
const check = validateModuleName(rawName);
|
|
135
|
+
if (check !== true)
|
|
136
|
+
throw new Error(check);
|
|
137
|
+
const pkg = toPackageName(rawName);
|
|
138
|
+
return {
|
|
139
|
+
name: pkg,
|
|
140
|
+
pkg,
|
|
141
|
+
pascalName: toPascalCase(pkg),
|
|
142
|
+
plural: pluralize(pkg),
|
|
143
|
+
errorPrefix: pkg.toUpperCase(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function resolveMethodNaming(rawName) {
|
|
147
|
+
const cleaned = rawName.trim();
|
|
148
|
+
const pascalName = toPascalCase(cleaned);
|
|
149
|
+
if (!pascalName) {
|
|
150
|
+
throw new Error(`invalid method name: "${rawName}" (must contain letters/numbers)`);
|
|
151
|
+
}
|
|
152
|
+
// handlerName becomes a Go method name (`func (h *Handler) <name>`)
|
|
153
|
+
assertNotGoKeyword(toCamelCase(cleaned), "method");
|
|
154
|
+
return {
|
|
155
|
+
name: cleaned,
|
|
156
|
+
pascalName,
|
|
157
|
+
handlerName: toCamelCase(cleaned),
|
|
158
|
+
pathSegment: toKebabCase(cleaned),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.patchOpenapiIndex = patchOpenapiIndex;
|
|
7
|
+
exports.unpatchOpenapiIndex = unpatchOpenapiIndex;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const PATHS_MARKER = "# go-scaffold:paths";
|
|
11
|
+
const SCHEMAS_MARKER = "# go-scaffold:schemas";
|
|
12
|
+
// exact lines a module contributes to docs/openapi.yaml — shared by patch and
|
|
13
|
+
// unpatch so removal pulls out precisely what was added. apiPrefix is the
|
|
14
|
+
// project-wide prefix chosen at create time (e.g. "v1", "" for none).
|
|
15
|
+
function openapiLines(naming, apiPrefix) {
|
|
16
|
+
const base = apiPrefix ? `/${apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
17
|
+
return {
|
|
18
|
+
paths: [
|
|
19
|
+
`${base}:`,
|
|
20
|
+
` $ref: './${naming.plural}/collection.yaml'`,
|
|
21
|
+
`${base}/{id}:`,
|
|
22
|
+
` $ref: './${naming.plural}/item.yaml'`,
|
|
23
|
+
],
|
|
24
|
+
schemas: [
|
|
25
|
+
`${naming.pascalName}CreateInput: { $ref: './${naming.plural}/schemas.yaml#/${naming.pascalName}CreateInput' }`,
|
|
26
|
+
`${naming.pascalName}UpdateInput: { $ref: './${naming.plural}/schemas.yaml#/${naming.pascalName}UpdateInput' }`,
|
|
27
|
+
`${naming.pascalName}Response: { $ref: './${naming.plural}/schemas.yaml#/${naming.pascalName}Response' }`,
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// patchOpenapiIndex wires a new module's collection/item docs into the
|
|
32
|
+
// docs/openapi.yaml index — same marker-comment approach as main.go.
|
|
33
|
+
function patchOpenapiIndex(openapiPath, naming, apiPrefix) {
|
|
34
|
+
let content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
|
|
35
|
+
const { paths, schemas } = openapiLines(naming, apiPrefix);
|
|
36
|
+
// sentinels keep re-runs idempotent (same reason as main-patcher)
|
|
37
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PATHS_MARKER, paths.join("\n"), paths[0]);
|
|
38
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMAS_MARKER, schemas.join("\n"), schemas[2]);
|
|
39
|
+
fs_extra_1.default.writeFileSync(openapiPath, content);
|
|
40
|
+
}
|
|
41
|
+
// unpatchOpenapiIndex removes a module's paths/schemas from the index — inverse
|
|
42
|
+
// of patchOpenapiIndex.
|
|
43
|
+
function unpatchOpenapiIndex(openapiPath, naming, apiPrefix) {
|
|
44
|
+
const content = fs_extra_1.default.readFileSync(openapiPath, "utf8");
|
|
45
|
+
const { paths, schemas } = openapiLines(naming, apiPrefix);
|
|
46
|
+
fs_extra_1.default.writeFileSync(openapiPath, (0, marker_patch_1.removeLines)(content, [...paths, ...schemas]));
|
|
47
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getTemplatesRoot = getTemplatesRoot;
|
|
7
|
+
exports.renderString = renderString;
|
|
8
|
+
exports.applyTemplateEntries = applyTemplateEntries;
|
|
9
|
+
exports.gofmtTree = gofmtTree;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const handlebars_1 = __importDefault(require("handlebars"));
|
|
15
|
+
handlebars_1.default.registerHelper("eq", (a, b) => a === b);
|
|
16
|
+
function getTemplatesRoot() {
|
|
17
|
+
const candidates = [
|
|
18
|
+
path_1.default.join(__dirname, "..", "..", "templates"),
|
|
19
|
+
path_1.default.join(__dirname, "..", "..", "..", "templates"),
|
|
20
|
+
];
|
|
21
|
+
const resolved = candidates.find((candidate) => fs_1.default.existsSync(path_1.default.join(candidate, "create", "base", "go.mod.hbs")));
|
|
22
|
+
if (!resolved) {
|
|
23
|
+
throw new Error("unable to locate templates directory");
|
|
24
|
+
}
|
|
25
|
+
return resolved;
|
|
26
|
+
}
|
|
27
|
+
function renderString(source, context) {
|
|
28
|
+
return handlebars_1.default.compile(source, { noEscape: true })(context);
|
|
29
|
+
}
|
|
30
|
+
async function applyTemplateEntries(projectRoot, entries, context) {
|
|
31
|
+
const root = getTemplatesRoot();
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.when && !entry.when(context))
|
|
34
|
+
continue;
|
|
35
|
+
const source = await fs_extra_1.default.readFile(path_1.default.join(root, entry.template), "utf8");
|
|
36
|
+
const rendered = renderString(source, context);
|
|
37
|
+
const outputPath = path_1.default.join(projectRoot, entry.output);
|
|
38
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(outputPath));
|
|
39
|
+
await fs_extra_1.default.writeFile(outputPath, rendered);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// gofmt the whole project tree; a missing gofmt (no local Go toolchain) is
|
|
43
|
+
// non-fatal — generated files are already hand-formatted templates.
|
|
44
|
+
function gofmtTree(projectRoot) {
|
|
45
|
+
try {
|
|
46
|
+
(0, child_process_1.execFileSync)("gofmt", ["-w", "."], { cwd: projectRoot, stdio: "ignore" });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// ponytail: no Go toolchain on this machine, skip formatting
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nakedev/go-scaffold",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold Gin + GORM + Postgres Go backend projects with a consistent domain-module standard",
|
|
5
|
+
"bin": {
|
|
6
|
+
"go-scaffold": "./bin/go-scaffold.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"templates",
|
|
11
|
+
"bin"
|
|
12
|
+
],
|
|
13
|
+
"type": "commonjs",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"test": "node scripts/smoke-test.mjs",
|
|
18
|
+
"verify": "pnpm run build && pnpm run test",
|
|
19
|
+
"prepublishOnly": "pnpm run verify"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"go",
|
|
23
|
+
"gin",
|
|
24
|
+
"gorm",
|
|
25
|
+
"scaffold",
|
|
26
|
+
"cli",
|
|
27
|
+
"generator"
|
|
28
|
+
],
|
|
29
|
+
"author": "nakedev",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@inquirer/prompts": "^7.5.1",
|
|
36
|
+
"commander": "^15.0.0",
|
|
37
|
+
"fs-extra": "^11.3.0",
|
|
38
|
+
"handlebars": "^4.7.8",
|
|
39
|
+
"picocolors": "^1.1.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/fs-extra": "^11.0.4",
|
|
43
|
+
"@types/node": "^24.0.0",
|
|
44
|
+
"typescript": "^5.7.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
}
|
|
49
|
+
}
|