@lenne.tech/cli 1.34.0 → 1.35.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 +6 -5
- package/build/commands/config/validate.js +0 -4
- package/build/commands/deployment/create.js +218 -213
- package/build/commands/dev/init.js +2 -1
- package/build/commands/dev/status.js +2 -0
- package/build/commands/dev/test.js +2 -2
- package/build/commands/dev/up.js +8 -4
- package/build/commands/fullstack/add-api.js +6 -9
- package/build/commands/fullstack/add-app.js +5 -7
- package/build/commands/fullstack/init.js +8 -9
- package/build/commands/status.js +2 -0
- package/build/commands/ticket/list.js +2 -0
- package/build/extensions/server.js +140 -18
- package/build/lib/angular-environments.js +108 -0
- package/build/lib/dev-identity.js +38 -18
- package/build/lib/dev-project.js +20 -7
- package/build/lib/dev-state.js +17 -1
- package/build/lib/dev-test-session.js +31 -3
- package/build/lib/ensure-root-dockerignore.js +92 -0
- package/build/lib/fs-utils.js +19 -0
- package/build/lib/hoist-workspace-pnpm-config.js +2 -11
- package/build/lib/remove-nested-lockfiles.js +53 -0
- package/build/lib/turboops-config.js +93 -0
- package/build/lib/workspace-integration.js +96 -3
- package/build/templates/deployment/turboops.json.ejs +3 -0
- package/build/templates/vendor-scripts/migrate-store.js +25 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +30 -9
- package/docs/lt.config.md +9 -19
- package/package.json +1 -1
- package/build/templates/deployment/.github/workflows/pre-release.yml.ejs +0 -41
- package/build/templates/deployment/.github/workflows/release.yml.ejs +0 -41
- package/build/templates/deployment/.gitlab-ci.yml.ejs +0 -181
- package/build/templates/deployment/Dockerfile.app.ejs +0 -13
- package/build/templates/deployment/Dockerfile.ejs +0 -18
- package/build/templates/deployment/docker-compose.dev.yml.ejs +0 -99
- package/build/templates/deployment/docker-compose.prod.yml.ejs +0 -92
- package/build/templates/deployment/docker-compose.test.yml.ejs +0 -98
- package/build/templates/deployment/scripts/build-push.sh.ejs +0 -20
- package/build/templates/deployment/scripts/deploy.sh.ejs +0 -7
|
@@ -9,6 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.TEST_INITIAL_ADMIN_ENV = void 0;
|
|
12
13
|
exports.autoShardCount = autoShardCount;
|
|
13
14
|
exports.bringUpTestSession = bringUpTestSession;
|
|
14
15
|
exports.hasTestSession = hasTestSession;
|
|
@@ -57,6 +58,25 @@ const TEST_APP_LOG = 'app.test.log';
|
|
|
57
58
|
const TEST_BRIDGE_FILE = '.env.test';
|
|
58
59
|
/** Internal port band for the test stack — distinct from the dev band (4000+). */
|
|
59
60
|
const TEST_PORT_BASE = 4500;
|
|
61
|
+
/**
|
|
62
|
+
* Throwaway initial-admin credentials for the isolated `lt dev test` database.
|
|
63
|
+
*
|
|
64
|
+
* The nest-server core auto-parses these `NSC__…` vars into
|
|
65
|
+
* `systemSetup.initialAdmin.*` and seeds the admin ONCE on an empty DB (it never
|
|
66
|
+
* overwrites an existing admin), so a set-up system exists before the suite runs.
|
|
67
|
+
* A fresh template project's standard auth E2E specs assume such a system — they
|
|
68
|
+
* do no self-setup — so without this they fail LOCALLY on the empty per-run DB.
|
|
69
|
+
* These are the exact values the lt-monorepo template CI uses
|
|
70
|
+
* (`.gitlab-ci.yml` / `.github/workflows/test.yml`), keeping CI ↔ local parity.
|
|
71
|
+
*
|
|
72
|
+
* Injected ONLY into the `lt dev test` API process (its DB is fresh + discarded
|
|
73
|
+
* per run), NEVER into `lt dev up` — no surprise admin in the persistent dev DB.
|
|
74
|
+
*/
|
|
75
|
+
exports.TEST_INITIAL_ADMIN_ENV = {
|
|
76
|
+
NSC__SYSTEM_SETUP__INITIAL_ADMIN__EMAIL: 'ci-admin@test.com',
|
|
77
|
+
NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME: 'CI Admin',
|
|
78
|
+
NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD: 'CiThrowawayAdmin123!',
|
|
79
|
+
};
|
|
60
80
|
/**
|
|
61
81
|
* Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
|
|
62
82
|
*
|
|
@@ -205,7 +225,12 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
205
225
|
const entry = ['dist/src/main.js', 'dist/main.js']
|
|
206
226
|
.map((rel) => (0, path_1.join)(layout.apiDir, rel))
|
|
207
227
|
.find((p) => (0, fs_1.existsSync)(p));
|
|
208
|
-
|
|
228
|
+
// Seed a throwaway initial admin into the fresh, isolated test DB so the
|
|
229
|
+
// standard auth E2E specs run against a set-up system — locally exactly like
|
|
230
|
+
// the lt-monorepo template CI. Defaults first so an explicitly inherited
|
|
231
|
+
// `NSC__…INITIAL_ADMIN__…` still wins (deliberate override respected). Only
|
|
232
|
+
// reached from `lt dev test` — `lt dev up` never calls bringUpTestSession.
|
|
233
|
+
const apiEnv = Object.assign(Object.assign(Object.assign({}, exports.TEST_INITIAL_ADMIN_ENV), devEnv.api.env), { NODE_ENV: 'local' });
|
|
209
234
|
let apiSpawn;
|
|
210
235
|
if (build === 0 && entry) {
|
|
211
236
|
apiSpawn = (0, dev_process_1.spawnDetached)('node', [entry], {
|
|
@@ -288,7 +313,10 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
288
313
|
if (!apiReady)
|
|
289
314
|
log.warn(`Test API did not answer 2xx on ${apiUrl}/meta within 120s — the first specs may skip.`);
|
|
290
315
|
}
|
|
291
|
-
|
|
316
|
+
// Expose THIS stack's API log path so the caller can point the auth E2E specs
|
|
317
|
+
// (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
|
|
318
|
+
const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
|
|
319
|
+
return { apiLogPath, apiUrl, appEnv: devEnv.app.env, appUrl, dbName, pids, testIdentity };
|
|
292
320
|
});
|
|
293
321
|
}
|
|
294
322
|
/** True when a test session file exists (used by status/down). */
|
|
@@ -345,7 +373,7 @@ function runShardedTestSession(layout, baseIdentity, log, opts) {
|
|
|
345
373
|
// suite runs under concurrent sharded load, so it can relax navigation /
|
|
346
374
|
// test timeouts (N built SSR servers + N Chromium saturate the CPU and slow
|
|
347
375
|
// every navigation) without loosening them for serial runs.
|
|
348
|
-
const env = Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
|
|
376
|
+
const env = Object.assign(Object.assign(Object.assign({}, ctx.appEnv), { LT_DEV_TEST_SHARDS: String(total), MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` }), (ctx.apiLogPath ? { NEST_SERVER_LOG: ctx.apiLogPath } : {}));
|
|
349
377
|
const logFile = (0, path_1.join)(layout.root, '.lt-dev', `shard.${index}.test.log`);
|
|
350
378
|
// Invoke Playwright DIRECTLY via the manager's `exec` (NOT `<pm> run
|
|
351
379
|
// test:e2e -- …`): forwarding option flags through `<pm> run`'s `--`
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureRootDockerignore = ensureRootDockerignore;
|
|
4
|
+
// Docker resolves `.dockerignore` patterns against the build CONTEXT ROOT, and
|
|
5
|
+
// it only ever reads the file at that root (or `<dockerfile>.dockerignore` next
|
|
6
|
+
// to the Dockerfile). A monorepo builds its images from the workspace root
|
|
7
|
+
// (`context: .`, `dockerfile: projects/app/Dockerfile`), so the starters' own
|
|
8
|
+
// `projects/app/.dockerignore` and `projects/api/.dockerignore` are never
|
|
9
|
+
// consulted — the workspace root needs its own file.
|
|
10
|
+
//
|
|
11
|
+
// The same rule bites the patterns: a bare `node_modules` matches only
|
|
12
|
+
// `./node_modules`, never `projects/app/node_modules`. Every pattern that must
|
|
13
|
+
// hit a sub-project therefore needs a globstar prefix. A root `.dockerignore`
|
|
14
|
+
// that looks complete but uses bare patterns silently ships a developer's local
|
|
15
|
+
// `.output` and `.env` into the image — and `nuxt build` reads `.env`, so the
|
|
16
|
+
// bundle itself changes.
|
|
17
|
+
const REQUIRED_PATTERNS = [
|
|
18
|
+
'**/node_modules',
|
|
19
|
+
'**/.output',
|
|
20
|
+
'**/.nuxt',
|
|
21
|
+
'**/dist',
|
|
22
|
+
// `**/.env` matches a path component EXACTLY, so it does NOT cover
|
|
23
|
+
// `.env.production` / `.env.staging` / `.env.test` — all of which routinely
|
|
24
|
+
// hold real secrets and would otherwise be copied into the build context.
|
|
25
|
+
// `**/.env.*` closes that gap (it also subsumes the old `.env.local` /
|
|
26
|
+
// `.env.*.local`); the trailing `!**/.env.example` re-includes the safe
|
|
27
|
+
// placeholder template and MUST stay after the broad glob to win.
|
|
28
|
+
'**/.env',
|
|
29
|
+
'**/.env.*',
|
|
30
|
+
'!**/.env.example',
|
|
31
|
+
// `.lt-dev/` holds the ENV bridge (`.env.test`, same DB/URLs) and session
|
|
32
|
+
// logs that contain email-verification tokens — never ship them.
|
|
33
|
+
'**/.lt-dev',
|
|
34
|
+
'**/coverage',
|
|
35
|
+
'**/test-results',
|
|
36
|
+
'**/playwright-report',
|
|
37
|
+
'**/*.log',
|
|
38
|
+
'.pnpm-store',
|
|
39
|
+
'.git',
|
|
40
|
+
];
|
|
41
|
+
const HEADER = `# Docker resolves these patterns against the build CONTEXT ROOT — this directory
|
|
42
|
+
# — and never against a Dockerfile's own directory. The starters ship a
|
|
43
|
+
# projects/*/.dockerignore for standalone builds; those are NOT read when
|
|
44
|
+
# building from here (see docker-compose.yml: \`context: .\`).
|
|
45
|
+
#
|
|
46
|
+
# For the same reason a bare \`node_modules\` would only match ./node_modules;
|
|
47
|
+
# every pattern that must also hit projects/* needs a globstar prefix. Without
|
|
48
|
+
# them a local .output would be copied into the image, and \`nuxt build\` would
|
|
49
|
+
# read a local .env and bake it into the bundle.
|
|
50
|
+
#
|
|
51
|
+
# Managed by \`lt fullstack init\` / \`add-api\` / \`add-app\`: missing entries are
|
|
52
|
+
# appended, existing ones are never removed. Add project-specific ignores freely.
|
|
53
|
+
`;
|
|
54
|
+
const MANAGED_MARKER = '# --- required by lt (depth-independent globs) ---';
|
|
55
|
+
/**
|
|
56
|
+
* Guarantee a workspace-root `.dockerignore` that keeps host artefacts and
|
|
57
|
+
* secrets out of every image built from this context.
|
|
58
|
+
*
|
|
59
|
+
* Creates the file when missing. When it exists, appends only the required
|
|
60
|
+
* patterns it does not already contain — an existing bare `node_modules` does
|
|
61
|
+
* not satisfy the globstar variant, so both may end up present, which is
|
|
62
|
+
* correct and harmless. Never removes or reorders what the project wrote.
|
|
63
|
+
*
|
|
64
|
+
* Idempotent: a second run on an unchanged file adds nothing.
|
|
65
|
+
*
|
|
66
|
+
* @param options.filesystem gluegun filesystem toolbox member
|
|
67
|
+
* @param options.projectDir workspace root (the docker build context)
|
|
68
|
+
* @returns which patterns were added, and whether the file had to be created
|
|
69
|
+
*/
|
|
70
|
+
function ensureRootDockerignore(options) {
|
|
71
|
+
const { filesystem, projectDir } = options;
|
|
72
|
+
const path = `${projectDir}/.dockerignore`;
|
|
73
|
+
if (!filesystem.exists(path)) {
|
|
74
|
+
filesystem.write(path, `${HEADER}\n${REQUIRED_PATTERNS.join('\n')}\n`);
|
|
75
|
+
return { added: [...REQUIRED_PATTERNS], created: true };
|
|
76
|
+
}
|
|
77
|
+
const content = filesystem.read(path) || '';
|
|
78
|
+
const present = activePatterns(content);
|
|
79
|
+
const missing = REQUIRED_PATTERNS.filter((pattern) => !present.has(pattern));
|
|
80
|
+
if (missing.length === 0)
|
|
81
|
+
return { added: [], created: false };
|
|
82
|
+
const separator = content.endsWith('\n') ? '' : '\n';
|
|
83
|
+
filesystem.write(path, `${content}${separator}\n${MANAGED_MARKER}\n${missing.join('\n')}\n`);
|
|
84
|
+
return { added: missing, created: false };
|
|
85
|
+
}
|
|
86
|
+
/** Non-empty, non-comment lines, trimmed — the patterns actually in effect. */
|
|
87
|
+
function activePatterns(content) {
|
|
88
|
+
return new Set(content
|
|
89
|
+
.split('\n')
|
|
90
|
+
.map((line) => line.trim())
|
|
91
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')));
|
|
92
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSymlink = isSymlink;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
/**
|
|
6
|
+
* Whether `path` is a symbolic link (false on any stat error).
|
|
7
|
+
*
|
|
8
|
+
* Shared by the workspace-normalization helpers that must not mutate a
|
|
9
|
+
* `--api-link` / `--frontend-link` sub-project pointing at the user's own
|
|
10
|
+
* checkout.
|
|
11
|
+
*/
|
|
12
|
+
function isSymlink(path) {
|
|
13
|
+
try {
|
|
14
|
+
return (0, fs_1.lstatSync)(path).isSymbolicLink();
|
|
15
|
+
}
|
|
16
|
+
catch (_a) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.hoistWorkspacePnpmConfig = hoistWorkspacePnpmConfig;
|
|
4
|
-
const fs_1 = require("fs");
|
|
5
4
|
const js_yaml_1 = require("js-yaml");
|
|
5
|
+
const fs_utils_1 = require("./fs-utils");
|
|
6
6
|
/**
|
|
7
7
|
* pnpm workspace-scoped fields that must live at the workspace root.
|
|
8
8
|
* When present in sub-project package.json files, pnpm emits:
|
|
@@ -77,7 +77,7 @@ function hoistWorkspacePnpmConfig(options) {
|
|
|
77
77
|
continue;
|
|
78
78
|
// Never mutate a symlinked sub-project — it points at the user's own
|
|
79
79
|
// checkout in link mode.
|
|
80
|
-
if (isSymlink(subPath))
|
|
80
|
+
if ((0, fs_utils_1.isSymlink)(subPath))
|
|
81
81
|
continue;
|
|
82
82
|
if (hoistFromSubPackageJson({ filesystem, rootWs, subPath })) {
|
|
83
83
|
rootChanged = true;
|
|
@@ -149,15 +149,6 @@ function hoistFromSubWorkspaceYaml(options) {
|
|
|
149
149
|
}
|
|
150
150
|
return true;
|
|
151
151
|
}
|
|
152
|
-
/** Whether `path` is a symbolic link (false on any stat error). */
|
|
153
|
-
function isSymlink(path) {
|
|
154
|
-
try {
|
|
155
|
-
return (0, fs_1.lstatSync)(path).isSymbolicLink();
|
|
156
|
-
}
|
|
157
|
-
catch (_a) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
152
|
/**
|
|
162
153
|
* Merge two values for a pnpm workspace-scoped field.
|
|
163
154
|
*
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.removeNestedLockfiles = removeNestedLockfiles;
|
|
4
|
+
const fs_utils_1 = require("./fs-utils");
|
|
5
|
+
// In a pnpm workspace only the ROOT pnpm-lock.yaml is read, resolved and
|
|
6
|
+
// updated. A `pnpm-lock.yaml` that a starter brought along from its standalone
|
|
7
|
+
// life keeps sitting in projects/api or projects/app, never gets refreshed, and
|
|
8
|
+
// silently drifts away from the root lockfile — in one project it was a month
|
|
9
|
+
// old and 11k lines shorter.
|
|
10
|
+
//
|
|
11
|
+
// Dead weight would be harmless; the file is worse than that, because anything
|
|
12
|
+
// that builds with the sub-project as its context (an old `Dockerfile.dev`, an
|
|
13
|
+
// ad-hoc `pnpm install --frozen-lockfile` inside projects/app) installs that
|
|
14
|
+
// stale tree instead of the workspace's. Removing it makes the workspace root
|
|
15
|
+
// the single source of truth.
|
|
16
|
+
//
|
|
17
|
+
// Only pnpm lockfiles are removed. `yarn.lock` / `package-lock.json` mark a
|
|
18
|
+
// sub-project that deliberately uses another package manager, and `lt dev`
|
|
19
|
+
// reads them to pick the right binary per project dir. A pnpm sub-project loses
|
|
20
|
+
// nothing: `pickPackageManager()` falls back to pnpm when no lockfile is found.
|
|
21
|
+
const PNPM_LOCKFILE = 'pnpm-lock.yaml';
|
|
22
|
+
/**
|
|
23
|
+
* Delete sub-project `pnpm-lock.yaml` files that the workspace root supersedes.
|
|
24
|
+
*
|
|
25
|
+
* No-op unless `projectDir` really is a pnpm workspace root (it must carry a
|
|
26
|
+
* `pnpm-workspace.yaml`) — outside one a nested lockfile may be the only one.
|
|
27
|
+
* Symlinked sub-projects are skipped: in `--api-link` / `--frontend-link` mode
|
|
28
|
+
* they point at the user's own checkout, which must not be mutated.
|
|
29
|
+
*
|
|
30
|
+
* @param options.filesystem gluegun filesystem toolbox member
|
|
31
|
+
* @param options.projectDir workspace root
|
|
32
|
+
* @param options.subProjects paths relative to the root, e.g. `projects/api`
|
|
33
|
+
* @returns the sub-project dirs whose lockfile was removed
|
|
34
|
+
*/
|
|
35
|
+
function removeNestedLockfiles(options) {
|
|
36
|
+
const { filesystem, projectDir, subProjects } = options;
|
|
37
|
+
if (!filesystem.exists(`${projectDir}/pnpm-workspace.yaml`))
|
|
38
|
+
return { removed: [] };
|
|
39
|
+
const removed = [];
|
|
40
|
+
for (const subDir of subProjects) {
|
|
41
|
+
const subPath = `${projectDir}/${subDir}`;
|
|
42
|
+
if (!filesystem.exists(subPath))
|
|
43
|
+
continue;
|
|
44
|
+
if ((0, fs_utils_1.isSymlink)(subPath))
|
|
45
|
+
continue;
|
|
46
|
+
const lockfile = `${subPath}/${PNPM_LOCKFILE}`;
|
|
47
|
+
if (!filesystem.exists(lockfile))
|
|
48
|
+
continue;
|
|
49
|
+
filesystem.remove(lockfile);
|
|
50
|
+
removed.push(subDir);
|
|
51
|
+
}
|
|
52
|
+
return { removed };
|
|
53
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROJECT_SLUG_PATTERN = exports.DOMAIN_PATTERN = void 0;
|
|
4
|
+
exports.isValidDomain = isValidDomain;
|
|
5
|
+
exports.isValidProjectSlug = isValidProjectSlug;
|
|
6
|
+
exports.mergeTurboOpsConfig = mergeTurboOpsConfig;
|
|
7
|
+
exports.optionString = optionString;
|
|
8
|
+
exports.readJsonObject = readJsonObject;
|
|
9
|
+
/**
|
|
10
|
+
* A plausible multi-label hostname (e.g. `myproject.lenne.tech`).
|
|
11
|
+
*
|
|
12
|
+
* The deploy `domain` is written VERBATIM into generated Angular
|
|
13
|
+
* `environment.*.ts` (which `ng build` bakes into the browser bundle) and into
|
|
14
|
+
* copy-paste checklist lines. A stray quote / `$` / `;` / whitespace would break
|
|
15
|
+
* out of the generated string literal (code injection into the bundle) or emit a
|
|
16
|
+
* non-compiling config, so the value is restricted to hostname characters at the
|
|
17
|
+
* input boundary.
|
|
18
|
+
*/
|
|
19
|
+
exports.DOMAIN_PATTERN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
20
|
+
/**
|
|
21
|
+
* TurboOps project slugs are the registry namespace + the `docker login` user,
|
|
22
|
+
* so `registry.turbo-ops.de/<project>` and `docker login -u "$TURBOOPS_PROJECT"`
|
|
23
|
+
* only accept a lowercase dash-separated slug.
|
|
24
|
+
*/
|
|
25
|
+
exports.PROJECT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
26
|
+
/** True when `domain` is a plausible multi-label hostname. */
|
|
27
|
+
function isValidDomain(domain) {
|
|
28
|
+
return exports.DOMAIN_PATTERN.test(domain);
|
|
29
|
+
}
|
|
30
|
+
/** True when `slug` is a usable TurboOps project slug. */
|
|
31
|
+
function isValidProjectSlug(slug) {
|
|
32
|
+
return exports.PROJECT_SLUG_PATTERN.test(slug);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute the merged `.turboops.json` content for a target `project` slug,
|
|
36
|
+
* WITHOUT any I/O. The file is committed and a user may have added keys by hand,
|
|
37
|
+
* so a re-run must MERGE, never clobber:
|
|
38
|
+
*
|
|
39
|
+
* - no existing config → a fresh `{ project }`;
|
|
40
|
+
* - existing config that is already exactly `{ project }` → `changed: false`
|
|
41
|
+
* (caller skips the write and reports "up to date");
|
|
42
|
+
* - otherwise → `{ ...existing, project }` with `changed: true` (user keys kept).
|
|
43
|
+
*
|
|
44
|
+
* `previousProject` is the prior `project` value when it was a string, so the
|
|
45
|
+
* caller can warn on a project rename.
|
|
46
|
+
*/
|
|
47
|
+
function mergeTurboOpsConfig(existing, project) {
|
|
48
|
+
if (!existing) {
|
|
49
|
+
return { changed: true, config: { project } };
|
|
50
|
+
}
|
|
51
|
+
const previousProject = typeof existing.project === 'string' ? existing.project : undefined;
|
|
52
|
+
const upToDate = existing.project === project && Object.keys(existing).length === 1;
|
|
53
|
+
if (upToDate) {
|
|
54
|
+
return { changed: false, config: existing, previousProject };
|
|
55
|
+
}
|
|
56
|
+
return { changed: true, config: Object.assign(Object.assign({}, existing), { project }), previousProject };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Read a CLI option as a string. gluegun parses a valueless `--project` as the
|
|
60
|
+
* boolean `true`; passing that straight through would write `{"project":"true"}`
|
|
61
|
+
* and point the pipeline at the registry namespace `true`. Whitespace-only is
|
|
62
|
+
* treated as absent.
|
|
63
|
+
*/
|
|
64
|
+
function optionString(value) {
|
|
65
|
+
if (typeof value !== 'string')
|
|
66
|
+
return undefined;
|
|
67
|
+
const trimmed = value.trim();
|
|
68
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read a JSON file without throwing. gluegun's `filesystem.read(path, 'json')`
|
|
72
|
+
* THROWS on malformed JSON (it does NOT return undefined), so every read of a
|
|
73
|
+
* hand-editable file has to guard for it or the whole command crashes with an
|
|
74
|
+
* uncaught stack trace mid-spinner.
|
|
75
|
+
*
|
|
76
|
+
* `found` distinguishes "file is absent" from "file exists but is unusable";
|
|
77
|
+
* `value` is only set when the file parsed into a plain object (a bare `null`,
|
|
78
|
+
* number, or array is valid JSON but cannot hold our keys, so it counts as
|
|
79
|
+
* unusable, not as a config).
|
|
80
|
+
*/
|
|
81
|
+
function readJsonObject(filesystem, path) {
|
|
82
|
+
if (filesystem.exists(path) !== 'file')
|
|
83
|
+
return { found: false };
|
|
84
|
+
try {
|
|
85
|
+
const value = filesystem.read(path, 'json');
|
|
86
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
87
|
+
? { found: true, value: value }
|
|
88
|
+
: { found: true };
|
|
89
|
+
}
|
|
90
|
+
catch (_a) {
|
|
91
|
+
return { found: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -26,6 +26,39 @@
|
|
|
26
26
|
* add-app run on an already-installed workspace and only need to wire
|
|
27
27
|
* in the new sub-project plus run a workspace-wide install.
|
|
28
28
|
*/
|
|
29
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
32
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
33
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
34
|
+
}
|
|
35
|
+
Object.defineProperty(o, k2, desc);
|
|
36
|
+
}) : (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
o[k2] = m[k];
|
|
39
|
+
}));
|
|
40
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
41
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
42
|
+
}) : function(o, v) {
|
|
43
|
+
o["default"] = v;
|
|
44
|
+
});
|
|
45
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
46
|
+
var ownKeys = function(o) {
|
|
47
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
48
|
+
var ar = [];
|
|
49
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
50
|
+
return ar;
|
|
51
|
+
};
|
|
52
|
+
return ownKeys(o);
|
|
53
|
+
};
|
|
54
|
+
return function (mod) {
|
|
55
|
+
if (mod && mod.__esModule) return mod;
|
|
56
|
+
var result = {};
|
|
57
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
58
|
+
__setModuleDefault(result, mod);
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
29
62
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
30
63
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
31
64
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -38,6 +71,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
38
71
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
72
|
exports.detectSubProjectContext = detectSubProjectContext;
|
|
40
73
|
exports.detectWorkspaceLayout = detectWorkspaceLayout;
|
|
74
|
+
exports.finalizeWorkspaceRoot = finalizeWorkspaceRoot;
|
|
41
75
|
exports.findWorkspaceRoot = findWorkspaceRoot;
|
|
42
76
|
exports.isNonInteractive = isNonInteractive;
|
|
43
77
|
exports.reconfigureUpstreamForDownstream = reconfigureUpstreamForDownstream;
|
|
@@ -45,6 +79,10 @@ exports.runExperimentalNestBaseRename = runExperimentalNestBaseRename;
|
|
|
45
79
|
exports.runStandaloneWorkspaceGate = runStandaloneWorkspaceGate;
|
|
46
80
|
exports.shouldProceedAsStandalone = shouldProceedAsStandalone;
|
|
47
81
|
exports.writeApiConfig = writeApiConfig;
|
|
82
|
+
const yaml = __importStar(require("js-yaml"));
|
|
83
|
+
const ensure_root_dockerignore_1 = require("./ensure-root-dockerignore");
|
|
84
|
+
const hoist_workspace_pnpm_config_1 = require("./hoist-workspace-pnpm-config");
|
|
85
|
+
const remove_nested_lockfiles_1 = require("./remove-nested-lockfiles");
|
|
48
86
|
/**
|
|
49
87
|
* Detect whether the current working directory IS a sub-project of an
|
|
50
88
|
* lt-monorepo workspace (i.e. cwd is `projects/api/` or `projects/app/`,
|
|
@@ -95,6 +133,27 @@ function detectWorkspaceLayout(workspaceDir, filesystem) {
|
|
|
95
133
|
const hasApp = filesystem.exists(`${appDir}/package.json`) === 'file';
|
|
96
134
|
return { hasApi, hasApp, hasWorkspace, workspaceDir };
|
|
97
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Normalize a freshly-populated workspace root after adding or removing a
|
|
138
|
+
* sub-project. Runs the three idempotent workspace-hygiene steps that
|
|
139
|
+
* `fullstack init`, `add-api`, and `add-app` all need:
|
|
140
|
+
*
|
|
141
|
+
* 1. hoist pnpm workspace-scoped config (`overrides`, `allowBuilds`, …) out
|
|
142
|
+
* of the sub-projects into the root — pnpm only honours it at the root;
|
|
143
|
+
* 2. remove nested `pnpm-lock.yaml` files the root lockfile supersedes;
|
|
144
|
+
* 3. guarantee a workspace-root `.dockerignore` (Docker never reads a
|
|
145
|
+
* sub-project's own `.dockerignore` when building from the root context).
|
|
146
|
+
*
|
|
147
|
+
* Each step is a no-op when there is nothing to do, so re-runs are safe.
|
|
148
|
+
*/
|
|
149
|
+
function finalizeWorkspaceRoot(options) {
|
|
150
|
+
var _a;
|
|
151
|
+
const { filesystem, projectDir } = options;
|
|
152
|
+
const subProjects = (_a = options.subProjects) !== null && _a !== void 0 ? _a : ['projects/api', 'projects/app'];
|
|
153
|
+
(0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir, subProjects });
|
|
154
|
+
(0, remove_nested_lockfiles_1.removeNestedLockfiles)({ filesystem, projectDir, subProjects });
|
|
155
|
+
(0, ensure_root_dockerignore_1.ensureRootDockerignore)({ filesystem, projectDir });
|
|
156
|
+
}
|
|
98
157
|
/**
|
|
99
158
|
* Walk up from `startDir` until a workspace marker is found or the
|
|
100
159
|
* filesystem root is reached. Limited to 6 levels to avoid pathological
|
|
@@ -374,8 +433,8 @@ function writeApiConfig(options) {
|
|
|
374
433
|
* `detectWorkspaceLayout` and `findWorkspaceRoot`.
|
|
375
434
|
*
|
|
376
435
|
* Recognised markers (any one is sufficient):
|
|
377
|
-
* - `pnpm-workspace.yaml`
|
|
378
|
-
* - `package.json` with `workspaces` field — npm/yarn/bun
|
|
436
|
+
* - `pnpm-workspace.yaml` that declares a non-empty `packages:` list
|
|
437
|
+
* - `package.json` with a non-empty `workspaces` field — npm/yarn/bun
|
|
379
438
|
* - `projects/` directory — lt-monorepo convention
|
|
380
439
|
*
|
|
381
440
|
* Returns false for `node_modules`-style directories that may contain
|
|
@@ -383,7 +442,7 @@ function writeApiConfig(options) {
|
|
|
383
442
|
*/
|
|
384
443
|
function hasWorkspaceMarker(dir, filesystem) {
|
|
385
444
|
var _a;
|
|
386
|
-
if (
|
|
445
|
+
if (pnpmWorkspaceDeclaresPackages(`${dir}/pnpm-workspace.yaml`, filesystem))
|
|
387
446
|
return true;
|
|
388
447
|
if (filesystem.exists(`${dir}/projects`) === 'dir')
|
|
389
448
|
return true;
|
|
@@ -404,3 +463,37 @@ function hasWorkspaceMarker(dir, filesystem) {
|
|
|
404
463
|
}
|
|
405
464
|
return false;
|
|
406
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* True only when `pnpm-workspace.yaml` actually declares member packages.
|
|
468
|
+
*
|
|
469
|
+
* The file's mere existence is NOT a workspace marker. Since pnpm 10/11 it
|
|
470
|
+
* is also the canonical home for workspace-scoped *settings* (`overrides`,
|
|
471
|
+
* `allowBuilds`, `onlyBuiltDependencies`, `minimumReleaseAge*`) — pnpm 11
|
|
472
|
+
* silently ignores those keys in package.json's `pnpm` block. Both
|
|
473
|
+
* nest-server-starter and nuxt-base-starter therefore ship a settings-only
|
|
474
|
+
* `pnpm-workspace.yaml` (no `packages:`) while being plain single-package
|
|
475
|
+
* projects.
|
|
476
|
+
*
|
|
477
|
+
* Treating existence as the marker made every standalone starter look like a
|
|
478
|
+
* monorepo: `resolveLayout` searched for `projects/api` + `projects/app`,
|
|
479
|
+
* found neither, and `lt dev up` / `lt dev init` aborted with
|
|
480
|
+
* "No API or App project detected at this path".
|
|
481
|
+
*
|
|
482
|
+
* Unparseable YAML yields false so the remaining markers still decide,
|
|
483
|
+
* rather than guessing "workspace" from a file we could not read.
|
|
484
|
+
*/
|
|
485
|
+
function pnpmWorkspaceDeclaresPackages(path, filesystem) {
|
|
486
|
+
if (filesystem.exists(path) !== 'file')
|
|
487
|
+
return false;
|
|
488
|
+
const content = filesystem.read(path);
|
|
489
|
+
if (!content)
|
|
490
|
+
return false;
|
|
491
|
+
try {
|
|
492
|
+
const doc = yaml.load(content);
|
|
493
|
+
const packages = doc === null || doc === void 0 ? void 0 : doc.packages;
|
|
494
|
+
return Array.isArray(packages) && packages.length > 0;
|
|
495
|
+
}
|
|
496
|
+
catch (_a) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Migration store. Loaded by the migrate CLI via `--store`.
|
|
2
|
+
//
|
|
3
|
+
// Runs from two layouts:
|
|
4
|
+
// - Repo: migrations-utils/migrate.js → vendored core is TypeScript-only, so
|
|
5
|
+
// ts-node has to be registered before requiring any vendor module.
|
|
6
|
+
// - Image: dist/migrations-utils/migrate.js → everything next to it is compiled
|
|
7
|
+
// JS and ts-node is NOT installed. Requiring it would crash the
|
|
8
|
+
// entrypoint (`set -e`) before the server ever starts.
|
|
9
|
+
//
|
|
10
|
+
// So probe for the compiled helper first and only fall back to ts-node.
|
|
11
|
+
const HELPER = '../src/core/modules/migrate/helpers/migration.helper';
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
require.resolve(`${HELPER}.js`);
|
|
15
|
+
} catch {
|
|
16
|
+
require('./ts-compiler');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { createMigrationStore } = require(HELPER);
|
|
20
|
+
const { resolveMongoUri } = require('./mongo-uri');
|
|
21
|
+
|
|
22
|
+
module.exports = createMigrationStore(
|
|
23
|
+
resolveMongoUri(),
|
|
24
|
+
'migrations', // optional, default is 'migrations'
|
|
25
|
+
);
|
|
@@ -247,7 +247,7 @@ Monorepo Subprojects:
|
|
|
247
247
|
| **MongoDB** | `lt mongodb collection-export`, `mongodb s3-restore` |
|
|
248
248
|
| **Qdrant** | `lt qdrant stats`, `qdrant delete` |
|
|
249
249
|
| **Directus** | `lt directus docker-setup`, `directus typegen`, `directus remove` |
|
|
250
|
-
| **Deployment** | `lt deployment create` (
|
|
250
|
+
| **Deployment** | `lt deployment create` (TurboOps config + GitLab pipeline) |
|
|
251
251
|
| **Blocks/Components** | `lt blocks add`, `lt components add` |
|
|
252
252
|
| **Tools** | `lt tools regex`, `tools sha256`, `tools jwt-read`, `tools crypt`, `tools install-scripts` |
|
|
253
253
|
| **Docs** | `lt docs open` |
|
package/docs/commands.md
CHANGED
|
@@ -1052,24 +1052,45 @@ For mode-aware update workflows after conversion, use:
|
|
|
1052
1052
|
|
|
1053
1053
|
### `lt deployment create`
|
|
1054
1054
|
|
|
1055
|
-
Creates deployment
|
|
1055
|
+
Creates the TurboOps deployment config (`.turboops.json`) in the workspace root and
|
|
1056
|
+
prints the one-time TurboOps setup checklist (project, stages, CI/CD variables, DNS,
|
|
1057
|
+
stage env vars).
|
|
1058
|
+
|
|
1059
|
+
The Dockerfiles, `docker-compose.yml` and `.gitlab-ci.yml` already ship with the
|
|
1060
|
+
lt-monorepo template, so nothing else is generated. An existing `.turboops.json` is
|
|
1061
|
+
merged, not overwritten — hand-added keys survive a re-run.
|
|
1062
|
+
|
|
1063
|
+
**Angular apps** additionally get their environment files pointed at the deployed
|
|
1064
|
+
stage URLs, because Angular bakes them into the bundle at build time (Nuxt reads
|
|
1065
|
+
them from the TurboOps stage env at runtime, so nothing is patched there):
|
|
1066
|
+
|
|
1067
|
+
| File | Stage | API URL | App URL |
|
|
1068
|
+
|------|-------|---------|---------|
|
|
1069
|
+
| `environment.prod.ts` | `production` | `api.<domain>` | `<domain>` |
|
|
1070
|
+
| `environment.develop.ts` | `dev` | `api.dev.<domain>` | `dev.<domain>` |
|
|
1071
|
+
| `environment.test.ts` | `dev` | `api.dev.<domain>` | `dev.<domain>` |
|
|
1072
|
+
|
|
1073
|
+
`environment.ts` (local dev) is never touched. Only the URL origin is replaced, so
|
|
1074
|
+
custom paths (`/v2/graphql`) survive and a re-run with a new domain updates them.
|
|
1056
1075
|
|
|
1057
1076
|
**Usage:**
|
|
1058
1077
|
```bash
|
|
1059
|
-
lt deployment create [name] [options]
|
|
1078
|
+
lt deployment create [name] [domain] [options]
|
|
1060
1079
|
```
|
|
1061
1080
|
|
|
1062
1081
|
**Options:**
|
|
1063
1082
|
| Option | Description |
|
|
1064
1083
|
|--------|-------------|
|
|
1065
|
-
| `--domain <domain>` | Main domain for the project |
|
|
1066
|
-
| `--
|
|
1067
|
-
| `--
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
| `--noConfirm` | Skip confirmation prompts |
|
|
1084
|
+
| `--domain <domain>` | Main domain for the project (default: `<name>.lenne.tech`) |
|
|
1085
|
+
| `--project <slug>` | TurboOps project slug = registry namespace + login user (default: kebab-case name) |
|
|
1086
|
+
| `--noConfirm` | Skip prompts; resolve every value from flags, `lt.config`, or defaults |
|
|
1087
|
+
|
|
1088
|
+
**Configuration:** `commands.deployment.domain`, `commands.deployment.noConfirm`, `defaults.domain`, `defaults.noConfirm`
|
|
1071
1089
|
|
|
1072
|
-
**
|
|
1090
|
+
> **Upgrading from the Docker-Swarm generator:** the old
|
|
1091
|
+
> `commands.deployment.gitHub`, `gitLab`, `prodRunner`, and `testRunner` config
|
|
1092
|
+
> keys were removed with the switch to TurboOps. They are now flagged as unknown
|
|
1093
|
+
> by `lt config validate` — delete them from your `lt.config` if present.
|
|
1073
1094
|
|
|
1074
1095
|
---
|
|
1075
1096
|
|