@ultimat3/cli 20.1.3 → 20.1.4
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/package.json +29 -29
- package/src/cmd-mcp.ts +2 -2
- package/src/cmd-shot.ts +8 -1
- package/src/error-codes.ts +5 -0
- package/src/mcp-errors.ts +3 -0
- package/src/mcp-host.ts +3 -0
- package/src/mcp-ui.ts +136 -0
- package/src/output.ts +1 -1
- package/src/templates/guard-unzoned-date.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "20.1.
|
|
3
|
+
"version": "20.1.4",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,34 +37,34 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@babel/core": "^7.28.4",
|
|
40
|
-
"@ultimat3/action": "20.1.
|
|
41
|
-
"@ultimat3/admin": "20.1.
|
|
42
|
-
"@ultimat3/ai": "20.1.
|
|
43
|
-
"@ultimat3/auth": "20.1.
|
|
44
|
-
"@ultimat3/cache": "20.1.
|
|
45
|
-
"@ultimat3/core": "20.1.
|
|
46
|
-
"@ultimat3/db": "20.1.
|
|
47
|
-
"@ultimat3/entity": "20.1.
|
|
48
|
-
"@ultimat3/flags": "20.1.
|
|
49
|
-
"@ultimat3/http": "20.1.
|
|
50
|
-
"@ultimat3/i18n": "20.1.
|
|
51
|
-
"@ultimat3/jobs": "20.1.
|
|
52
|
-
"@ultimat3/mail": "20.1.
|
|
53
|
-
"@ultimat3/manifest": "20.1.
|
|
54
|
-
"@ultimat3/mcp": "20.1.
|
|
55
|
-
"@ultimat3/money": "20.1.
|
|
56
|
-
"@ultimat3/notify": "20.1.
|
|
57
|
-
"@ultimat3/policy": "20.1.
|
|
58
|
-
"@ultimat3/pwa": "20.1.
|
|
59
|
-
"@ultimat3/query": "20.1.
|
|
60
|
-
"@ultimat3/realtime": "20.1.
|
|
61
|
-
"@ultimat3/render": "20.1.
|
|
62
|
-
"@ultimat3/schema": "20.1.
|
|
63
|
-
"@ultimat3/scraping": "20.1.
|
|
64
|
-
"@ultimat3/seo": "20.1.
|
|
65
|
-
"@ultimat3/storage": "20.1.
|
|
66
|
-
"@ultimat3/testing": "20.1.
|
|
67
|
-
"@ultimat3/time": "20.1.
|
|
40
|
+
"@ultimat3/action": "20.1.4",
|
|
41
|
+
"@ultimat3/admin": "20.1.4",
|
|
42
|
+
"@ultimat3/ai": "20.1.4",
|
|
43
|
+
"@ultimat3/auth": "20.1.4",
|
|
44
|
+
"@ultimat3/cache": "20.1.4",
|
|
45
|
+
"@ultimat3/core": "20.1.4",
|
|
46
|
+
"@ultimat3/db": "20.1.4",
|
|
47
|
+
"@ultimat3/entity": "20.1.4",
|
|
48
|
+
"@ultimat3/flags": "20.1.4",
|
|
49
|
+
"@ultimat3/http": "20.1.4",
|
|
50
|
+
"@ultimat3/i18n": "20.1.4",
|
|
51
|
+
"@ultimat3/jobs": "20.1.4",
|
|
52
|
+
"@ultimat3/mail": "20.1.4",
|
|
53
|
+
"@ultimat3/manifest": "20.1.4",
|
|
54
|
+
"@ultimat3/mcp": "20.1.4",
|
|
55
|
+
"@ultimat3/money": "20.1.4",
|
|
56
|
+
"@ultimat3/notify": "20.1.4",
|
|
57
|
+
"@ultimat3/policy": "20.1.4",
|
|
58
|
+
"@ultimat3/pwa": "20.1.4",
|
|
59
|
+
"@ultimat3/query": "20.1.4",
|
|
60
|
+
"@ultimat3/realtime": "20.1.4",
|
|
61
|
+
"@ultimat3/render": "20.1.4",
|
|
62
|
+
"@ultimat3/schema": "20.1.4",
|
|
63
|
+
"@ultimat3/scraping": "20.1.4",
|
|
64
|
+
"@ultimat3/seo": "20.1.4",
|
|
65
|
+
"@ultimat3/storage": "20.1.4",
|
|
66
|
+
"@ultimat3/testing": "20.1.4",
|
|
67
|
+
"@ultimat3/time": "20.1.4",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/cmd-mcp.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `x mcp serve` — the framework's dev MCP server over stdio or HTTP. The
|
|
1
|
+
// `x mcp serve` — the framework's dev MCP server over stdio or HTTP. The 15 tools, the JSON-RPC
|
|
2
2
|
// dispatch, both transports and the structural SQL refusals all come from `@ultimat3/mcp`; the CLI
|
|
3
3
|
// supplies only the app, the caller and the socket. A tool answered here would be a second answer
|
|
4
4
|
// to a question the framework already answers.
|
|
@@ -117,7 +117,7 @@ export function startMcpHttp(host: CliMcpServer, port: number): McpHttpServer {
|
|
|
117
117
|
* What the session reports when it is over — on STDERR, which is the half this file's header
|
|
118
118
|
* claimed and did not have. `dispatch` renders a `CommandResult` only after `run` resolves, and
|
|
119
119
|
* this resolves when the peer closes stdin, so nothing lands mid-session; but fd 1 under this
|
|
120
|
-
* transport carries JSON-RPC frames, and `✓ mcp stdio serving
|
|
120
|
+
* transport carries JSON-RPC frames, and `✓ mcp stdio serving 15 tools` arriving on it after the
|
|
121
121
|
* loop is a malformed frame to a peer still draining, and a second document under `--json`.
|
|
122
122
|
*
|
|
123
123
|
* Its own function so the addressing is testable without a live peer: `serveStdio` resolves only
|
package/src/cmd-shot.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { mkdirSync } from 'node:fs';
|
|
9
9
|
import { join, resolve } from 'node:path';
|
|
10
10
|
import { IDLE_HYDRATE_TIMEOUT_MS } from '@ultimat3/render';
|
|
11
|
-
import type { ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
|
|
11
|
+
import type { ColorScheme, ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
|
|
12
12
|
import { DEFAULT_PAGE_TIMEOUT_MS, systemScrapeClock } from '@ultimat3/scraping';
|
|
13
13
|
import { requireAppRoot } from './app-root';
|
|
14
14
|
import { appBrowser } from './browser-launcher';
|
|
@@ -190,6 +190,12 @@ export interface ShotRun {
|
|
|
190
190
|
* with `--port 0` the port — and therefore the origin — does not exist until after the boot.
|
|
191
191
|
*/
|
|
192
192
|
readonly extraHosts?: string | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* What `prefers-color-scheme` the page sees, emulated BEFORE navigation so the boot script's
|
|
195
|
+
* "system" branch answers the same on every box. Absent means the box's own preference — what
|
|
196
|
+
* `x shot` has always done — and `ui.shot` names one explicitly for exactly that reason.
|
|
197
|
+
*/
|
|
198
|
+
readonly colorScheme?: ColorScheme | undefined;
|
|
193
199
|
readonly now?: (() => Date) | undefined;
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -219,6 +225,7 @@ export async function runShot(options: ShotRun): Promise<ShotArtifacts> {
|
|
|
219
225
|
timeoutMs: options.timeoutMs,
|
|
220
226
|
});
|
|
221
227
|
const page = session.page;
|
|
228
|
+
if (options.colorScheme !== undefined) await page.colorScheme(options.colorScheme);
|
|
222
229
|
await page.goto(requestedUrl, { timeout: options.timeoutMs });
|
|
223
230
|
if (options.settleMs > 0) await Bun.sleep(options.settleMs);
|
|
224
231
|
// The probe may legitimately answer nothing — a page that refuses evaluation, a driver with no
|
package/src/error-codes.ts
CHANGED
|
@@ -154,6 +154,9 @@ export const CLI_OWNED_ERROR_CODES = [
|
|
|
154
154
|
// repair is an install, and only `duplicate-packages.ts` can see that.
|
|
155
155
|
'X_PACKAGE_DUPLICATED',
|
|
156
156
|
'X_SHOT_BROWSER_MISSING',
|
|
157
|
+
// `ui.shot` (the dev MCP server): a route it will not photograph, and why.
|
|
158
|
+
'X_UI_SHOT_ROUTE_UNKNOWN',
|
|
159
|
+
'X_UI_SHOT_ROUTE_UNBUDGETED',
|
|
157
160
|
// `x shot --island` — one code per way a component's named state fails to become a picture.
|
|
158
161
|
// The last of the four is the one that gates: it is checked against the expansion computed
|
|
159
162
|
// before a browser existed, so a capture loop that swallowed a failure cannot exit 0.
|
|
@@ -297,6 +300,8 @@ export const CLI_ERROR_TITLES: Readonly<Record<CliOwnedErrorCode, string>> = {
|
|
|
297
300
|
X_WORKSPACE_DEP_UNDECLARED: 'a workspace imports another workspace it does not declare',
|
|
298
301
|
X_PACKAGE_DUPLICATED: 'two copies of one registry-holding framework package are installed',
|
|
299
302
|
X_SHOT_BROWSER_MISSING: 'x shot found no browser library in the app',
|
|
303
|
+
X_UI_SHOT_ROUTE_UNKNOWN: 'ui.shot was asked for a path no route answers',
|
|
304
|
+
X_UI_SHOT_ROUTE_UNBUDGETED: 'ui.shot refused a route that declares no budget.js',
|
|
300
305
|
X_SHOT_ISLAND_STATES_EMPTY: 'an island states file declares no manifest',
|
|
301
306
|
X_SHOT_ISLAND_UNPHOTOGRAPHABLE: 'the island never reached a state worth photographing',
|
|
302
307
|
X_SHOT_ISLAND_UNSTUBBED_REQUEST: 'the island requested something no state stub answers',
|
package/src/mcp-errors.ts
CHANGED
|
@@ -55,6 +55,9 @@ const CLI_FIXES: Readonly<Record<CliErrorCode, string>> = {
|
|
|
55
55
|
X_PACKAGE_DUPLICATED:
|
|
56
56
|
'x i18n check --json # the finding names both copies and the package.json to pin',
|
|
57
57
|
X_SHOT_BROWSER_MISSING: 'bun add -d puppeteer-core',
|
|
58
|
+
X_UI_SHOT_ROUTE_UNKNOWN: 'x routes --json # then ui.shot with one of its path values',
|
|
59
|
+
X_UI_SHOT_ROUTE_UNBUDGETED:
|
|
60
|
+
"x build --target static --json && x verify --only budgets --json # after declaring budget: { js: '<n>kb' } in the route file",
|
|
58
61
|
// The four island-capture codes. Each one's real repair is an edit to the app's own states file
|
|
59
62
|
// or component, which no command can perform — so each names the command that REPRODUCES it with
|
|
60
63
|
// the file and the reason attached, which is the runnable half.
|
package/src/mcp-host.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { execOutput } from './exec';
|
|
|
44
44
|
import { databaseTarget } from './mcp-db-target';
|
|
45
45
|
import { explainErrorCode } from './mcp-errors';
|
|
46
46
|
import { parseBunTest } from './mcp-test-output';
|
|
47
|
+
import { uiCapabilities } from './mcp-ui';
|
|
47
48
|
import { readMigrations } from './migrations';
|
|
48
49
|
import { retryMemo } from './retry-memo';
|
|
49
50
|
import { testEnvOverrides } from './test-dotenv';
|
|
@@ -281,6 +282,8 @@ function capabilities(input: DevHostInput, lazy: LazyServices): DevCapabilities
|
|
|
281
282
|
|
|
282
283
|
explainError: explainErrorCode,
|
|
283
284
|
|
|
285
|
+
...uiCapabilities({ root, env }),
|
|
286
|
+
|
|
284
287
|
async verify(fix: boolean): Promise<VerifyResult> {
|
|
285
288
|
// The one safe autofix this repo actually has. Anything more would be the gate rewriting
|
|
286
289
|
// code it was asked to judge.
|
package/src/mcp-ui.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The dev MCP server's two eyes: `ui.shot` (a route) and `ui.island` (a component's states), as
|
|
2
|
+
// the `DevCapabilities` half `packages/mcp` declares and cannot satisfy — a browser is the CLI's
|
|
3
|
+
// to launch. Both are `x shot` under another name: the same server lookup (a running `x dev` is
|
|
4
|
+
// reused through its lock, otherwise a scratch one boots), the same driver, the same verdict.
|
|
5
|
+
// Nothing here is a new capability; it is the existing one made reachable from inside the loop
|
|
6
|
+
// an agent already works in, so "does it look right" stops needing a hand-written script.
|
|
7
|
+
|
|
8
|
+
// why: Bun exposes no path-join primitive, and the picture's directory is a path an agent opens.
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { UltimateError } from '@ultimat3/core';
|
|
11
|
+
import type { UiIslandInput, UiIslandResult, UiShotInput, UiShotResult } from '@ultimat3/mcp';
|
|
12
|
+
import { describeRoutes } from '@ultimat3/render';
|
|
13
|
+
import { DEFAULT_PAGE_TIMEOUT_MS } from '@ultimat3/scraping';
|
|
14
|
+
import { appBrowser } from './browser-launcher';
|
|
15
|
+
import { DEFAULT_SETTLE_MS, runShot, SHOT_DIR, shotSlug } from './cmd-shot';
|
|
16
|
+
import { islandShot } from './cmd-shot-island';
|
|
17
|
+
import type { Env } from './dev-services';
|
|
18
|
+
import { islandVerdictJson } from './island-verdict';
|
|
19
|
+
import { shotBrowserChoice } from './shot-browser';
|
|
20
|
+
import { devServerFor } from './shot-server';
|
|
21
|
+
import { verdictJson } from './shot-verdict';
|
|
22
|
+
|
|
23
|
+
/** Kernel-picked, as `x shot` picks it: a scratch server never fights another project for :3000. */
|
|
24
|
+
const SCRATCH_PORT = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The route a picture is of has to be a route this app declares WITH a JS budget. The gate
|
|
28
|
+
* refuses an unmeasured route (`X_BUDGET_UNMEASURED`), and a picture of a route nobody has
|
|
29
|
+
* finished declaring is a picture of a draft — an agent judging it would judge the wrong thing.
|
|
30
|
+
* Matched on the declared pattern, so `/links/abc123` finds `/links/:slug`.
|
|
31
|
+
*/
|
|
32
|
+
export interface DeclaredRoute {
|
|
33
|
+
readonly path: string;
|
|
34
|
+
readonly file: string;
|
|
35
|
+
readonly budgetJs: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function assertBudgetedRoute(route: string, declared: readonly DeclaredRoute[]): void {
|
|
39
|
+
const path = route.split('?')[0] ?? route;
|
|
40
|
+
const hit = declared.find((entry) => matches(entry.path, path));
|
|
41
|
+
if (hit === undefined) {
|
|
42
|
+
throw new UltimateError({
|
|
43
|
+
code: 'X_UI_SHOT_ROUTE_UNKNOWN',
|
|
44
|
+
cause: `no route in this app answers ${route}`,
|
|
45
|
+
fix: 'x routes --json # then ui.shot with one of its `path` values',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (hit.budgetJs === null) {
|
|
49
|
+
throw new UltimateError({
|
|
50
|
+
code: 'X_UI_SHOT_ROUTE_UNBUDGETED',
|
|
51
|
+
cause: `${hit.file} declares no budget.js, so x verify would refuse it as X_BUDGET_UNMEASURED — a picture of it would be a picture of a draft`,
|
|
52
|
+
fix: `declare budget: { js: '<n>kb' } in ${hit.file}, then: x build --target static --json && x verify --only budgets --json`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `/links/:slug` matches `/links/abc123`; one segment per `:param`, no globbing. */
|
|
58
|
+
export function matches(pattern: string, path: string): boolean {
|
|
59
|
+
const want = pattern.split('/');
|
|
60
|
+
const have = path.split('/');
|
|
61
|
+
if (want.length !== have.length) return false;
|
|
62
|
+
return want.every((segment, index) => segment.startsWith(':') || segment === have[index]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface UiHostInput {
|
|
66
|
+
readonly root: string;
|
|
67
|
+
readonly env: Env;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function uiCapabilities(input: UiHostInput): {
|
|
71
|
+
shotRoute(shot: UiShotInput): Promise<UiShotResult>;
|
|
72
|
+
shotIsland(island: UiIslandInput): Promise<UiIslandResult>;
|
|
73
|
+
} {
|
|
74
|
+
const { root, env } = input;
|
|
75
|
+
const boot = () => devServerFor(root, env, SCRATCH_PORT);
|
|
76
|
+
// The same choice `x shot` makes from the environment: `PUPPETEER_EXECUTABLE_PATH`, a
|
|
77
|
+
// provider's CDP URL, or the launcher's own discovery.
|
|
78
|
+
const browser = () => shotBrowserChoice({ cdpFlag: undefined, browserFlag: undefined, env });
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
async shotRoute(shot) {
|
|
82
|
+
assertBudgetedRoute(shot.route, describeRoutes());
|
|
83
|
+
const { cdpUrl, executablePath } = browser();
|
|
84
|
+
const driver = await appBrowser({
|
|
85
|
+
root,
|
|
86
|
+
viewport: shot.viewport,
|
|
87
|
+
...(executablePath === undefined ? {} : { executablePath }),
|
|
88
|
+
...(cdpUrl === undefined ? {} : { cdpUrl }),
|
|
89
|
+
});
|
|
90
|
+
// One directory per (route, viewport, scheme), so two pictures of one route at two widths
|
|
91
|
+
// never overwrite each other and an agent can hold both.
|
|
92
|
+
const outDir = join(
|
|
93
|
+
root,
|
|
94
|
+
SHOT_DIR,
|
|
95
|
+
shotSlug(shot.route),
|
|
96
|
+
`${shot.viewport.width}x${shot.viewport.height}-${shot.colorScheme}`,
|
|
97
|
+
);
|
|
98
|
+
const artifacts = await runShot({
|
|
99
|
+
route: shot.route,
|
|
100
|
+
outDir,
|
|
101
|
+
driver,
|
|
102
|
+
boot,
|
|
103
|
+
settleMs: DEFAULT_SETTLE_MS,
|
|
104
|
+
timeoutMs: DEFAULT_PAGE_TIMEOUT_MS,
|
|
105
|
+
fullPage: shot.fullPage,
|
|
106
|
+
colorScheme: shot.colorScheme,
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
ok: artifacts.verdict.ok,
|
|
110
|
+
image: artifacts.image,
|
|
111
|
+
verdictFile: artifacts.verdictFile,
|
|
112
|
+
verdict: verdictJson(artifacts.verdict),
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
async shotIsland(island) {
|
|
117
|
+
const { cdpUrl, executablePath } = browser();
|
|
118
|
+
const artifacts = await islandShot({
|
|
119
|
+
root,
|
|
120
|
+
island: island.island,
|
|
121
|
+
...(island.state === undefined ? {} : { state: island.state }),
|
|
122
|
+
settleMs: DEFAULT_SETTLE_MS,
|
|
123
|
+
timeoutMs: DEFAULT_PAGE_TIMEOUT_MS,
|
|
124
|
+
...(executablePath === undefined ? {} : { executablePath }),
|
|
125
|
+
...(cdpUrl === undefined ? {} : { cdpUrl }),
|
|
126
|
+
boot,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
ok: artifacts.verdict.ok,
|
|
130
|
+
dir: artifacts.dir,
|
|
131
|
+
verdictFile: artifacts.verdictFile,
|
|
132
|
+
verdict: islandVerdictJson(artifacts.verdict),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
package/src/output.ts
CHANGED
|
@@ -64,7 +64,7 @@ export interface CommandResult {
|
|
|
64
64
|
* Which fd this result is written to. `stdout` for every command, absent included — and
|
|
65
65
|
* `stderr` for the one case where fd 1 is not the command's to write on: `x mcp serve
|
|
66
66
|
* --transport stdio`, whose stdout carries JSON-RPC frames, and where the `✓ mcp stdio serving
|
|
67
|
-
*
|
|
67
|
+
* 15 tools` line printed after the loop exits is a malformed frame to whatever is reading.
|
|
68
68
|
*
|
|
69
69
|
* Behaviour, not a fact, exactly like `hold` above — so NEITHER renderer carries it. It says
|
|
70
70
|
* where a rendered line goes, and a payload that also claimed it would be a second answer to a
|
|
@@ -74,10 +74,17 @@ export function unzonedDates(files: readonly SourceFile[]): readonly Finding[] {
|
|
|
74
74
|
const open = match.index + match[0].length - 1;
|
|
75
75
|
if (argumentsOf(text, open).includes('timeZone')) continue;
|
|
76
76
|
const line = lineOf(text, match.index);
|
|
77
|
+
// The bare \`.toLocaleString(\` is ALSO \`Number.prototype.toLocaleString\`, and a regex cannot
|
|
78
|
+
// tell a count from a date. The match stands — a date formatted this way is the defect this
|
|
79
|
+
// guard exists for — but the fix names the number exit too, because \`{ timeZone }\` is not
|
|
80
|
+
// one: a number ignores it, and an author following the fix verbatim would ship a lie.
|
|
81
|
+
const bare = match[0].trim() === '.toLocaleString(';
|
|
77
82
|
findings.push({
|
|
78
83
|
code: CODE,
|
|
79
84
|
cause: \`\${file.path}:\${line} calls \${match[0].trim()}) with no timeZone — it formats in whatever zone the process happens to run in, so one row reads as two different days across two containers\`,
|
|
80
|
-
fix:
|
|
85
|
+
fix: bare
|
|
86
|
+
? \`in \${file.path}: a Date → pass an explicit IANA zone, at.toLocaleString(locale, { timeZone: 'UTC' }); a NUMBER → new Intl.NumberFormat(locale).format(n) instead — then: x verify\`
|
|
87
|
+
: \`pass an explicit IANA zone in \${file.path} — toLocaleDateString(locale, { timeZone: 'UTC' }) — then: x verify\`,
|
|
81
88
|
at: file.path,
|
|
82
89
|
});
|
|
83
90
|
}
|
|
@@ -130,6 +137,16 @@ unitTest('Intl.DateTimeFormat and toLocaleTimeString are the same rule', () => {
|
|
|
130
137
|
expect(unzonedDates(file("at.toLocaleTimeString('en-US');"))).toHaveLength(1);
|
|
131
138
|
});
|
|
132
139
|
|
|
140
|
+
unitTest('the bare toLocaleString names the number exit, since a count matches it too', () => {
|
|
141
|
+
const findings = unzonedDates(file("const shown = count.toLocaleString('en-US');"));
|
|
142
|
+
expect(findings).toHaveLength(1);
|
|
143
|
+
expect(findings[0]?.fix).toContain('Intl.NumberFormat');
|
|
144
|
+
expect(findings[0]?.fix).toContain('timeZone');
|
|
145
|
+
// The dated forms are unambiguous and keep the zone-only fix.
|
|
146
|
+
const dated = unzonedDates(file("at.toLocaleDateString('en-US');"));
|
|
147
|
+
expect(dated[0]?.fix).not.toContain('Intl.NumberFormat');
|
|
148
|
+
});
|
|
149
|
+
|
|
133
150
|
unitTest('a commented-out call is a note, not a call', () => {
|
|
134
151
|
expect(unzonedDates(file("// at.toLocaleDateString('en-US');"))).toEqual([]);
|
|
135
152
|
});
|