@nakedev/go-scaffold 0.1.4 → 0.3.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/LICENSE +21 -0
- package/README.md +133 -44
- package/dist/commands/auth.js +116 -11
- package/dist/commands/create.js +13 -1
- package/dist/commands/generate.js +21 -11
- package/dist/commands/method.js +32 -3
- package/dist/commands/observability.js +114 -0
- package/dist/commands/rbac.js +19 -2
- package/dist/commands/undo.js +331 -0
- package/dist/commands/worker.js +92 -32
- package/dist/index.js +366 -63
- package/dist/prompts/auth-wizard.js +29 -0
- package/dist/prompts/create-wizard.js +5 -2
- package/dist/prompts/generate-wizard.js +57 -0
- package/dist/prompts/worker-wizard.js +25 -0
- package/dist/templates/auth-manifest.js +20 -3
- package/dist/templates/create-manifest.js +13 -20
- package/dist/templates/observability-manifest.js +24 -0
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/templates/worker-manifest.js +23 -6
- package/dist/utils/auth-patcher.js +96 -21
- package/dist/utils/config.js +58 -10
- package/dist/utils/gocheck.js +57 -5
- package/dist/utils/golangci-patcher.js +73 -0
- package/dist/utils/gomod-patcher.js +53 -0
- package/dist/utils/main-patcher.js +58 -4
- package/dist/utils/marker-patch.js +125 -3
- package/dist/utils/method-patcher.js +17 -2
- package/dist/utils/module-location.js +37 -1
- package/dist/utils/naming.js +50 -2
- package/dist/utils/observability-patcher.js +107 -0
- package/dist/utils/platform-patcher.js +98 -12
- package/dist/utils/rbac-patcher.js +60 -10
- package/package.json +3 -5
- package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
- package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
- package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
- package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
- package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
- package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
- package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
- package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
- package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
- package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
- package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
- package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
- package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
- package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
- package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +59 -12
- package/templates/create/base/.dockerignore.hbs +13 -0
- package/templates/create/base/.env.example.hbs +18 -9
- package/templates/create/base/.github/dependabot.yml.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
- package/templates/create/base/.golangci.yml.hbs +27 -0
- package/templates/create/base/AGENTS.md.hbs +8 -4
- package/templates/create/base/Dockerfile.hbs +42 -0
- package/templates/create/base/Makefile.hbs +43 -13
- package/templates/create/base/README.md.hbs +45 -8
- package/templates/create/base/cmd/api/main.go.hbs +17 -130
- package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
- package/templates/create/base/go.mod.hbs +4 -4
- package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
- package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
- package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
- package/templates/create/base/redocly.yaml.hbs +21 -0
- package/templates/create/features/docs/architecture.md.hbs +32 -11
- package/templates/create/features/docs/openapi.yaml.hbs +0 -4
- package/templates/create/features/docs/patterns.md.hbs +82 -8
- package/templates/create/features/docs/techstack.md.hbs +8 -3
- package/templates/generate/module/dto.go.hbs +8 -1
- package/templates/generate/module/errors.go.hbs +5 -0
- package/templates/generate/module/field-column.down.sql.hbs +2 -0
- package/templates/generate/module/field-column.up.sql.hbs +15 -0
- package/templates/generate/module/handler_test.go.hbs +8 -1
- package/templates/generate/module/migration.down.sql.hbs +3 -1
- package/templates/generate/module/migration.up.sql.hbs +7 -2
- package/templates/generate/module/minimal/dto.go.hbs +3 -1
- package/templates/generate/module/model/model.go.hbs +10 -1
- package/templates/generate/module/permission.up.sql.hbs +3 -1
- package/templates/generate/module/repository.go.hbs +60 -6
- package/templates/generate/module/repository_test.go.hbs +30 -0
- package/templates/generate/module/service.go.hbs +10 -1
- package/templates/generate/module/service_test.go.hbs +45 -0
- package/dist/commands/remove.js +0 -88
- package/scripts/smoke-test.mjs +0 -2058
- package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
- package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
- package/tests/integration/default-module.test.mjs +0 -46
- package/tests/integration/generator-naming.test.mjs +0 -81
- package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
- package/tests/integration/legacy-method-compat.test.mjs +0 -222
- package/tests/integration/remove-module.test.mjs +0 -58
- package/tests/unit/naming.test.mjs +0 -94
- package/tests/unit/smoke-isolation.test.mjs +0 -35
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
//
|
|
5
|
-
|
|
3
|
+
exports.authFiles = authFiles;
|
|
4
|
+
// Everything that is the same whichever store backs the tokens.
|
|
5
|
+
const SHARED = [
|
|
6
6
|
{ template: "add/auth/internal/shared/middleware/auth.go.hbs", output: "internal/shared/middleware/auth.go" },
|
|
7
7
|
{ template: "add/auth/internal/shared/middleware/ratelimit.go.hbs", output: "internal/shared/middleware/ratelimit.go" },
|
|
8
8
|
{ template: "add/auth/internal/app/user/model/user.go.hbs", output: "internal/app/user/model/user.go" },
|
|
9
9
|
{ template: "add/auth/internal/app/user/model/identity.go.hbs", output: "internal/app/user/model/identity.go" },
|
|
10
|
+
{ template: "add/auth/internal/app/user/model/loginthrottle.go.hbs", output: "internal/app/user/model/loginthrottle.go" },
|
|
10
11
|
{ template: "add/auth/internal/app/user/dto.go.hbs", output: "internal/app/user/dto.go" },
|
|
11
12
|
{ template: "add/auth/internal/app/user/errors.go.hbs", output: "internal/app/user/errors.go" },
|
|
12
13
|
{ template: "add/auth/internal/app/user/jwt.go.hbs", output: "internal/app/user/jwt.go" },
|
|
@@ -14,6 +15,22 @@ exports.AUTH_FILES = [
|
|
|
14
15
|
{ template: "add/auth/internal/app/user/repository.go.hbs", output: "internal/app/user/repository.go" },
|
|
15
16
|
{ template: "add/auth/internal/app/user/service.go.hbs", output: "internal/app/user/service.go" },
|
|
16
17
|
{ template: "add/auth/internal/app/user/service_test.go.hbs", output: "internal/app/user/service_test.go" },
|
|
18
|
+
{ template: "add/auth/internal/app/user/repository_test.go.hbs", output: "internal/app/user/repository_test.go" },
|
|
17
19
|
{ template: "add/auth/internal/app/user/handler.go.hbs", output: "internal/app/user/handler.go" },
|
|
18
20
|
{ template: "add/auth/cmd/seed/main.go.hbs", output: "cmd/seed/main.go" },
|
|
19
21
|
];
|
|
22
|
+
// Only the chosen store's implementation is written. Shipping both would drag
|
|
23
|
+
// go-redis into every project's go.mod for a file it never constructs — the
|
|
24
|
+
// same reason `add worker` writes one queue adapter, not two.
|
|
25
|
+
const POSTGRES = [
|
|
26
|
+
{ template: "add/auth/internal/app/user/model/authtoken.go.hbs", output: "internal/app/user/model/authtoken.go" },
|
|
27
|
+
{ template: "add/auth/internal/app/user/tokenstore_pg.go.hbs", output: "internal/app/user/tokenstore_pg.go" },
|
|
28
|
+
{ template: "add/auth/internal/shared/middleware/ratelimit_memory.go.hbs", output: "internal/shared/middleware/ratelimit_memory.go" },
|
|
29
|
+
];
|
|
30
|
+
const REDIS = [
|
|
31
|
+
{ template: "add/auth/internal/app/user/tokenstore_redis.go.hbs", output: "internal/app/user/tokenstore_redis.go" },
|
|
32
|
+
{ template: "add/auth/internal/shared/middleware/ratelimit_redis.go.hbs", output: "internal/shared/middleware/ratelimit_redis.go" },
|
|
33
|
+
];
|
|
34
|
+
function authFiles(store) {
|
|
35
|
+
return [...SHARED, ...(store === "postgres" ? POSTGRES : REDIS)];
|
|
36
|
+
}
|
|
@@ -4,11 +4,14 @@ exports.CREATE_MANIFEST = void 0;
|
|
|
4
4
|
exports.CREATE_MANIFEST = [
|
|
5
5
|
{ template: "create/base/go.mod.hbs", output: "go.mod" },
|
|
6
6
|
{ template: "create/base/.gitignore.hbs", output: ".gitignore" },
|
|
7
|
+
{ template: "create/base/Dockerfile.hbs", output: "Dockerfile" },
|
|
8
|
+
{ template: "create/base/.dockerignore.hbs", output: ".dockerignore" },
|
|
7
9
|
{ template: "create/base/.env.example.hbs", output: ".env.example" },
|
|
8
10
|
{ template: "create/base/Makefile.hbs", output: "Makefile" },
|
|
9
11
|
{ template: "create/base/.golangci.yml.hbs", output: ".golangci.yml" },
|
|
10
12
|
{ template: "create/base/.vscode/settings.json.hbs", output: ".vscode/settings.json" },
|
|
11
13
|
{ template: "create/base/.github/workflows/ci.yml.hbs", output: ".github/workflows/ci.yml" },
|
|
14
|
+
{ template: "create/base/.github/dependabot.yml.hbs", output: ".github/dependabot.yml" },
|
|
12
15
|
{ template: "create/base/README.md.hbs", output: "README.md" },
|
|
13
16
|
{ template: "create/base/AGENTS.md.hbs", output: "AGENTS.md" },
|
|
14
17
|
{ template: "create/base/CLAUDE.md.hbs", output: "CLAUDE.md" },
|
|
@@ -17,6 +20,7 @@ exports.CREATE_MANIFEST = [
|
|
|
17
20
|
output: ".claude/skills/go-scaffold/SKILL.md",
|
|
18
21
|
},
|
|
19
22
|
{ template: "create/base/cmd/api/main.go.hbs", output: "cmd/api/main.go" },
|
|
23
|
+
{ template: "create/base/cmd/api/wiring.go.hbs", output: "cmd/api/wiring.go" },
|
|
20
24
|
{
|
|
21
25
|
template: "create/base/internal/platform/database/database.go.hbs",
|
|
22
26
|
output: "internal/platform/database/database.go",
|
|
@@ -45,6 +49,10 @@ exports.CREATE_MANIFEST = [
|
|
|
45
49
|
template: "create/base/internal/shared/pagination/pagination.go.hbs",
|
|
46
50
|
output: "internal/shared/pagination/pagination.go",
|
|
47
51
|
},
|
|
52
|
+
{
|
|
53
|
+
template: "create/base/internal/shared/tx/tx.go.hbs",
|
|
54
|
+
output: "internal/shared/tx/tx.go",
|
|
55
|
+
},
|
|
48
56
|
{
|
|
49
57
|
template: "create/base/internal/shared/middleware/cors.go.hbs",
|
|
50
58
|
output: "internal/shared/middleware/cors.go",
|
|
@@ -87,6 +95,11 @@ exports.CREATE_MANIFEST = [
|
|
|
87
95
|
output: "docs/openapi.yaml",
|
|
88
96
|
when: (ctx) => ctx.openapiDocs,
|
|
89
97
|
},
|
|
98
|
+
{
|
|
99
|
+
template: "create/base/redocly.yaml.hbs",
|
|
100
|
+
output: "redocly.yaml",
|
|
101
|
+
when: (ctx) => ctx.openapiDocs,
|
|
102
|
+
},
|
|
90
103
|
{
|
|
91
104
|
template: "create/features/docs/common/parameters.yaml.hbs",
|
|
92
105
|
output: "docs/common/parameters.yaml",
|
|
@@ -112,24 +125,4 @@ exports.CREATE_MANIFEST = [
|
|
|
112
125
|
output: "docs/health/health-readyz.yaml",
|
|
113
126
|
when: (ctx) => ctx.openapiDocs,
|
|
114
127
|
},
|
|
115
|
-
{
|
|
116
|
-
template: "create/features/docs/observability/metrics.yaml.hbs",
|
|
117
|
-
output: "docs/observability/metrics.yaml",
|
|
118
|
-
when: (ctx) => ctx.openapiDocs && ctx.observability,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
template: "create/features/observability/middleware/metrics.go.hbs",
|
|
122
|
-
output: "internal/shared/middleware/metrics.go",
|
|
123
|
-
when: (ctx) => ctx.observability,
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
template: "create/features/observability/middleware/tracing.go.hbs",
|
|
127
|
-
output: "internal/shared/middleware/tracing.go",
|
|
128
|
-
when: (ctx) => ctx.observability,
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
template: "create/features/observability/platform/telemetry/tracing.go.hbs",
|
|
132
|
-
output: "internal/platform/telemetry/tracing.go",
|
|
133
|
-
when: (ctx) => ctx.observability,
|
|
134
|
-
},
|
|
135
128
|
];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OBSERVABILITY_FILES = void 0;
|
|
4
|
+
// output paths are relative to the project root
|
|
5
|
+
exports.OBSERVABILITY_FILES = [
|
|
6
|
+
{
|
|
7
|
+
template: "create/features/observability/middleware/metrics.go.hbs",
|
|
8
|
+
output: "internal/shared/middleware/metrics.go",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
template: "create/features/observability/middleware/tracing.go.hbs",
|
|
12
|
+
output: "internal/shared/middleware/tracing.go",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
template: "create/features/observability/platform/telemetry/tracing.go.hbs",
|
|
16
|
+
output: "internal/platform/telemetry/tracing.go",
|
|
17
|
+
},
|
|
18
|
+
// only meaningful once there's an openapi.yaml to $ref it from
|
|
19
|
+
{
|
|
20
|
+
template: "create/features/docs/observability/metrics.yaml.hbs",
|
|
21
|
+
output: "docs/observability/metrics.yaml",
|
|
22
|
+
when: (ctx) => ctx.openapiDocs,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
@@ -11,6 +11,7 @@ exports.RBAC_FILES = [
|
|
|
11
11
|
{ template: "add/rbac/internal/app/role/repository.go.hbs", output: "internal/app/role/repository.go" },
|
|
12
12
|
{ template: "add/rbac/internal/app/role/service.go.hbs", output: "internal/app/role/service.go" },
|
|
13
13
|
{ template: "add/rbac/internal/app/role/service_test.go.hbs", output: "internal/app/role/service_test.go" },
|
|
14
|
+
{ template: "add/rbac/internal/app/role/repository_test.go.hbs", output: "internal/app/role/repository_test.go" },
|
|
14
15
|
{ template: "add/rbac/internal/app/role/handler.go.hbs", output: "internal/app/role/handler.go" },
|
|
15
16
|
{ template: "add/rbac/internal/app/role/dto.go.hbs", output: "internal/app/role/dto.go" },
|
|
16
17
|
{ template: "add/rbac/internal/app/role/errors.go.hbs", output: "internal/app/role/errors.go" },
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
exports.MAIL_CLIENT_ONLY = void 0;
|
|
4
|
+
exports.workerFiles = workerFiles;
|
|
5
|
+
// The SMTP client on its own. `add auth` installs just this when the project
|
|
6
|
+
// has no worker: mail/task.go is the piece that knows about the queue, and
|
|
7
|
+
// pulling it in would drag platform/queue along with it.
|
|
8
|
+
exports.MAIL_CLIENT_ONLY = [
|
|
9
9
|
{ template: "add/worker/internal/platform/mail/mail.go.hbs", output: "internal/platform/mail/mail.go" },
|
|
10
|
+
];
|
|
11
|
+
const SHARED = [
|
|
12
|
+
{ template: "add/worker/internal/platform/queue/queue.go.hbs", output: "internal/platform/queue/queue.go" },
|
|
13
|
+
...exports.MAIL_CLIENT_ONLY,
|
|
10
14
|
{ template: "add/worker/internal/platform/mail/task.go.hbs", output: "internal/platform/mail/task.go" },
|
|
11
15
|
{ template: "add/worker/cmd/worker/main.go.hbs", output: "cmd/worker/main.go" },
|
|
12
16
|
];
|
|
17
|
+
// Only the chosen backend's adapter is written: an unused adapter would drag
|
|
18
|
+
// its whole dependency tree into go.mod for nothing. Swapping later means
|
|
19
|
+
// re-running `add worker` with the other backend, or copying the adapter in
|
|
20
|
+
// by hand — the queue.go contract it implements does not change.
|
|
21
|
+
const RIVER = [{ template: "add/worker/internal/platform/queue/river.go.hbs", output: "internal/platform/queue/river.go" }];
|
|
22
|
+
const ASYNQ = [
|
|
23
|
+
{ template: "add/worker/internal/platform/queue/asynq.go.hbs", output: "internal/platform/queue/asynq.go" },
|
|
24
|
+
// Redis only comes along when it is actually the queue's backing store.
|
|
25
|
+
{ template: "add/worker/internal/platform/cache/redis.go.hbs", output: "internal/platform/cache/redis.go" },
|
|
26
|
+
];
|
|
27
|
+
function workerFiles(backend) {
|
|
28
|
+
return [...SHARED, ...(backend === "river" ? RIVER : ASYNQ)];
|
|
29
|
+
}
|
|
@@ -4,7 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.patchConfigForAuth = patchConfigForAuth;
|
|
7
|
+
exports.authWiringLines = authWiringLines;
|
|
7
8
|
exports.patchMainGoForAuth = patchMainGoForAuth;
|
|
9
|
+
exports.upgradeMailerToQueue = upgradeMailerToQueue;
|
|
8
10
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
11
|
const marker_patch_1 = require("./marker-patch");
|
|
10
12
|
const IMPORT_MARKER = "// go-scaffold:imports";
|
|
@@ -12,6 +14,7 @@ const CONFIG_FIELDS_MARKER = "// go-scaffold:config-fields";
|
|
|
12
14
|
const CONFIG_LOAD_MARKER = "// go-scaffold:config-load";
|
|
13
15
|
const CONFIG_CHECKS_MARKER = "// go-scaffold:config-checks";
|
|
14
16
|
const PLATFORM_INIT_MARKER = "// go-scaffold:platform-init";
|
|
17
|
+
const SCHEMA_MARKER = "// go-scaffold:schemas";
|
|
15
18
|
const MODEL_MARKER = "// go-scaffold:models";
|
|
16
19
|
const ROUTE_MARKER = "// go-scaffold:routes";
|
|
17
20
|
const SHUTDOWN_MARKER = "// go-scaffold:shutdown";
|
|
@@ -26,6 +29,7 @@ function patchConfigForAuth(configGoPath) {
|
|
|
26
29
|
"JWTAccessTTL time.Duration",
|
|
27
30
|
"JWTRefreshTTL time.Duration",
|
|
28
31
|
"CookieSecure bool",
|
|
32
|
+
"CookieSameSite string",
|
|
29
33
|
"",
|
|
30
34
|
"PasswordResetTTL time.Duration",
|
|
31
35
|
"PasswordResetURL string",
|
|
@@ -37,12 +41,13 @@ function patchConfigForAuth(configGoPath) {
|
|
|
37
41
|
"GoogleClientSecret string",
|
|
38
42
|
"GoogleRedirectURL string",
|
|
39
43
|
].join("\n");
|
|
40
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "JWTSecret
|
|
44
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_FIELDS_MARKER, fieldsBlock, "JWTSecret");
|
|
41
45
|
const loadBlock = [
|
|
42
46
|
'JWTSecret: env("JWT_SECRET", "dev-secret-change-me"),',
|
|
43
47
|
'JWTAccessTTL: time.Duration(envInt("JWT_ACCESS_TTL_MIN", 15)) * time.Minute,',
|
|
44
48
|
'JWTRefreshTTL: time.Duration(envInt("JWT_REFRESH_TTL_MIN", 43200)) * time.Minute,',
|
|
45
49
|
'CookieSecure: env("COOKIE_SECURE", "false") == "true",',
|
|
50
|
+
'CookieSameSite: env("COOKIE_SAMESITE", "strict"),',
|
|
46
51
|
"",
|
|
47
52
|
'PasswordResetTTL: time.Duration(envInt("PASSWORD_RESET_TTL_MIN", 30)) * time.Minute,',
|
|
48
53
|
'PasswordResetURL: env("PASSWORD_RESET_URL", "http://localhost:3000/reset-password"),',
|
|
@@ -54,43 +59,113 @@ function patchConfigForAuth(configGoPath) {
|
|
|
54
59
|
'GoogleClientSecret: env("GOOGLE_CLIENT_SECRET", ""),',
|
|
55
60
|
'GoogleRedirectURL: env("GOOGLE_REDIRECT_URL", ""),',
|
|
56
61
|
].join("\n");
|
|
57
|
-
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, '
|
|
62
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_LOAD_MARKER, loadBlock, 'env("JWT_SECRET"');
|
|
58
63
|
fs_extra_1.default.writeFileSync(configGoPath, content);
|
|
59
64
|
}
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
// authWiringLines is the single source of truth for the two lines that differ
|
|
66
|
+
// between stores, so patchMainGoForAuth and `add rbac`'s rewrite of the same
|
|
67
|
+
// lines can never drift apart.
|
|
68
|
+
function authWiringLines(w) {
|
|
69
|
+
const postgres = w.store === "postgres";
|
|
70
|
+
return {
|
|
71
|
+
tokenStore: postgres ? "user.NewPgTokenStore(db)" : "user.NewRedisTokenStore(rdb)",
|
|
72
|
+
limiter: postgres ? "middleware.NewMemoryLimiter()" : "middleware.NewRedisLimiter(rdb)",
|
|
73
|
+
// with a queue the mail is enqueued and the request returns immediately;
|
|
74
|
+
// without one it goes out inline, which is the cost of not running a worker
|
|
75
|
+
mailer: w.worker ? "mail.NewAsyncClient(q)" : "mail.NewSyncClient(mail.Open(cfg))",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function patchMainGoForAuth(mainGoPath, w) {
|
|
79
|
+
const { goModule, queueBackend, store } = w;
|
|
68
80
|
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
69
81
|
const importLine = `"${goModule}/internal/app/user"`;
|
|
70
82
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, importLine, importLine);
|
|
71
83
|
const modelImportLine = `usermodel "${goModule}/internal/app/user/model"`;
|
|
72
84
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, modelImportLine, modelImportLine);
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
if (w.worker) {
|
|
86
|
+
const queueImportLine = `"${goModule}/internal/platform/queue"`;
|
|
87
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
|
|
88
|
+
}
|
|
75
89
|
const mailImportLine = `"${goModule}/internal/platform/mail"`;
|
|
76
90
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, mailImportLine, mailImportLine);
|
|
77
91
|
const checkBlock = [
|
|
78
92
|
'if cfg.IsProd() && cfg.JWTSecret == "dev-secret-change-me" {',
|
|
79
|
-
'\
|
|
80
|
-
"\tos.Exit(1)",
|
|
93
|
+
'\treturn errors.New("JWT_SECRET is still the dev default — set a real secret before deploying with APP_ENV=production")',
|
|
81
94
|
"}",
|
|
82
95
|
].join("\n");
|
|
83
96
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, checkBlock, "JWT_SECRET is still the dev default");
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
// Without SMTP the mail client logs the message instead of sending it — a
|
|
98
|
+
// deliberate dev convenience that in production means password-reset and
|
|
99
|
+
// email-verification links land in the log aggregator while
|
|
100
|
+
// /auth/forgot-password still answers 200, so nobody finds out the mail
|
|
101
|
+
// never went anywhere.
|
|
102
|
+
const smtpCheckBlock = [
|
|
103
|
+
'if cfg.IsProd() && cfg.SMTPHost == "" {',
|
|
104
|
+
'\treturn errors.New("SMTP_HOST is unset — password reset and email verification links would be written to the log instead of sent")',
|
|
105
|
+
"}",
|
|
106
|
+
].join("\n");
|
|
107
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, CONFIG_CHECKS_MARKER, smtpCheckBlock, "SMTP_HOST is unset");
|
|
108
|
+
// The enqueuer is built from whatever backend `add worker` chose — the
|
|
109
|
+
// constructor differs, everything downstream of it (mail.NewAsyncClient)
|
|
110
|
+
// only sees the queue.Enqueuer interface and doesn't change.
|
|
111
|
+
if (w.worker) {
|
|
112
|
+
const queueCtor = queueBackend === "river" ? "queue.NewRiverEnqueuer(db)" : "queue.NewAsynqEnqueuer(cfg.RedisURL)";
|
|
113
|
+
const queueInitBlock = [`q, err := ${queueCtor}`, "if err != nil {", '\treturn fmt.Errorf("open queue: %w", err)', "}"].join("\n");
|
|
114
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, queueInitBlock, "q, err := queue.New");
|
|
115
|
+
}
|
|
116
|
+
const schemaBlock = [
|
|
117
|
+
'if err := db.Exec("CREATE SCHEMA IF NOT EXISTS user_svc").Error; err != nil {',
|
|
118
|
+
'\treturn fmt.Errorf("create schema user_svc: %w", err)',
|
|
119
|
+
"}",
|
|
120
|
+
].join("\n");
|
|
121
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SCHEMA_MARKER, schemaBlock, "CREATE SCHEMA IF NOT EXISTS user_svc");
|
|
122
|
+
const migrateLines = ["&usermodel.User{},", "&usermodel.Identity{},", "&usermodel.LoginThrottle{},"];
|
|
123
|
+
// only the Postgres store has a table for AutoMigrate to create
|
|
124
|
+
if (store === "postgres")
|
|
125
|
+
migrateLines.push("&usermodel.AuthToken{},");
|
|
126
|
+
for (const line of migrateLines) {
|
|
127
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, MODEL_MARKER, line, line);
|
|
128
|
+
}
|
|
129
|
+
// same two-line shape every generated module uses: a named service, then
|
|
130
|
+
// the handler that registers it. `add rbac` extends both lines later, and a
|
|
131
|
+
// human wiring another domain into user's service edits line one in place.
|
|
132
|
+
const { tokenStore, limiter, mailer } = authWiringLines(w);
|
|
133
|
+
const svcLine = `userSvc := user.NewService(user.NewRepository(db), ${tokenStore}, ${mailer}, cfg)`;
|
|
134
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, svcLine, "userSvc :=");
|
|
135
|
+
const routeLine = `user.NewHandler(userSvc, cfg.JWTSecret, cfg.JWTRefreshTTL, cfg.CookieSecure, cfg.CookieSameSite, ${limiter}).Register(api)`;
|
|
91
136
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, ROUTE_MARKER, routeLine, routeLine);
|
|
92
137
|
content = content.replace(/\n\t_ = api \/\/ dropped once `generate module` registers the first route\n/, "\n");
|
|
138
|
+
if (w.worker) {
|
|
139
|
+
const shutdownBlock = ["if err := q.Close(); err != nil {", '\tlogger.Error("close queue", "error", err)', "}"].join("\n");
|
|
140
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := q.Close()");
|
|
141
|
+
}
|
|
142
|
+
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
143
|
+
}
|
|
144
|
+
// upgradeMailerToQueue is `add worker` arriving after `add auth`. Auth wired a
|
|
145
|
+
// synchronous mailer because there was no queue at the time; now there is one,
|
|
146
|
+
// so the enqueuer gets built and the mailer swapped for the async client.
|
|
147
|
+
//
|
|
148
|
+
// Without this the printed "run `add worker` later to move it onto the queue"
|
|
149
|
+
// would be a lie, and the project would keep blocking on SMTP with a perfectly
|
|
150
|
+
// good queue sitting next to it.
|
|
151
|
+
//
|
|
152
|
+
// No-op on a project whose auth already had a worker, and on one with no auth
|
|
153
|
+
// at all — both simply don't contain the line it looks for. Returns whether
|
|
154
|
+
// it actually upgraded something, so the caller's printed summary can say
|
|
155
|
+
// which happened instead of always assuming "no auth yet".
|
|
156
|
+
function upgradeMailerToQueue(mainGoPath, goModule, queueBackend) {
|
|
157
|
+
let content = fs_extra_1.default.readFileSync(mainGoPath, "utf8");
|
|
158
|
+
const syncMailer = "mail.NewSyncClient(mail.Open(cfg))";
|
|
159
|
+
if (!content.includes(syncMailer))
|
|
160
|
+
return false;
|
|
161
|
+
const queueImportLine = `"${goModule}/internal/platform/queue"`;
|
|
162
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, IMPORT_MARKER, queueImportLine, queueImportLine);
|
|
163
|
+
const queueCtor = queueBackend === "river" ? "queue.NewRiverEnqueuer(db)" : "queue.NewAsynqEnqueuer(cfg.RedisURL)";
|
|
164
|
+
const queueInitBlock = [`q, err := ${queueCtor}`, "if err != nil {", '\treturn fmt.Errorf("open queue: %w", err)', "}"].join("\n");
|
|
165
|
+
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, PLATFORM_INIT_MARKER, queueInitBlock, "q, err := queue.New");
|
|
93
166
|
const shutdownBlock = ["if err := q.Close(); err != nil {", '\tlogger.Error("close queue", "error", err)', "}"].join("\n");
|
|
94
167
|
content = (0, marker_patch_1.insertBeforeMarkerOnce)(content, SHUTDOWN_MARKER, shutdownBlock, "if err := q.Close()");
|
|
168
|
+
content = content.replace(syncMailer, () => "mail.NewAsyncClient(q)");
|
|
95
169
|
fs_extra_1.default.writeFileSync(mainGoPath, content);
|
|
170
|
+
return true;
|
|
96
171
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.configPath = configPath;
|
|
7
7
|
exports.writeConfig = writeConfig;
|
|
8
8
|
exports.readConfig = readConfig;
|
|
9
|
+
exports.isProjectDir = isProjectDir;
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
10
11
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
12
|
const CONFIG_FILE = "go-scaffold.config.json";
|
|
@@ -26,28 +27,75 @@ function readConfig(projectDir) {
|
|
|
26
27
|
}
|
|
27
28
|
function detectConfig(projectDir) {
|
|
28
29
|
const goModPath = path_1.default.join(projectDir, "go.mod");
|
|
30
|
+
const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "main.go");
|
|
29
31
|
if (!fs_extra_1.default.existsSync(goModPath)) {
|
|
30
32
|
throw new Error(`no ${CONFIG_FILE} and no go.mod found in ${projectDir} — run this inside a go-scaffold project`);
|
|
31
33
|
}
|
|
34
|
+
// go.mod alone is just "some Go module". Without cmd/api/main.go there is
|
|
35
|
+
// nothing to wire a module into, and every command would write its files
|
|
36
|
+
// first and only then die on a missing main.go — a half-scaffolded
|
|
37
|
+
// directory the user has to clean up by hand.
|
|
38
|
+
if (!fs_extra_1.default.existsSync(mainGoPath)) {
|
|
39
|
+
throw new Error(`${projectDir} has a go.mod but no cmd/api/main.go — this looks like a plain Go module, not a go-scaffold project.\n` +
|
|
40
|
+
`Run \`go-scaffold create <name>\` to start one.`);
|
|
41
|
+
}
|
|
32
42
|
const goMod = fs_extra_1.default.readFileSync(goModPath, "utf8");
|
|
33
43
|
const moduleMatch = goMod.match(/^module\s+(\S+)/m);
|
|
34
44
|
const goModule = moduleMatch ? moduleMatch[1] : path_1.default.basename(projectDir);
|
|
35
|
-
// parse the chosen prefix back out of `api := r.Group("/v1")
|
|
36
|
-
//
|
|
45
|
+
// parse the chosen prefix back out of `api := r.Group("/v1")`; an empty
|
|
46
|
+
// group (`r.Group("")`) or no match at all means no prefix.
|
|
47
|
+
//
|
|
48
|
+
// Read from cmd/api/wiring.go where the composition root lives now, falling
|
|
49
|
+
// back to main.go for projects scaffolded before it was split out — this is
|
|
50
|
+
// the config-less path, so it is exactly the old projects that reach it.
|
|
51
|
+
const wiringGoPath = path_1.default.join(projectDir, "cmd", "api", "wiring.go");
|
|
52
|
+
const compositionRoot = fs_extra_1.default.existsSync(wiringGoPath) ? wiringGoPath : mainGoPath;
|
|
37
53
|
let apiPrefix = "";
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
const groupMatch = fs_extra_1.default.readFileSync(compositionRoot, "utf8").match(/api\s*:=\s*r\.Group\("\/?([a-z0-9/]*)"\)/);
|
|
55
|
+
if (groupMatch)
|
|
56
|
+
apiPrefix = groupMatch[1];
|
|
57
|
+
// Every feature is detectable from the tree each `add` command creates, so
|
|
58
|
+
// detect them all rather than reporting only docker/openapi. Leaving the
|
|
59
|
+
// rest undefined made a config-less project a dead end: `add auth` refused
|
|
60
|
+
// ("needs add worker first") while `add worker` also refused ("queue
|
|
61
|
+
// already exists"), with no way out. Worse, the first `add` to succeed then
|
|
62
|
+
// wrote a config that recorded the undetected features as absent.
|
|
63
|
+
const has = (...segments) => fs_extra_1.default.existsSync(path_1.default.join(projectDir, ...segments));
|
|
64
|
+
const worker = has("internal", "platform", "queue");
|
|
44
65
|
return {
|
|
45
66
|
projectName: path_1.default.basename(projectDir),
|
|
46
67
|
goModule,
|
|
47
68
|
apiPrefix,
|
|
48
69
|
features: {
|
|
49
|
-
docker:
|
|
50
|
-
openapiDocs:
|
|
70
|
+
docker: has("docker-compose.yml"),
|
|
71
|
+
openapiDocs: has("docs", "openapi.yaml"),
|
|
72
|
+
worker,
|
|
73
|
+
// which adapter file is present is what `add worker --queue` decided
|
|
74
|
+
queue: !worker ? undefined : has("internal", "platform", "queue", "asynq.go") ? "asynq" : "river",
|
|
75
|
+
auth: has("internal", "app", "user"),
|
|
76
|
+
// which store `add auth` chose is readable from which implementation
|
|
77
|
+
// file it wrote — same trick as the queue adapter above. Projects from
|
|
78
|
+
// before the option existed have neither name and read as "redis",
|
|
79
|
+
// which is what they in fact are.
|
|
80
|
+
authStore: !has("internal", "app", "user")
|
|
81
|
+
? undefined
|
|
82
|
+
: has("internal", "app", "user", "tokenstore_pg.go")
|
|
83
|
+
? "postgres"
|
|
84
|
+
: "redis",
|
|
85
|
+
rbac: has("internal", "app", "role"),
|
|
86
|
+
observability: has("internal", "platform", "telemetry"),
|
|
51
87
|
},
|
|
52
88
|
};
|
|
53
89
|
}
|
|
90
|
+
// isProjectDir answers "would readConfig succeed here?" without throwing, so
|
|
91
|
+
// the top-level menu can offer only what can actually run in this directory
|
|
92
|
+
// instead of asking two questions and then failing.
|
|
93
|
+
function isProjectDir(projectDir) {
|
|
94
|
+
try {
|
|
95
|
+
readConfig(projectDir);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/utils/gocheck.js
CHANGED
|
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.typeChecks = typeChecks;
|
|
7
|
+
exports.parseChecks = parseChecks;
|
|
8
|
+
exports.assertStillParses = assertStillParses;
|
|
7
9
|
exports.assertNoDrift = assertNoDrift;
|
|
8
10
|
const child_process_1 = require("child_process");
|
|
9
11
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
@@ -32,6 +34,44 @@ function typeChecks(projectRoot) {
|
|
|
32
34
|
return { ok: false, output: `${e.stderr?.toString() ?? ""}${e.stdout?.toString() ?? ""}`.trim() };
|
|
33
35
|
}
|
|
34
36
|
}
|
|
37
|
+
// parseChecks asks the weaker question typeChecks can't: does every .go file
|
|
38
|
+
// in the tree still *parse*? gofmt only reads files, so this works before
|
|
39
|
+
// `go mod tidy` has resolved a new feature's third-party imports — which is
|
|
40
|
+
// precisely when `add auth`/`add worker`/`add observability` do their most
|
|
41
|
+
// invasive patching and when `go vet` is guaranteed to fail for reasons that
|
|
42
|
+
// have nothing to do with us.
|
|
43
|
+
function parseChecks(projectRoot) {
|
|
44
|
+
try {
|
|
45
|
+
(0, child_process_1.execFileSync)("gofmt", ["-l", "-e", "."], { cwd: projectRoot, stdio: ["ignore", "pipe", "pipe"] });
|
|
46
|
+
return { ok: true, output: "" };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const e = err;
|
|
50
|
+
if (e.code === "ENOENT")
|
|
51
|
+
return null; // no Go toolchain — can't conclude anything
|
|
52
|
+
return { ok: false, output: `${e.stderr?.toString() ?? ""}`.trim() };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// assertStillParses is the before/after pair for a command that patches files
|
|
56
|
+
// rather than only adding them. A marker patch that lands in the wrong place
|
|
57
|
+
// produces Go that doesn't parse, and without this the command goes on to
|
|
58
|
+
// print success over it — gofmt's own failure used to be swallowed whole.
|
|
59
|
+
//
|
|
60
|
+
// Same "only blame yourself for a passed → broken transition" rule as
|
|
61
|
+
// assertNoDrift: a project that already had an unparseable file (mid-edit, a
|
|
62
|
+
// scratch file) is not this command's problem.
|
|
63
|
+
function assertStillParses(projectRoot, before, didWhat) {
|
|
64
|
+
if (before === null || !before.ok)
|
|
65
|
+
return;
|
|
66
|
+
const after = parseChecks(projectRoot);
|
|
67
|
+
if (after === null || after.ok)
|
|
68
|
+
return;
|
|
69
|
+
throw new Error(`${picocolors_1.default.red(`${didWhat}, but the result is not valid Go — a patch landed somewhere it doesn't fit.`)}\n\n` +
|
|
70
|
+
`${picocolors_1.default.dim("gofmt says:")}\n${after.output}\n\n` +
|
|
71
|
+
`The files were left as-is so you can see the damage. Most likely one of the files this\n` +
|
|
72
|
+
`command patches was hand-edited away from the shape it was generated in. Please report\n` +
|
|
73
|
+
`this if that isn't the case — the CLI should never write Go that doesn't parse.`);
|
|
74
|
+
}
|
|
35
75
|
// assertNoDrift is the second half of a before/after pair: given what
|
|
36
76
|
// typeChecks said *before* the files were written, it re-checks and fails
|
|
37
77
|
// loudly if generating is what broke the project.
|
|
@@ -46,20 +86,32 @@ function typeChecks(projectRoot) {
|
|
|
46
86
|
// is normal, expected work — the generated code stops compiling against it.
|
|
47
87
|
// Without this check that lands as a mystery build error some time later, in
|
|
48
88
|
// files the user never wrote.
|
|
49
|
-
function assertNoDrift(projectRoot, before, config
|
|
89
|
+
function assertNoDrift(projectRoot, before, config,
|
|
90
|
+
// What the command did, for commands that patch rather than generate
|
|
91
|
+
// (`add rbac`, `undo module`). Their failure isn't template drift, it's a
|
|
92
|
+
// marker patch that landed against a main.go it didn't recognise.
|
|
93
|
+
patched) {
|
|
50
94
|
if (before === null || !before.ok)
|
|
51
95
|
return; // no Go here, or already broken — not ours to judge
|
|
52
96
|
const after = typeChecks(projectRoot);
|
|
53
97
|
if (after === null || after.ok)
|
|
54
98
|
return;
|
|
55
99
|
const scaffoldedWith = config.scaffoldVersion ?? "unknown (predates version stamping)";
|
|
100
|
+
const versions = ` scaffolded with: go-scaffold ${scaffoldedWith}\n` +
|
|
101
|
+
` this CLI: go-scaffold ${(0, version_1.cliVersion)()}\n\n` +
|
|
102
|
+
`${picocolors_1.default.dim("go vet ./... says:")}\n${after.output}`;
|
|
103
|
+
if (patched) {
|
|
104
|
+
throw new Error(`${picocolors_1.default.red(`${patched.didWhat}, but the project no longer compiles.`)}\n\n` +
|
|
105
|
+
`This command patches existing files at marker comments. If those files were\n` +
|
|
106
|
+
`hand-edited — or were written by a different CLI version — a patch can land\n` +
|
|
107
|
+
`somewhere it doesn't fit, and the result only shows up as a build error.\n\n` +
|
|
108
|
+
`${versions}\n\n${patched.recover}`);
|
|
109
|
+
}
|
|
56
110
|
throw new Error(`${picocolors_1.default.red("the generated code doesn't compile, but this project was fine a moment ago.")}\n\n` +
|
|
57
111
|
`The most likely cause is drift: this project's internal/shared layer has been edited\n` +
|
|
58
112
|
`since it was scaffolded, so the templates this CLI emits no longer match it.\n\n` +
|
|
59
|
-
|
|
60
|
-
` this CLI: go-scaffold ${(0, version_1.cliVersion)()}\n\n` +
|
|
61
|
-
`${picocolors_1.default.dim("go vet ./... says:")}\n${after.output}\n\n` +
|
|
113
|
+
`${versions}\n\n` +
|
|
62
114
|
`The generated files were left in place — reconcile them with your shared/ layer by\n` +
|
|
63
|
-
`hand, or undo (\`go-scaffold
|
|
115
|
+
`hand, or undo (\`go-scaffold undo module <name>\` for a module) and generate again\n` +
|
|
64
116
|
`with a CLI version that matches this project.`);
|
|
65
117
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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.patchGolangciForModule = patchGolangciForModule;
|
|
7
|
+
exports.unpatchGolangciForModule = unpatchGolangciForModule;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const marker_patch_1 = require("./marker-patch");
|
|
10
|
+
const DEPGUARD_MARKER = "# go-scaffold:depguard-rules";
|
|
11
|
+
function ruleName(pkg) {
|
|
12
|
+
return `domain-isolation-${pkg}`;
|
|
13
|
+
}
|
|
14
|
+
// depguardRule is the per-domain boundary rule. It has to be per-domain
|
|
15
|
+
// because depguard matches import paths as static prefixes with no idea which
|
|
16
|
+
// domain a file belongs to: a single `deny: internal/app` rule would also
|
|
17
|
+
// reject `order` importing its own `order/model`. Scoping with `files:` and
|
|
18
|
+
// allowing exactly this domain's own path gives "may import myself, may not
|
|
19
|
+
// import a sibling".
|
|
20
|
+
//
|
|
21
|
+
// Written with no leading indentation — insertBeforeMarker re-indents the
|
|
22
|
+
// whole block to match the marker's own column.
|
|
23
|
+
function depguardRule(goModule, pkg) {
|
|
24
|
+
return [
|
|
25
|
+
`${ruleName(pkg)}:`,
|
|
26
|
+
` list-mode: lax`,
|
|
27
|
+
` files:`,
|
|
28
|
+
` - "**/internal/app/${pkg}/**"`,
|
|
29
|
+
` allow:`,
|
|
30
|
+
` - "${goModule}/internal/app/${pkg}"`,
|
|
31
|
+
` deny:`,
|
|
32
|
+
` - pkg: "${goModule}/internal/app"`,
|
|
33
|
+
` desc: >-`,
|
|
34
|
+
` a domain must not import another domain directly — declare a`,
|
|
35
|
+
` consumer-side interface for what you need and let`,
|
|
36
|
+
` cmd/api/wiring.go wire the concrete service in`,
|
|
37
|
+
` (docs/architect/patterns.md)`,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
// patchGolangciForModule adds this domain's boundary rule. No-op on a project
|
|
41
|
+
// whose .golangci.yml predates the marker (or was replaced wholesale) — a
|
|
42
|
+
// missing lint rule must never fail code generation.
|
|
43
|
+
function patchGolangciForModule(golangciPath, goModule, pkg) {
|
|
44
|
+
if (!fs_extra_1.default.existsSync(golangciPath))
|
|
45
|
+
return;
|
|
46
|
+
const content = fs_extra_1.default.readFileSync(golangciPath, "utf8");
|
|
47
|
+
if (!content.includes(DEPGUARD_MARKER))
|
|
48
|
+
return;
|
|
49
|
+
fs_extra_1.default.writeFileSync(golangciPath, (0, marker_patch_1.insertBeforeMarkerOnce)(content, DEPGUARD_MARKER, depguardRule(goModule, pkg), `${ruleName(pkg)}:`));
|
|
50
|
+
}
|
|
51
|
+
// unpatchGolangciForModule drops the rule again, matched by its heading and
|
|
52
|
+
// everything indented under it — the inverse of patchGolangciForModule.
|
|
53
|
+
function unpatchGolangciForModule(golangciPath, pkg) {
|
|
54
|
+
if (!fs_extra_1.default.existsSync(golangciPath))
|
|
55
|
+
return;
|
|
56
|
+
const lines = fs_extra_1.default.readFileSync(golangciPath, "utf8").split("\n");
|
|
57
|
+
const heading = `${ruleName(pkg)}:`;
|
|
58
|
+
const start = lines.findIndex((l) => l.trim() === heading);
|
|
59
|
+
if (start === -1)
|
|
60
|
+
return;
|
|
61
|
+
const indent = lines[start].length - lines[start].trimStart().length;
|
|
62
|
+
let end = start + 1;
|
|
63
|
+
while (end < lines.length) {
|
|
64
|
+
const line = lines[end];
|
|
65
|
+
// blank lines belong to the block only if something indented follows
|
|
66
|
+
const deeper = line.trim() === "" || line.length - line.trimStart().length > indent;
|
|
67
|
+
if (!deeper)
|
|
68
|
+
break;
|
|
69
|
+
end++;
|
|
70
|
+
}
|
|
71
|
+
lines.splice(start, end - start);
|
|
72
|
+
fs_extra_1.default.writeFileSync(golangciPath, lines.join("\n"));
|
|
73
|
+
}
|