@lenne.tech/cli 1.34.0 → 1.35.1
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/frontend/nuxt.js +6 -0
- 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-env.js +2 -2
- 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
|
@@ -10,7 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.help = void 0;
|
|
13
|
-
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
14
13
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
15
14
|
/**
|
|
16
15
|
* Add an API (`projects/api/`) to a fullstack workspace that currently
|
|
@@ -320,14 +319,12 @@ const NewCommand = {
|
|
|
320
319
|
if (apiResult.method !== 'link') {
|
|
321
320
|
(0, workspace_integration_1.writeApiConfig)({ apiDir: apiDest, apiMode, filesystem, frameworkMode });
|
|
322
321
|
}
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
// ignores
|
|
326
|
-
(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
subProjects: ['projects/api', 'projects/app'],
|
|
330
|
-
});
|
|
322
|
+
// Normalize the workspace root: hoist pnpm config out of the sub-projects
|
|
323
|
+
// (`setupServerForFullstack` may have left `pnpm.overrides` etc. at a
|
|
324
|
+
// non-root level pnpm ignores), drop nested lockfiles the root supersedes,
|
|
325
|
+
// and guarantee a root `.dockerignore` (Docker never reads a sub-project's
|
|
326
|
+
// own when building from the root context).
|
|
327
|
+
(0, workspace_integration_1.finalizeWorkspaceRoot)({ filesystem, projectDir: workspaceDir });
|
|
331
328
|
// Run install + format unless explicitly skipped (CI/agents may
|
|
332
329
|
// want to chain multiple add-* calls before installing once).
|
|
333
330
|
if (!skipInstall && !experimental) {
|
|
@@ -10,7 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.help = void 0;
|
|
13
|
-
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
14
13
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
15
14
|
/**
|
|
16
15
|
* Add a frontend app (`projects/app/`) to a fullstack workspace that
|
|
@@ -249,12 +248,11 @@ const NewCommand = {
|
|
|
249
248
|
warning('Continuing with npm mode for frontend.');
|
|
250
249
|
}
|
|
251
250
|
}
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
});
|
|
251
|
+
// Normalize the workspace root: hoist pnpm config (frontend templates may
|
|
252
|
+
// carry pnpm.overrides too), drop the frontend starter's standalone
|
|
253
|
+
// pnpm-lock.yaml the root supersedes, and guarantee a root `.dockerignore`
|
|
254
|
+
// (else the image inherits the host's node_modules, .output and .env).
|
|
255
|
+
(0, workspace_integration_1.finalizeWorkspaceRoot)({ filesystem, projectDir: workspaceDir });
|
|
258
256
|
if (!skipInstall) {
|
|
259
257
|
const installSpinner = spin('Install workspace packages');
|
|
260
258
|
try {
|
|
@@ -16,7 +16,6 @@ const gluegun_1 = require("gluegun");
|
|
|
16
16
|
const caddy_1 = require("../../lib/caddy");
|
|
17
17
|
const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
|
|
18
18
|
const dev_project_1 = require("../../lib/dev-project");
|
|
19
|
-
const hoist_workspace_pnpm_config_1 = require("../../lib/hoist-workspace-pnpm-config");
|
|
20
19
|
const package_name_1 = require("../../lib/package-name");
|
|
21
20
|
const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
|
|
22
21
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
@@ -621,14 +620,14 @@ const NewCommand = {
|
|
|
621
620
|
else {
|
|
622
621
|
serverSpinner.warn('Nest Server Starter not integrated');
|
|
623
622
|
}
|
|
624
|
-
//
|
|
625
|
-
// honors `pnpm.overrides
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
-
(0,
|
|
623
|
+
// Normalize the workspace root. Hoists workspace-scoped pnpm config out
|
|
624
|
+
// of the sub-projects (pnpm only honors `pnpm.overrides` etc. at the
|
|
625
|
+
// root — otherwise CVE overrides are silently disabled), drops the
|
|
626
|
+
// starters' standalone pnpm-lock.yaml files the root supersedes, and
|
|
627
|
+
// guarantees a root `.dockerignore` (else the images carry the host's
|
|
628
|
+
// node_modules, a stale .output, and a local .env that `nuxt build`
|
|
629
|
+
// bakes in).
|
|
630
|
+
(0, workspace_integration_1.finalizeWorkspaceRoot)({ filesystem, projectDir });
|
|
632
631
|
// Install all packages
|
|
633
632
|
if (!experimental) {
|
|
634
633
|
const installSpinner = spin('Install all packages');
|
package/build/commands/status.js
CHANGED
|
@@ -251,6 +251,8 @@ const StatusCommand = {
|
|
|
251
251
|
for (const [sub, host] of Object.entries(entry.subdomains)) {
|
|
252
252
|
info(` ${sub.padEnd(6)} https://${host}`);
|
|
253
253
|
}
|
|
254
|
+
// `loadRegistry` strips dbName from API-less entries, so its
|
|
255
|
+
// presence alone means there is a database to advertise.
|
|
254
256
|
if (entry.dbName)
|
|
255
257
|
info(` db mongodb://127.0.0.1/${entry.dbName}`);
|
|
256
258
|
const session = (0, dev_state_1.loadSession)(registryRoot);
|
|
@@ -61,6 +61,8 @@ const ListCommand = {
|
|
|
61
61
|
if (entry) {
|
|
62
62
|
for (const [sub, host] of Object.entries(entry.subdomains))
|
|
63
63
|
info(` ${sub.padEnd(4)} https://${host}`);
|
|
64
|
+
// `loadRegistry` strips dbName from API-less entries, so its presence
|
|
65
|
+
// alone means there is a database to show.
|
|
64
66
|
if (entry.dbName)
|
|
65
67
|
info(` db mongodb://127.0.0.1/${entry.dbName}`);
|
|
66
68
|
}
|
|
@@ -1333,25 +1333,74 @@ class Server {
|
|
|
1333
1333
|
// above works indirectly (via `bin/migrate.js` loading ts-node first),
|
|
1334
1334
|
// but it's fragile: if somebody invokes migrate.js standalone, `require
|
|
1335
1335
|
// ('../src/core')` would crash because Node cannot load TypeScript.
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1336
|
+
//
|
|
1337
|
+
// Two things this file must get right, both learned the hard way:
|
|
1338
|
+
// 1. ts-node is registered ONLY when the core next to it is still
|
|
1339
|
+
// TypeScript. In the production image everything is compiled and
|
|
1340
|
+
// ts-node is pruned, so an unconditional `require('./ts-compiler')`
|
|
1341
|
+
// would throw MODULE_NOT_FOUND and — under `set -e` in the
|
|
1342
|
+
// entrypoint — take the whole container down before the server starts.
|
|
1343
|
+
// 2. The Mongo URI comes from resolveMongoUri(), which falls back to
|
|
1344
|
+
// NSC__MONGOOSE__URI when config.env.ts is not loadable (Docker).
|
|
1345
|
+
// Reading `config.default.mongoose.uri` directly crashes there.
|
|
1339
1346
|
const migrateJsPath = `${dest}/migrations-utils/migrate.js`;
|
|
1340
|
-
filesystem.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
'
|
|
1353
|
-
|
|
1354
|
-
|
|
1347
|
+
const migrateStoreTemplate = filesystem.read(path.join(__dirname, '..', 'templates', 'vendor-scripts', 'migrate-store.js'));
|
|
1348
|
+
if (!migrateStoreTemplate) {
|
|
1349
|
+
throw new Error('CLI build is missing templates/vendor-scripts/migrate-store.js — cannot generate migrations-utils/migrate.js.');
|
|
1350
|
+
}
|
|
1351
|
+
filesystem.write(migrateJsPath, migrateStoreTemplate);
|
|
1352
|
+
// The generated migrate.js does `require('./mongo-uri')` (resolveMongoUri).
|
|
1353
|
+
// Unlike ts-compiler.js (written below), that file is NOT generated here — it
|
|
1354
|
+
// ships with nest-server-starter (the vendor clone base) and is copied into
|
|
1355
|
+
// dist/ by `copy:migrations`. Assert it so a non-standard/older starter that
|
|
1356
|
+
// lacks it fails loudly HERE instead of silently skipping every migration at
|
|
1357
|
+
// container start (the entrypoint's `migrate up` is best-effort under `set -e`).
|
|
1358
|
+
if (!filesystem.exists(`${dest}/migrations-utils/mongo-uri.js`)) {
|
|
1359
|
+
this.toolbox.print.warning(' ⚠ migrations-utils/mongo-uri.js is missing — the generated migrate.js requires it ' +
|
|
1360
|
+
'(resolveMongoUri). Migrations will not run. It normally ships with nest-server-starter; ' +
|
|
1361
|
+
'add it (must export resolveMongoUri) so `migrate up` can resolve the Mongo URI.');
|
|
1362
|
+
}
|
|
1363
|
+
// Compile migrations to JavaScript. The production image prunes ts-node, so
|
|
1364
|
+
// `migrate up` can only load plain .js migrations there. Without this the
|
|
1365
|
+
// entrypoint's migration step never applies anything.
|
|
1366
|
+
const tsconfigBuildPath = `${dest}/tsconfig.build.json`;
|
|
1367
|
+
if (filesystem.exists(tsconfigBuildPath)) {
|
|
1368
|
+
const tsconfigBuildRaw = filesystem.read(tsconfigBuildPath) || '';
|
|
1369
|
+
if (!tsconfigBuildRaw.includes('migrations/**/*.ts')) {
|
|
1370
|
+
// 1. Extend `include` so tsc also emits the .ts migrations + helpers.
|
|
1371
|
+
const includePatched = tsconfigBuildRaw.replace(/"include":\s*\[\s*"src\/\*\*\/\*"\s*\]/, '"include": ["src/**/*", "migrations/**/*.ts", "migrations-utils/**/*.ts"]');
|
|
1372
|
+
if (includePatched === tsconfigBuildRaw) {
|
|
1373
|
+
this.toolbox.print.warning(' ⚠ tsconfig.build.json: could not extend "include" automatically — ' +
|
|
1374
|
+
'add "migrations/**/*.ts" and "migrations-utils/**/*.ts" manually, ' +
|
|
1375
|
+
'otherwise migrations are never compiled and the container skips them.');
|
|
1376
|
+
}
|
|
1377
|
+
else {
|
|
1378
|
+
// 2. Pin rootDir. Once `include` reaches beyond src/, tsc re-infers the
|
|
1379
|
+
// rootDir from the common parent of ALL included files, which can
|
|
1380
|
+
// shift the emit layout and relocate the entry point. An explicit
|
|
1381
|
+
// "rootDir": "./" keeps the output mirroring the project tree:
|
|
1382
|
+
// `dist/src/main.js` — which is exactly what the Docker entrypoint
|
|
1383
|
+
// runs (see nest-server-starter/docker-entrypoint.sh) — plus
|
|
1384
|
+
// `dist/migrations/` and `dist/migrations-utils/`. Without the pin the
|
|
1385
|
+
// entry can move and the container's `node dist/src/main.js` dies with
|
|
1386
|
+
// MODULE_NOT_FOUND. Newer starters ship the pin; older ones (pre-9.0) do not.
|
|
1387
|
+
let patched = includePatched;
|
|
1388
|
+
let rootDirPinned = /"rootDir"\s*:/.test(patched);
|
|
1389
|
+
if (!rootDirPinned) {
|
|
1390
|
+
const withRootDir = patched.replace(/("compilerOptions"\s*:\s*\{)/, '$1\n "rootDir": "./",');
|
|
1391
|
+
if (withRootDir !== patched) {
|
|
1392
|
+
patched = withRootDir;
|
|
1393
|
+
rootDirPinned = true;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
filesystem.write(tsconfigBuildPath, patched);
|
|
1397
|
+
if (!rootDirPinned) {
|
|
1398
|
+
this.toolbox.print.warning(' ⚠ tsconfig.build.json: extended "include" but could not confirm "rootDir": "./" — ' +
|
|
1399
|
+
'add it manually, otherwise tsc may relocate the emit and `node dist/src/main.js` fails.');
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1355
1404
|
// ── 4b. (removed) ─────────────────────────────────────────────────────
|
|
1356
1405
|
//
|
|
1357
1406
|
// Previous versions copied three maintenance scripts into
|
|
@@ -1471,6 +1520,79 @@ class Server {
|
|
|
1471
1520
|
scripts['migrate:test:up'] = `NODE_ENV=test node ./bin/migrate.js up ${migrateArgs}`;
|
|
1472
1521
|
scripts['migrate:preview:up'] = `NODE_ENV=preview node ./bin/migrate.js up ${migrateArgs}`;
|
|
1473
1522
|
scripts['migrate:prod:up'] = `NODE_ENV=production node ./bin/migrate.js up ${migrateArgs}`;
|
|
1523
|
+
// Make the production build carry a runnable migration setup.
|
|
1524
|
+
//
|
|
1525
|
+
// The image has no ts-node, and the docker entrypoint looks for
|
|
1526
|
+
// `dist/bin/migrate.js`. Without these three scripts the entrypoint
|
|
1527
|
+
// silently skips migrations forever — the failure mode is invisible
|
|
1528
|
+
// until the first real data migration quietly does not run.
|
|
1529
|
+
//
|
|
1530
|
+
// copy:bin ships bin/migrate.js into dist/
|
|
1531
|
+
// copy:migrations ships only the .js helpers (the .ts migrations
|
|
1532
|
+
// are compiled by tsc, see tsconfig.build.json)
|
|
1533
|
+
// prune:migrations drops the *.d.ts / *.js.map that tsc emits next
|
|
1534
|
+
// to each compiled migration — the runner would
|
|
1535
|
+
// otherwise pick the declaration file up as a
|
|
1536
|
+
// second migration and throw on `export declare`
|
|
1537
|
+
//
|
|
1538
|
+
// Define each only if the project doesn't already have it, so a
|
|
1539
|
+
// deliberate customization survives a re-run (older starters simply
|
|
1540
|
+
// lack them → still repaired).
|
|
1541
|
+
if (typeof scripts['copy:bin'] !== 'string') {
|
|
1542
|
+
scripts['copy:bin'] = 'cpy ./bin ./dist/ || true';
|
|
1543
|
+
}
|
|
1544
|
+
if (typeof scripts['copy:migrations'] !== 'string') {
|
|
1545
|
+
scripts['copy:migrations'] =
|
|
1546
|
+
'cpy "./migrations-utils/*.js" ./dist/migrations-utils/ && cpy ./tsconfig.json ./dist/';
|
|
1547
|
+
}
|
|
1548
|
+
if (typeof scripts['prune:migrations'] !== 'string') {
|
|
1549
|
+
scripts['prune:migrations'] = 'rimraf --glob "./dist/migrations/*.d.ts" "./dist/migrations/*.js.map"';
|
|
1550
|
+
}
|
|
1551
|
+
// Wire the three steps into the build. Order among them is irrelevant:
|
|
1552
|
+
// all three only need to run AFTER tsc, which `build` does first
|
|
1553
|
+
// (copy:bin/copy:migrations copy source files; prune only cleans
|
|
1554
|
+
// tsc's dist/migrations output). A step is already covered when it is
|
|
1555
|
+
// named directly in `build` or reachable through a `copy` aggregator
|
|
1556
|
+
// that `build` runs (newer starters). Older starters have no such
|
|
1557
|
+
// aggregator, so append to `build` — the one script guaranteed to
|
|
1558
|
+
// exist and to run tsc first; without this the sub-scripts are defined
|
|
1559
|
+
// but never invoked and the image ships without dist/bin/migrate.js.
|
|
1560
|
+
//
|
|
1561
|
+
// Use the package-manager `run` the project's own scripts already use
|
|
1562
|
+
// (older starters: npm, newer: pnpm); a hard-coded `pnpm run` would
|
|
1563
|
+
// make `build` die with "pnpm: command not found" in an npm project.
|
|
1564
|
+
const scriptText = Object.values(scripts)
|
|
1565
|
+
.filter((v) => typeof v === 'string')
|
|
1566
|
+
.join('\n');
|
|
1567
|
+
const pmRun = /\bpnpm\s+run\b/.test(scriptText)
|
|
1568
|
+
? 'pnpm run'
|
|
1569
|
+
: /\byarn\s+run\b/.test(scriptText)
|
|
1570
|
+
? 'yarn run'
|
|
1571
|
+
: /\bnpm\s+run\b/.test(scriptText)
|
|
1572
|
+
? 'npm run'
|
|
1573
|
+
: 'pnpm run';
|
|
1574
|
+
if (typeof scripts.build === 'string') {
|
|
1575
|
+
const copyAggregator = typeof scripts.copy === 'string' ? scripts.copy : '';
|
|
1576
|
+
const covered = (step) => scripts.build.includes(step) || copyAggregator.includes(step);
|
|
1577
|
+
const missingSteps = ['copy:bin', 'copy:migrations', 'prune:migrations'].filter((s) => !covered(s));
|
|
1578
|
+
if (missingSteps.length > 0) {
|
|
1579
|
+
scripts.build = `${scripts.build}${missingSteps.map((s) => ` && ${pmRun} ${s}`).join('')}`;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
this.toolbox.print.warning(' ⚠ package.json has no "build" script — copy:bin/copy:migrations/prune:migrations were defined but ' +
|
|
1584
|
+
'not wired in; the production image may ship without dist/bin/migrate.js and skip migrations.');
|
|
1585
|
+
}
|
|
1586
|
+
// prune:migrations needs `rimraf`, copy:* need `cpy` (cpy-cli). Older
|
|
1587
|
+
// starters ship cpy-cli but not rimraf — add whatever is missing so
|
|
1588
|
+
// `build` doesn't die on an absent binary.
|
|
1589
|
+
const devDependencies = (pkg.devDependencies && typeof pkg.devDependencies === 'object'
|
|
1590
|
+
? pkg.devDependencies
|
|
1591
|
+
: (pkg.devDependencies = {}));
|
|
1592
|
+
if (typeof devDependencies.rimraf !== 'string')
|
|
1593
|
+
devDependencies.rimraf = '^6.0.1';
|
|
1594
|
+
if (typeof devDependencies['cpy-cli'] !== 'string')
|
|
1595
|
+
devDependencies['cpy-cli'] = '^6.0.0';
|
|
1474
1596
|
// Vendor freshness check: reads VENDOR.md baseline version and
|
|
1475
1597
|
// compares against npm registry. Non-blocking (always exits 0).
|
|
1476
1598
|
// Uses a short inline script — no external file needed.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findAngularEnvironmentsDir = findAngularEnvironmentsDir;
|
|
4
|
+
exports.patchAngularEnvironments = patchAngularEnvironments;
|
|
5
|
+
exports.stageHosts = stageHosts;
|
|
6
|
+
/**
|
|
7
|
+
* Rewrite the hardcoded localhost URLs in an Angular app's environment files
|
|
8
|
+
* to the deployed TurboOps stage URLs.
|
|
9
|
+
*
|
|
10
|
+
* WHY: a Nuxt app reads its API URL from the stage env at runtime
|
|
11
|
+
* (`NUXT_PUBLIC_API_URL`, injected by TurboOps). Angular has no such runtime
|
|
12
|
+
* config — `ng build --configuration production` BAKES `environment.prod.ts`
|
|
13
|
+
* into the bundle. Without this patch a deployed Angular app ships
|
|
14
|
+
* `apiUrl: 'http://127.0.0.1:3000/graphql'` and calls the end user's own
|
|
15
|
+
* machine.
|
|
16
|
+
*
|
|
17
|
+
* Stage mapping (TurboOps knows exactly two stages, `dev` and `production`):
|
|
18
|
+
*
|
|
19
|
+
* environment.prod.ts → production → <domain> + api.<domain>
|
|
20
|
+
* environment.develop.ts → dev → dev.<domain> + api.dev.<domain>
|
|
21
|
+
* environment.test.ts → dev → dev.<domain> + api.dev.<domain>
|
|
22
|
+
*
|
|
23
|
+
* `environment.ts` is the local-dev config and is deliberately left alone.
|
|
24
|
+
* `environment.test.ts` and `environment.develop.ts` are identical in
|
|
25
|
+
* ng-base-starter (both `prefix: 'app-test'`); with no TurboOps "test" stage,
|
|
26
|
+
* the non-production stage is `dev` for both.
|
|
27
|
+
*
|
|
28
|
+
* Only the URL ORIGIN (scheme + host + optional port) is replaced — the path
|
|
29
|
+
* (`/graphql`, `/api`) is preserved, so a project that customised it keeps its
|
|
30
|
+
* routes. That also makes the patch idempotent AND re-runnable with a new
|
|
31
|
+
* domain: an already-deployed URL is rewritten to the new one instead of being
|
|
32
|
+
* skipped (the old `http://127.0.0.1:3000`-only replace silently did nothing
|
|
33
|
+
* on the second run).
|
|
34
|
+
*/
|
|
35
|
+
const fs_1 = require("fs");
|
|
36
|
+
const path_1 = require("path");
|
|
37
|
+
/** Which environment file belongs to which TurboOps stage. */
|
|
38
|
+
const ENVIRONMENT_STAGES = {
|
|
39
|
+
'environment.develop.ts': 'dev',
|
|
40
|
+
'environment.prod.ts': 'production',
|
|
41
|
+
'environment.test.ts': 'dev',
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Locate the Angular `environments/` directory for a project root.
|
|
45
|
+
* Returns null for non-Angular apps (e.g. Nuxt), which makes the whole
|
|
46
|
+
* patch a no-op there.
|
|
47
|
+
*/
|
|
48
|
+
function findAngularEnvironmentsDir(projectRoot) {
|
|
49
|
+
const candidates = [(0, path_1.join)('projects', 'app', 'src', 'environments'), (0, path_1.join)('src', 'environments')];
|
|
50
|
+
for (const rel of candidates) {
|
|
51
|
+
const dir = (0, path_1.join)(projectRoot, rel);
|
|
52
|
+
if ((0, fs_1.existsSync)(dir))
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Point every stage-specific Angular environment file at the deployed URLs.
|
|
59
|
+
* Returns one {@link PatchResult} per file that exists (empty array when the
|
|
60
|
+
* project is not an Angular app).
|
|
61
|
+
*/
|
|
62
|
+
function patchAngularEnvironments(options) {
|
|
63
|
+
const dir = findAngularEnvironmentsDir(options.projectRoot);
|
|
64
|
+
if (!dir)
|
|
65
|
+
return [];
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const [fileName, stage] of Object.entries(ENVIRONMENT_STAGES)) {
|
|
68
|
+
const file = (0, path_1.join)(dir, fileName);
|
|
69
|
+
if (!(0, fs_1.existsSync)(file))
|
|
70
|
+
continue;
|
|
71
|
+
results.push(patchEnvironmentFile(file, options.domain, stage));
|
|
72
|
+
}
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Public hostnames of a stage. The API always lives one label in front of the
|
|
77
|
+
* app host, so `dev` nests as `api.dev.<domain>`.
|
|
78
|
+
*/
|
|
79
|
+
function stageHosts(domain, stage) {
|
|
80
|
+
const app = stage === 'production' ? domain : `dev.${domain}`;
|
|
81
|
+
return { api: `api.${app}`, app };
|
|
82
|
+
}
|
|
83
|
+
/** Rewrite the four URL properties of a single environment file. */
|
|
84
|
+
function patchEnvironmentFile(file, domain, stage) {
|
|
85
|
+
const hosts = stageHosts(domain, stage);
|
|
86
|
+
const original = (0, fs_1.readFileSync)(file, 'utf8');
|
|
87
|
+
let content = original;
|
|
88
|
+
let replacements = 0;
|
|
89
|
+
let recognized = 0;
|
|
90
|
+
const rewriteOrigin = (property, origin) => {
|
|
91
|
+
// (property: )(quote)scheme://host[:port](path)(same quote)
|
|
92
|
+
const pattern = new RegExp(`(${property}:\\s*)(['"])[a-z]+://[^'"/]+([^'"]*)\\2`, 'g');
|
|
93
|
+
content = content.replace(pattern, (match, prefix, quote, path) => {
|
|
94
|
+
recognized++;
|
|
95
|
+
const next = `${prefix}${quote}${origin}${path}${quote}`;
|
|
96
|
+
if (next !== match)
|
|
97
|
+
replacements++;
|
|
98
|
+
return next;
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
rewriteOrigin('apiUrl', `https://${hosts.api}`);
|
|
102
|
+
rewriteOrigin('restUrl', `https://${hosts.api}`);
|
|
103
|
+
rewriteOrigin('wsUrl', `wss://${hosts.api}`);
|
|
104
|
+
rewriteOrigin('appUrl', `https://${hosts.app}`);
|
|
105
|
+
if (replacements > 0)
|
|
106
|
+
(0, fs_1.writeFileSync)(file, content, 'utf8');
|
|
107
|
+
return { file, patched: replacements > 0, recognized: recognized > 0, replacements };
|
|
108
|
+
}
|
package/build/lib/dev-env.js
CHANGED
|
@@ -52,14 +52,14 @@ function buildDevEnv(input) {
|
|
|
52
52
|
internalPort: apiInternalPort,
|
|
53
53
|
},
|
|
54
54
|
app: {
|
|
55
|
-
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
55
|
+
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
56
56
|
// See API note above: pin the dev server to IPv4 so Caddy's
|
|
57
57
|
// `127.0.0.1` upstream is unambiguous.
|
|
58
58
|
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
|
|
59
59
|
// Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
|
|
60
60
|
// both subdomains under HTTPS with shared cookie domain, so
|
|
61
61
|
// same-origin trickery is no longer required.
|
|
62
|
-
NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug, PORT: String(appInternalPort) }),
|
|
62
|
+
NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug, PORT: String(appInternalPort) }), (process.platform === 'darwin' ? { TMPDIR: '/tmp' } : {})),
|
|
63
63
|
internalPort: appInternalPort,
|
|
64
64
|
},
|
|
65
65
|
};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildIdentity = buildIdentity;
|
|
4
4
|
exports.buildTestIdentity = buildTestIdentity;
|
|
5
5
|
exports.buildTicketIdentity = buildTicketIdentity;
|
|
6
|
+
exports.detectStandaloneKind = detectStandaloneKind;
|
|
6
7
|
exports.isUnmodifiedTemplateName = isUnmodifiedTemplateName;
|
|
7
8
|
exports.projectSlug = projectSlug;
|
|
8
9
|
exports.slugify = slugify;
|
|
@@ -31,43 +32,46 @@ const path_1 = require("path");
|
|
|
31
32
|
* - `projects/app` → `<slug>.localhost` (primary)
|
|
32
33
|
* - `projects/<other>` → `<other>.<slug>.localhost`
|
|
33
34
|
*
|
|
34
|
-
* For standalone projects (single repo, no `projects
|
|
35
|
+
* For standalone projects (single repo, no `projects/api` or `projects/app`):
|
|
35
36
|
* - API project (config.env.ts present) → `api.<slug>.localhost`
|
|
36
37
|
* - App project (nuxt.config.ts present) → `<slug>.localhost`
|
|
38
|
+
*
|
|
39
|
+
* Every combination is valid: api+app, api-only, and app-only.
|
|
37
40
|
*/
|
|
38
41
|
function buildIdentity(root) {
|
|
39
42
|
const slug = projectSlug(root);
|
|
40
43
|
const subdomains = {};
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const apiDir = (0, path_1.join)(root, 'projects', 'api');
|
|
45
|
+
const appDir = (0, path_1.join)(root, 'projects', 'app');
|
|
46
|
+
const hasApi = (0, fs_1.existsSync)(apiDir);
|
|
47
|
+
const hasApp = (0, fs_1.existsSync)(appDir);
|
|
48
|
+
// Monorepo only when a known subproject is actually present. A bare (or
|
|
49
|
+
// unrelated) `projects/` directory must not shadow the standalone probe —
|
|
50
|
+
// it would yield an identity with zero subdomains and no routable URLs.
|
|
51
|
+
if (hasApi || hasApp) {
|
|
52
|
+
if (hasApi) {
|
|
47
53
|
subdomains.api = {
|
|
48
54
|
hostname: `api.${slug}.localhost`,
|
|
49
55
|
isPrimaryApp: false,
|
|
50
56
|
subdir: 'projects/api',
|
|
51
57
|
};
|
|
52
58
|
}
|
|
53
|
-
if (
|
|
59
|
+
if (hasApp) {
|
|
54
60
|
subdomains.app = {
|
|
55
61
|
hostname: `${slug}.localhost`,
|
|
56
62
|
isPrimaryApp: true,
|
|
57
63
|
subdir: 'projects/app',
|
|
58
64
|
};
|
|
59
65
|
}
|
|
66
|
+
return { root, slug, subdomains };
|
|
60
67
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
if (isApp) {
|
|
69
|
-
subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
|
|
70
|
-
}
|
|
68
|
+
// Standalone — derive from project shape.
|
|
69
|
+
const { isApi, isApp } = detectStandaloneKind(root);
|
|
70
|
+
if (isApi) {
|
|
71
|
+
subdomains.api = { hostname: `api.${slug}.localhost`, isPrimaryApp: false, subdir: null };
|
|
72
|
+
}
|
|
73
|
+
if (isApp) {
|
|
74
|
+
subdomains.app = { hostname: `${slug}.localhost`, isPrimaryApp: true, subdir: null };
|
|
71
75
|
}
|
|
72
76
|
return { root, slug, subdomains };
|
|
73
77
|
}
|
|
@@ -105,6 +109,22 @@ function buildTestIdentity(base, suffix = '-test') {
|
|
|
105
109
|
function buildTicketIdentity(base, id) {
|
|
106
110
|
return buildTestIdentity(base, `-${id}`);
|
|
107
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Probe a single directory for the shape of a STANDALONE lt project.
|
|
114
|
+
*
|
|
115
|
+
* Single source of truth for "is this an API / an App?", shared by
|
|
116
|
+
* {@link buildIdentity} and `dev-project.ts#resolveLayout` so the identity
|
|
117
|
+
* (URLs) and the layout (which processes to spawn) can never disagree.
|
|
118
|
+
*
|
|
119
|
+
* Both flags can be true (a single repo holding both), and both can be false
|
|
120
|
+
* (not an lt-dev project).
|
|
121
|
+
*/
|
|
122
|
+
function detectStandaloneKind(dir) {
|
|
123
|
+
return {
|
|
124
|
+
isApi: (0, fs_1.existsSync)((0, path_1.join)(dir, 'src', 'config.env.ts')) || (0, fs_1.existsSync)((0, path_1.join)(dir, 'nest-cli.json')),
|
|
125
|
+
isApp: (0, fs_1.existsSync)((0, path_1.join)(dir, 'nuxt.config.ts')),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
108
128
|
/**
|
|
109
129
|
* package.json `name` values that are unchanged starter-template defaults.
|
|
110
130
|
*
|
package/build/lib/dev-project.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.deriveTicketDbName = deriveTicketDbName;
|
|
|
8
8
|
exports.resolveLayout = resolveLayout;
|
|
9
9
|
const fs_1 = require("fs");
|
|
10
10
|
const path_1 = require("path");
|
|
11
|
+
const dev_identity_1 = require("./dev-identity");
|
|
11
12
|
const workspace_integration_1 = require("./workspace-integration");
|
|
12
13
|
/**
|
|
13
14
|
* Detect whether the API project still has the legacy hardcoded `port: 3000`.
|
|
@@ -85,20 +86,32 @@ function deriveTicketDbName(devDbName, ticketId) {
|
|
|
85
86
|
/**
|
|
86
87
|
* Resolve layout starting from `cwd`. Walks up to find a workspace if
|
|
87
88
|
* cwd is inside `projects/api/` or `projects/app/`.
|
|
89
|
+
*
|
|
90
|
+
* A workspace marker alone never wins: it only selects the monorepo layout
|
|
91
|
+
* when `projects/api` or `projects/app` actually exists. Otherwise we fall
|
|
92
|
+
* through to the standalone probe — a single-package repo may legitimately
|
|
93
|
+
* carry a settings-only `pnpm-workspace.yaml` (pnpm 10/11 keeps `overrides`
|
|
94
|
+
* and build allowlists there), and an npm workspace may use `packages/*`
|
|
95
|
+
* rather than the lt `projects/*` convention.
|
|
88
96
|
*/
|
|
89
97
|
function resolveLayout(cwd, filesystem) {
|
|
90
98
|
const subContext = (0, workspace_integration_1.detectSubProjectContext)(cwd, filesystem);
|
|
91
99
|
if (subContext)
|
|
92
100
|
return monorepoLayout(subContext.workspaceRoot);
|
|
93
101
|
const layout = (0, workspace_integration_1.detectWorkspaceLayout)(cwd, filesystem);
|
|
94
|
-
if (layout.hasWorkspace)
|
|
95
|
-
|
|
102
|
+
if (layout.hasWorkspace) {
|
|
103
|
+
const mono = monorepoLayout(layout.workspaceDir);
|
|
104
|
+
if (mono.apiDir || mono.appDir)
|
|
105
|
+
return mono;
|
|
106
|
+
}
|
|
96
107
|
const workspaceRoot = (0, workspace_integration_1.findWorkspaceRoot)(cwd, filesystem);
|
|
97
|
-
if (workspaceRoot)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
if (workspaceRoot) {
|
|
109
|
+
const mono = monorepoLayout(workspaceRoot);
|
|
110
|
+
if (mono.apiDir || mono.appDir)
|
|
111
|
+
return mono;
|
|
112
|
+
}
|
|
113
|
+
// Standalone project — API-only, App-only, or a single repo holding both.
|
|
114
|
+
const { isApi, isApp } = (0, dev_identity_1.detectStandaloneKind)(cwd);
|
|
102
115
|
return {
|
|
103
116
|
apiDir: isApi ? cwd : null,
|
|
104
117
|
appDir: isApp ? cwd : null,
|
package/build/lib/dev-state.js
CHANGED
|
@@ -118,7 +118,7 @@ function loadRegistry() {
|
|
|
118
118
|
try {
|
|
119
119
|
const parsed = JSON.parse((0, fs_1.readFileSync)(REGISTRY_PATH, 'utf8'));
|
|
120
120
|
if (parsed && typeof parsed === 'object' && parsed.version === 1 && typeof parsed.projects === 'object') {
|
|
121
|
-
return parsed;
|
|
121
|
+
return normalizeRegistry(parsed);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
catch (_a) {
|
|
@@ -184,6 +184,22 @@ function takenInternalPorts(reg, excludeSlug) {
|
|
|
184
184
|
}
|
|
185
185
|
return ports;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Drop a `dbName` from any entry that has no `api` subdomain. An App-only
|
|
189
|
+
* project has no database, yet older registries (written before `lt dev up`
|
|
190
|
+
* stopped persisting one for App-only stacks) still carry a derived name.
|
|
191
|
+
* Normalizing once on load lets every reader test `entry.dbName` alone,
|
|
192
|
+
* instead of repeating `entry.dbName && entry.subdomains.api` at each display.
|
|
193
|
+
*/
|
|
194
|
+
function normalizeRegistry(reg) {
|
|
195
|
+
var _a;
|
|
196
|
+
for (const entry of Object.values(reg.projects)) {
|
|
197
|
+
if ((entry === null || entry === void 0 ? void 0 : entry.dbName) && !((_a = entry.subdomains) === null || _a === void 0 ? void 0 : _a.api)) {
|
|
198
|
+
delete entry.dbName;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return reg;
|
|
202
|
+
}
|
|
187
203
|
/** True if two paths resolve to the same location (normalising symlinks, e.g. /var → /private/var). */
|
|
188
204
|
function sameRealPath(a, b) {
|
|
189
205
|
try {
|