@lenne.tech/cli 1.48.0 → 1.50.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/build/commands/dev/doctor.js +27 -10
- package/build/commands/dev/down.js +48 -5
- package/build/commands/dev/status.js +32 -8
- package/build/commands/dev/test.js +5 -3
- package/build/commands/dev/up.js +59 -21
- package/build/commands/fullstack/init.js +12 -3
- package/build/commands/git/squash.js +4 -1
- package/build/extensions/api-mode.js +69 -0
- package/build/extensions/frontend-helper.js +11 -12
- package/build/extensions/server.js +43 -33
- package/build/lib/caddy.js +17 -10
- package/build/lib/check-template-sync.js +62 -2
- package/build/lib/config-env-graphql.js +106 -0
- package/build/lib/cross-env.js +94 -0
- package/build/lib/dev-api-launch.js +34 -8
- package/build/lib/dev-env-bridge.js +7 -0
- package/build/lib/dev-env.js +44 -4
- package/build/lib/dev-log-tail.js +91 -0
- package/build/lib/dev-package-manager.js +13 -9
- package/build/lib/dev-patches.js +15 -5
- package/build/lib/dev-process.js +362 -127
- package/build/lib/dev-project.js +14 -1
- package/build/lib/dev-prune.js +15 -5
- package/build/lib/dev-service.js +29 -15
- package/build/lib/dev-state.js +40 -4
- package/build/lib/dev-test-session.js +141 -73
- package/build/lib/dev-ticket.js +13 -1
- package/build/lib/fail-run.js +11 -0
- package/build/lib/heal-vendor-migrate-store.js +4 -3
- package/build/lib/hoist-workspace-pnpm-config.js +17 -6
- package/build/lib/migrate-scripts.js +69 -0
- package/build/lib/module-specifiers.js +144 -0
- package/build/lib/platform.js +53 -1
- package/build/lib/spinner-lifetime.js +245 -0
- package/build/templates/check/check.mjs +59 -9
- package/build/templates/check/lt-monorepo-pin.json +4 -4
- package/build/templates/nest-server-starter/README.md.ejs +13 -6
- package/docs/commands.md +16 -12
- package/package.json +4 -1
- package/build/lib/strip-comments.js +0 -78
package/build/lib/dev-env.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildDevEnv = buildDevEnv;
|
|
4
|
+
exports.internalUrl = internalUrl;
|
|
4
5
|
/**
|
|
5
6
|
* Build environment variables for `lt dev up`.
|
|
6
7
|
*
|
|
@@ -41,13 +42,16 @@ function buildDevEnv(input) {
|
|
|
41
42
|
const appSub = identity.subdomains.app;
|
|
42
43
|
const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
|
|
43
44
|
const appUrl = appSub ? `https://${appSub.hostname}` : '';
|
|
45
|
+
// Same two services, addressed the way Node can reach them. See `internalUrl`.
|
|
46
|
+
const apiInternal = internalUrl(apiInternalPort);
|
|
47
|
+
const appInternal = internalUrl(appInternalPort);
|
|
44
48
|
const caPath = (0, dev_env_bridge_1.detectCaddyRootCa)();
|
|
45
|
-
const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign({
|
|
49
|
+
const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({
|
|
46
50
|
// Marks the API + App processes as running under `lt dev`. Consumed by the
|
|
47
51
|
// backend to relax dev-only behaviour (rate limiting, Better-Auth
|
|
48
52
|
// user-cache) so E2E suites run without a separate VITEST/PLAYWRIGHT flag.
|
|
49
53
|
// (Also written to the .lt-dev/.env bridge for external test runners.)
|
|
50
|
-
LT_DEV_ACTIVE: 'true' }, (apiUrl ? { BASE_URL: apiUrl, NSC__BASE_URL: apiUrl } : {})), (appUrl ? { APP_URL: appUrl, NSC__APP_URL: appUrl } : {})), (dbName ? { DATABASE_URL: buildPostgresUrl(dbName), NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${dbName}` } : {})), (caPath ? { NODE_EXTRA_CA_CERTS: caPath } : {}));
|
|
54
|
+
LT_DEV_ACTIVE: 'true' }, (apiInternal ? { LT_DEV_API_INTERNAL_URL: apiInternal } : {})), (appInternal ? { LT_DEV_APP_INTERNAL_URL: appInternal } : {})), (apiUrl ? { BASE_URL: apiUrl, NSC__BASE_URL: apiUrl } : {})), (appUrl ? { APP_URL: appUrl, NSC__APP_URL: appUrl } : {})), (dbName ? { DATABASE_URL: buildPostgresUrl(dbName), NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${dbName}` } : {})), (caPath ? { NODE_EXTRA_CA_CERTS: caPath } : {}));
|
|
51
55
|
return {
|
|
52
56
|
api: {
|
|
53
57
|
env: Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
@@ -59,10 +63,10 @@ function buildDevEnv(input) {
|
|
|
59
63
|
internalPort: apiInternalPort,
|
|
60
64
|
},
|
|
61
65
|
app: {
|
|
62
|
-
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
66
|
+
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
63
67
|
// See API note above: pin the dev server to IPv4 so Caddy's
|
|
64
68
|
// `127.0.0.1` upstream is unambiguous.
|
|
65
|
-
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl,
|
|
69
|
+
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (apiInternal || apiUrl ? { NUXT_API_URL: apiInternal || apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
|
|
66
70
|
// Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
|
|
67
71
|
// both subdomains under HTTPS with shared cookie domain, so
|
|
68
72
|
// same-origin trickery is no longer required.
|
|
@@ -75,6 +79,42 @@ function buildDevEnv(input) {
|
|
|
75
79
|
},
|
|
76
80
|
};
|
|
77
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The loopback address of a component, for anything that RESOLVES a name.
|
|
84
|
+
*
|
|
85
|
+
* `lt dev` is URL-first: a project is reached at `https://<slug>.localhost`, and
|
|
86
|
+
* Caddy proxies that to an opaque internal port. That promise holds for
|
|
87
|
+
* **browsers**. It does not hold for Node: on Windows `*.localhost` subdomains do
|
|
88
|
+
* not resolve at all (`dns.lookup('api.demo.localhost')` → ENOTFOUND, and `curl`
|
|
89
|
+
* agrees), while Chromium resolves them internally without asking a resolver.
|
|
90
|
+
*
|
|
91
|
+
* So the question at every call site is not "which URL" but **"who reads it"**:
|
|
92
|
+
*
|
|
93
|
+
* | | example | address to use |
|
|
94
|
+
* |---|---|---|
|
|
95
|
+
* | a browser resolves it | Playwright `baseURL`, a printed link | the public name |
|
|
96
|
+
* | Node resolves it | readiness probes, SSR fetches, a proxy target | **this** |
|
|
97
|
+
* | another binary resolves it | `cloudflared` | **this** |
|
|
98
|
+
* | nobody resolves it — it is COMPARED | `APP_URL` (CORS / `trustedOrigins`), the Caddy vhost matcher, the tunnel `Host:` header | the public name, untouched |
|
|
99
|
+
*
|
|
100
|
+
* That last row is why "rewrite every internal URL to 127.0.0.1" would be wrong:
|
|
101
|
+
* `APP_URL` is matched as a string against the `Origin` header a browser sends,
|
|
102
|
+
* and the browser arrives from `https://<slug>.localhost`. Rewriting it would
|
|
103
|
+
* break every login — on the platform where only a health probe was broken before.
|
|
104
|
+
*
|
|
105
|
+
* **Loopback on every platform, not only on Windows.** 127.0.0.1 works everywhere,
|
|
106
|
+
* so there is no branch to get wrong, and the path Windows depends on is the one
|
|
107
|
+
* macOS exercises daily. A branch only the other platform runs is an unchecked
|
|
108
|
+
* branch.
|
|
109
|
+
*
|
|
110
|
+
* One consequence to keep in mind: this goes PAST Caddy. For "is the component
|
|
111
|
+
* alive?" that is an advantage — it measures the component, not the proxy. For
|
|
112
|
+
* "does the routing work?" it is the wrong question, and that one belongs to
|
|
113
|
+
* `lt dev doctor` over the public name.
|
|
114
|
+
*/
|
|
115
|
+
function internalUrl(port) {
|
|
116
|
+
return port ? `http://127.0.0.1:${port}` : '';
|
|
117
|
+
}
|
|
78
118
|
/** Postgres convenience URL — used by Postgres-based projects (e.g. nest-base). */
|
|
79
119
|
function buildPostgresUrl(dbName) {
|
|
80
120
|
return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.describeLog = describeLog;
|
|
13
|
+
exports.diagnoseLog = diagnoseLog;
|
|
14
|
+
exports.findEarlyExits = findEarlyExits;
|
|
15
|
+
exports.isSilentLog = isSilentLog;
|
|
16
|
+
exports.tailLines = tailLines;
|
|
17
|
+
/**
|
|
18
|
+
* What a component's log can tell someone whose component just died.
|
|
19
|
+
*
|
|
20
|
+
* Two silent failures made this necessary, both seen on a Windows laptop:
|
|
21
|
+
* `lt dev up` reported success for an API that was dead seconds later, and its
|
|
22
|
+
* log file existed, was even rotated, but stayed at 0 bytes, so nobody looked in
|
|
23
|
+
* the extra console window that held the actual error. A diagnosis therefore has
|
|
24
|
+
* to tell "the log says X" apart from "the log is empty", and say the latter out
|
|
25
|
+
* loud instead of printing nothing.
|
|
26
|
+
*/
|
|
27
|
+
const fs_1 = require("fs");
|
|
28
|
+
/**
|
|
29
|
+
* The lines to print under "<name> is not running". Never an empty list: an
|
|
30
|
+
* empty or missing log is itself the finding.
|
|
31
|
+
*/
|
|
32
|
+
function describeLog(d) {
|
|
33
|
+
if (d.state === 'missing')
|
|
34
|
+
return [`No log at ${d.file}.`];
|
|
35
|
+
if (d.state === 'empty') {
|
|
36
|
+
return [`${d.file} is EMPTY — the output was not captured, so the reason is not in the log.`];
|
|
37
|
+
}
|
|
38
|
+
return [`Last lines of ${d.file}:`, ...d.tail.map((l) => ` ${l}`)];
|
|
39
|
+
}
|
|
40
|
+
/** Read `file` and keep its last `lines` non-empty lines. */
|
|
41
|
+
function diagnoseLog(file, lines = 20) {
|
|
42
|
+
if (!(0, fs_1.existsSync)(file))
|
|
43
|
+
return { file, size: -1, state: 'missing', tail: [] };
|
|
44
|
+
const size = (0, fs_1.statSync)(file).size;
|
|
45
|
+
if (size === 0)
|
|
46
|
+
return { file, size, state: 'empty', tail: [] };
|
|
47
|
+
return { file, size, state: 'ok', tail: tailLines((0, fs_1.readFileSync)(file, 'utf8'), lines) };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Components whose pid is gone within `budgetMs` of their start.
|
|
51
|
+
*
|
|
52
|
+
* Catches what dies at once: a missing script, a package manager that cannot
|
|
53
|
+
* start, a crash before any watcher takes over. A crash under nodemon keeps the
|
|
54
|
+
* supervisor alive and is `lt dev status`'s job (`crashed` after the grace window).
|
|
55
|
+
*/
|
|
56
|
+
function findEarlyExits(components, opts) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
var _a;
|
|
59
|
+
const sleep = (_a = opts.sleep) !== null && _a !== void 0 ? _a : ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
60
|
+
const watched = components.filter((c) => typeof c.pid === 'number');
|
|
61
|
+
const dead = new Set();
|
|
62
|
+
const deadline = Date.now() + opts.budgetMs;
|
|
63
|
+
do {
|
|
64
|
+
for (const c of watched)
|
|
65
|
+
if (!dead.has(c.name) && !opts.isAlive(c.pid))
|
|
66
|
+
dead.add(c.name);
|
|
67
|
+
if (dead.size === watched.length)
|
|
68
|
+
break;
|
|
69
|
+
yield sleep(250);
|
|
70
|
+
} while (Date.now() < deadline);
|
|
71
|
+
return watched.filter((c) => dead.has(c.name)).map((c) => c.name);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* True when a component has been up for `minAgeMs` and its log is still empty.
|
|
76
|
+
* A booting app writes within seconds (nuxt/nest print a banner), so an empty
|
|
77
|
+
* log after that means output is going somewhere else, not that nothing happened.
|
|
78
|
+
*/
|
|
79
|
+
function isSilentLog(d, startedAt, now, minAgeMs = 15000) {
|
|
80
|
+
if (d.state !== 'empty' || !startedAt)
|
|
81
|
+
return false;
|
|
82
|
+
const age = now - new Date(startedAt).getTime();
|
|
83
|
+
return Number.isFinite(age) && age >= minAgeMs;
|
|
84
|
+
}
|
|
85
|
+
/** Last `n` non-empty lines of `content`, oldest first. */
|
|
86
|
+
function tailLines(content, n) {
|
|
87
|
+
return content
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.filter((l) => l.trim().length > 0)
|
|
90
|
+
.slice(-n);
|
|
91
|
+
}
|
|
@@ -74,16 +74,20 @@ function buildCommand(bin, name) {
|
|
|
74
74
|
},
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Manager behind an override like `LT_PM_BIN`.
|
|
79
|
+
*
|
|
80
|
+
* Reads the file NAME, split on both separators and stripped of a Windows launcher
|
|
81
|
+
* extension: there the binary is `C:\\…\\pnpm.cmd`, which neither `endsWith('pnpm')`
|
|
82
|
+
* nor `includes('/pnpm')` matched — the override then resolved to `unknown` and the
|
|
83
|
+
* caller drove the wrong manager.
|
|
84
|
+
*/
|
|
77
85
|
function inferNameFromBin(bin) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return 'yarn';
|
|
84
|
-
}
|
|
85
|
-
if (lower.endsWith('npm') || lower.includes('/npm')) {
|
|
86
|
-
return 'npm';
|
|
86
|
+
var _a;
|
|
87
|
+
const file = (_a = bin.toLowerCase().split(/[\\/]/).pop()) !== null && _a !== void 0 ? _a : '';
|
|
88
|
+
const name = file.replace(/\.(bat|cmd|exe|ps1)$/, '');
|
|
89
|
+
if (name === 'pnpm' || name === 'yarn' || name === 'npm') {
|
|
90
|
+
return name;
|
|
87
91
|
}
|
|
88
92
|
return 'unknown';
|
|
89
93
|
}
|
package/build/lib/dev-patches.js
CHANGED
|
@@ -125,7 +125,7 @@ function patchApiConfig(file) {
|
|
|
125
125
|
* in place.
|
|
126
126
|
*/
|
|
127
127
|
function patchClaudeMd(file, options) {
|
|
128
|
-
const { dbName, identity } = options;
|
|
128
|
+
const { apiPort, appPort, dbName, identity } = options;
|
|
129
129
|
const startMarker = '<!-- lt-dev:url-block:start -->';
|
|
130
130
|
const endMarker = '<!-- lt-dev:url-block:end -->';
|
|
131
131
|
const apiSub = identity.subdomains.api;
|
|
@@ -150,10 +150,20 @@ function patchClaudeMd(file, options) {
|
|
|
150
150
|
'**Active URLs for THIS project:**',
|
|
151
151
|
'',
|
|
152
152
|
];
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
// Both addresses, with who each one is for. The block tells an agent never to
|
|
154
|
+
// assume `localhost:3000/3001` — so if it only named the `*.localhost` host, an
|
|
155
|
+
// agent following it would `curl` a name that does not resolve for Node on
|
|
156
|
+
// Windows, and fail silently. The browser address stays first: it is the one a
|
|
157
|
+
// human opens.
|
|
158
|
+
if (appSub) {
|
|
159
|
+
lines.push(`- App: \`https://${appSub.hostname}\`${appPort ? ` — from a script: \`http://127.0.0.1:${appPort}\`` : ''}`);
|
|
160
|
+
}
|
|
161
|
+
if (apiSub) {
|
|
162
|
+
lines.push(`- API: \`https://${apiSub.hostname}\`${apiPort ? ` — from a script: \`http://127.0.0.1:${apiPort}\`` : ''}`);
|
|
163
|
+
}
|
|
164
|
+
if (appPort || apiPort) {
|
|
165
|
+
lines.push('- The `*.localhost` names are resolved by BROWSERS. Node, `curl` and other tools may not resolve them (they do not on Windows) — use the loopback address from a script.');
|
|
166
|
+
}
|
|
157
167
|
if (dbName)
|
|
158
168
|
lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
|
|
159
169
|
lines.push('');
|