toiljs 0.0.113 → 0.0.114
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/CHANGELOG.md +5 -0
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +145 -27
- package/build/client/.tsbuildinfo +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.js +1 -1
- package/build/compiler/pages.js +1 -13
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +39 -2
- package/build/compiler/toil-docs.generated.js +3 -3
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/host.d.ts +2 -1
- package/build/devserver/daemon/host.js +12 -12
- package/build/devserver/daemon/index.js +3 -3
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/docs/background/daemons.md +49 -3
- package/docs/cli/README.md +4 -2
- package/docs/getting-started/installation.md +18 -0
- package/package.json +15 -15
- package/src/cli/create.ts +1 -0
- package/src/cli/diagnostics.ts +74 -0
- package/src/cli/doctor.ts +105 -27
- package/src/cli/update.ts +30 -3
- package/src/cli/updates.ts +23 -0
- package/src/compiler/index.ts +11 -2
- package/src/compiler/pages.ts +1 -22
- package/src/compiler/prerender.ts +57 -3
- package/src/compiler/toil-docs.generated.ts +3 -3
- package/src/devserver/daemon/host.ts +29 -19
- package/src/devserver/daemon/index.ts +12 -5
- package/test/daemon-emulation.test.ts +44 -2
- package/test/doctor.test.ts +28 -0
- package/test/fixtures/daemon-app.ts +9 -4
- package/test/prerender.test.ts +64 -2
- package/test/update.test.ts +15 -1
- package/tsconfig.base.json +0 -1
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* - Part 3 error bridge: a u16 subsystem code `c` is returned as `-(0x10000 + c)`;
|
|
11
11
|
* the buffer sentinels `-1` (TOO_SMALL) / `-2` (ABSENT) are unchanged.
|
|
12
12
|
*
|
|
13
|
+
* These live in their OWN wasm module namespace, `daemon`, with BARE names, exactly
|
|
14
|
+
* as the edge registers them (toil-backend `src/wasm/cold/imports.rs`, `NS_DAEMON`).
|
|
15
|
+
* They are NOT dotted names under `env` like `data.*` / `crypto.*` are: a cold box
|
|
16
|
+
* that declares them that way resolves here and then trap-stubs at the edge, which
|
|
17
|
+
* is the one shape of drift a dev emulator must never hide.
|
|
18
|
+
*
|
|
13
19
|
* The cold box also imports the request-surface `env.*` MINUS the response/stream
|
|
14
20
|
* functions it must not have (no `set_status`/`set_header`/`respond_file`/
|
|
15
21
|
* `client_ip`/`ratelimit_check`); it keeps `@data`/crypto/env/`Date.now`/email so a
|
|
@@ -38,10 +44,14 @@ export interface ResolvedDaemonConfig {
|
|
|
38
44
|
|
|
39
45
|
/** RECONCILIATION Part 3 u16 error registry (the subset the daemon imports use). */
|
|
40
46
|
export const enum AbiError {
|
|
41
|
-
|
|
47
|
+
DaemonNotLeader = 0x0401,
|
|
48
|
+
DaemonLeaseLost = 0x0402,
|
|
42
49
|
DaemonCallFailed = 0x0405,
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/** The edge's `EPOCH_NONE` / "no such task" sentinel: a plain -1, not a bridged error. */
|
|
53
|
+
const DAEMON_NONE = -1n;
|
|
54
|
+
|
|
45
55
|
/** Encode a u16 subsystem error per the Part 3 negative-return bridge:
|
|
46
56
|
* `code = (-v) - 0x10000`, so `v = -(0x10000 + code)`. */
|
|
47
57
|
export function encodeAbiError(code: number): number {
|
|
@@ -64,26 +74,27 @@ export interface DaemonRuntime {
|
|
|
64
74
|
* `number | bigint` individually, matching the existing db/crypto import maps). */
|
|
65
75
|
type HostFnMap = Record<string, (...args: number[]) => number | bigint>;
|
|
66
76
|
|
|
67
|
-
/** Build the `daemon
|
|
68
|
-
*
|
|
69
|
-
*
|
|
77
|
+
/** Build the `daemon` namespace, closing over the resident `DaemonRuntime`. The names
|
|
78
|
+
* are BARE (`is_leader`, not `daemon.is_leader`): they are keyed under the `daemon`
|
|
79
|
+
* wasm module, matching the edge. These imports do not read guest memory (they answer
|
|
80
|
+
* from the resident scheduler state), so they take no `MemoryRef`. */
|
|
70
81
|
export function buildDaemonNamespace(rt: DaemonRuntime): HostFnMap {
|
|
71
82
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
is_leader: (): number => (rt.isLeader() ? 1 : 0),
|
|
84
|
+
// The edge answers -1 (EPOCH_NONE) when this node does not hold the lease.
|
|
85
|
+
current_epoch: (): bigint => (rt.isLeader() ? rt.epoch() : DAEMON_NONE),
|
|
74
86
|
// The dev lease never expires, so yield/sleep never report LEASE_LOST.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
yield: (): number => 0,
|
|
88
|
+
sleep_ms: (_ms: number | bigint): number => 0,
|
|
89
|
+
task_count: (): number => rt.taskCount(),
|
|
90
|
+
// An unknown / never-firing task is -1 at the edge, NOT a bridged error.
|
|
91
|
+
next_fire_ms: (taskId: number): bigint => {
|
|
79
92
|
const at = rt.nextFireMs(taskId);
|
|
80
|
-
return at === null
|
|
81
|
-
? BigInt(encodeAbiError(AbiError.DaemonScheduleRejected))
|
|
82
|
-
: BigInt(at);
|
|
93
|
+
return at === null ? DAEMON_NONE : BigInt(at);
|
|
83
94
|
},
|
|
84
95
|
// Outbound HTTP call stub: dev returns a "call failed" sentinel rather than
|
|
85
96
|
// performing real network I/O from a synchronous wasm import (section 5.4).
|
|
86
|
-
|
|
97
|
+
http_call: (
|
|
87
98
|
_reqPtr: number,
|
|
88
99
|
_reqLen: number,
|
|
89
100
|
_outPtr: number,
|
|
@@ -103,10 +114,9 @@ export function freshDaemonState(): DaemonState {
|
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
/**
|
|
106
|
-
* The
|
|
107
|
-
*
|
|
108
|
-
* `daemon
|
|
109
|
-
* surface.
|
|
117
|
+
* The import object for the cold daemon box: the request-surface `env` MINUS the
|
|
118
|
+
* response/stream functions (built by `buildEnvImports`), plus the separate
|
|
119
|
+
* `daemon` module. The cold box has no `handle` entry and no response surface.
|
|
110
120
|
*/
|
|
111
121
|
export function buildDaemonImports(
|
|
112
122
|
ref: MemoryRef,
|
|
@@ -118,7 +128,7 @@ export function buildDaemonImports(
|
|
|
118
128
|
...buildEnvImports(ref, state),
|
|
119
129
|
...buildCryptoImports(ref, state.crypto),
|
|
120
130
|
...buildDatabaseImports(ref, state.db),
|
|
121
|
-
...buildDaemonNamespace(rt),
|
|
122
131
|
},
|
|
132
|
+
daemon: buildDaemonNamespace(rt),
|
|
123
133
|
};
|
|
124
134
|
}
|
|
@@ -195,12 +195,19 @@ export class DaemonHost implements DaemonRuntime {
|
|
|
195
195
|
const imports = buildDaemonImports(ref, this.state, this);
|
|
196
196
|
|
|
197
197
|
// Fail-closed up front, with names, when the cold box imports anything outside its allowed
|
|
198
|
-
// surface (the request env subset + crypto + @data
|
|
199
|
-
// functions). Mirrors `WasmServerModule.assertImportSurface` (section 7.1).
|
|
200
|
-
|
|
198
|
+
// surface (the request env subset + crypto + @data, and the separate `daemon` module; NO
|
|
199
|
+
// response/stream functions). Mirrors `WasmServerModule.assertImportSurface` (section 7.1).
|
|
200
|
+
// Both namespaces are checked: `daemon.*` is its own wasm module at the edge, so a guest
|
|
201
|
+
// that declares those names under `env` must fail HERE rather than trap only in production.
|
|
202
|
+
// A non-function import is rejected outright, as the edge does (`NonFunctionImport`):
|
|
203
|
+
// no memory/global/table is ever registered, so leaving one to fail later would
|
|
204
|
+
// surface as an opaque LinkError rather than a named, fail-closed diagnostic.
|
|
205
|
+
const provided = imports as Record<string, Record<string, unknown>>;
|
|
201
206
|
const missing = WebAssembly.Module.imports(this.module)
|
|
202
|
-
.filter(
|
|
203
|
-
|
|
207
|
+
.filter(
|
|
208
|
+
(i) => i.kind !== 'function' || !Object.hasOwn(provided[i.module] ?? {}, i.name),
|
|
209
|
+
)
|
|
210
|
+
.map((i) => `${i.module}.${i.name}${i.kind === 'function' ? '' : ` (${i.kind})`}`);
|
|
204
211
|
if (missing.length > 0) {
|
|
205
212
|
this.log(
|
|
206
213
|
pc.red(
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
-
import { existsSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, utimesSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { tmpdir } from 'node:os';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
@@ -24,7 +24,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
24
24
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
25
25
|
|
|
26
26
|
import { DaemonHost } from '../src/devserver/daemon/index.js';
|
|
27
|
-
import
|
|
27
|
+
import {
|
|
28
|
+
buildDaemonImports,
|
|
29
|
+
freshDaemonState,
|
|
30
|
+
type ResolvedDaemonConfig,
|
|
31
|
+
} from '../src/devserver/daemon/host.js';
|
|
28
32
|
|
|
29
33
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
30
34
|
const FIXTURE = join(here, 'fixtures', 'daemon-app.ts');
|
|
@@ -91,6 +95,44 @@ describe.skipIf(!existsSync(LOCAL_TOILSCRIPT_BIN))('dev daemon emulation', () =>
|
|
|
91
95
|
expect(toilscriptAvailable, 'cold compile of the @daemon fixture failed').toBe(true);
|
|
92
96
|
});
|
|
93
97
|
|
|
98
|
+
// The edge registers these in their OWN wasm module (`NS_DAEMON`), not as dotted
|
|
99
|
+
// names under `env` the way `data.*` and `crypto.*` are. A cold box that gets it
|
|
100
|
+
// wrong resolves against this emulator and then trap-stubs in production, so pin
|
|
101
|
+
// the namespace on both sides: the guest's declared imports, and what we provide.
|
|
102
|
+
it('declares its daemon imports in the `daemon` module with bare names', () => {
|
|
103
|
+
const module = new WebAssembly.Module(readFileSync(coldWasm));
|
|
104
|
+
const daemonImports = WebAssembly.Module.imports(module)
|
|
105
|
+
.filter((i) => i.module === 'daemon')
|
|
106
|
+
.map((i) => i.name)
|
|
107
|
+
.sort();
|
|
108
|
+
expect(daemonImports).toEqual(['current_epoch', 'is_leader', 'task_count']);
|
|
109
|
+
expect(
|
|
110
|
+
WebAssembly.Module.imports(module).filter((i) => i.name.startsWith('daemon.')),
|
|
111
|
+
).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('provides the daemon host functions under the `daemon` namespace', () => {
|
|
115
|
+
const imports = buildDaemonImports({ memory: null }, freshDaemonState(), {
|
|
116
|
+
isLeader: () => true,
|
|
117
|
+
epoch: () => 1n,
|
|
118
|
+
taskCount: () => 0,
|
|
119
|
+
nextFireMs: () => null,
|
|
120
|
+
}) as unknown as Record<string, Record<string, unknown>>;
|
|
121
|
+
expect(Object.keys(imports).sort()).toEqual(['daemon', 'env']);
|
|
122
|
+
expect(Object.keys(imports.daemon).sort()).toEqual([
|
|
123
|
+
'current_epoch',
|
|
124
|
+
'http_call',
|
|
125
|
+
'is_leader',
|
|
126
|
+
'next_fire_ms',
|
|
127
|
+
'sleep_ms',
|
|
128
|
+
'task_count',
|
|
129
|
+
'yield',
|
|
130
|
+
]);
|
|
131
|
+
expect(Object.keys(imports.env).some((k) => k.startsWith('daemon.'))).toBe(false);
|
|
132
|
+
// An unknown task is the edge's plain -1 sentinel, not a bridged error code.
|
|
133
|
+
expect((imports.daemon.next_fire_ms as (id: number) => bigint)(9)).toBe(-1n);
|
|
134
|
+
});
|
|
135
|
+
|
|
94
136
|
it('runs daemon_start exactly once on load', () => {
|
|
95
137
|
const host = new DaemonHost(coldWasm, DAEMON_CFG, 'all', () => {});
|
|
96
138
|
host.refresh();
|
package/test/doctor.test.ts
CHANGED
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
checkSeoUrl,
|
|
19
19
|
checkServerTsPlugin,
|
|
20
20
|
checkStyling,
|
|
21
|
+
checkTypeScript,
|
|
21
22
|
findRelativeAssets,
|
|
23
|
+
rangeMajor,
|
|
22
24
|
satisfiesMin,
|
|
23
25
|
summarize,
|
|
24
26
|
} from '../src/cli/diagnostics';
|
|
@@ -91,6 +93,32 @@ describe('config + environment checks', () => {
|
|
|
91
93
|
expect(checkPeer('react', '^19.0.0', '>=18.0.0').status).toBe('pass');
|
|
92
94
|
});
|
|
93
95
|
|
|
96
|
+
it('checkTypeScript fails an installed 7.x (the native port, no compiler API)', () => {
|
|
97
|
+
const check = checkTypeScript('7.0.2', '^7.0.2');
|
|
98
|
+
expect(check.status).toBe('fail');
|
|
99
|
+
expect(check.detail).toContain('7.0.2');
|
|
100
|
+
expect(check.fix).toContain('^6.0.3');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('checkTypeScript fails a declared 7.x even while a supported copy is installed', () => {
|
|
104
|
+
// The installed copy still works, but the next install would pull the native port.
|
|
105
|
+
expect(checkTypeScript('6.0.3', '^7.0.0').status).toBe('fail');
|
|
106
|
+
expect(checkTypeScript('6.0.3', '>=7').status).toBe('fail');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('checkTypeScript passes a supported 6.x and warns below the floor', () => {
|
|
110
|
+
expect(checkTypeScript('6.0.3', '^6.0.3').status).toBe('pass');
|
|
111
|
+
expect(checkTypeScript('5.9.3', '^5.9.0').status).toBe('warn');
|
|
112
|
+
expect(checkTypeScript(null, null).status).toBe('fail');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('rangeMajor reads the major a range floats to, ignoring comparators', () => {
|
|
116
|
+
expect(rangeMajor('^7.0.2')).toBe(7);
|
|
117
|
+
expect(rangeMajor('>=6.0.0 <7.0.0')).toBe(6);
|
|
118
|
+
expect(rangeMajor('~6.0')).toBe(6);
|
|
119
|
+
expect(rangeMajor('*')).toBe(null);
|
|
120
|
+
});
|
|
121
|
+
|
|
94
122
|
it('checkDuplicatePatterns flags repeated route URLs', () => {
|
|
95
123
|
expect(checkDuplicatePatterns(['/', '/about', '/blog/:id']).status).toBe('pass');
|
|
96
124
|
expect(checkDuplicatePatterns(['/a', '/a']).status).toBe('warn');
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
// A minimal @daemon fixture for the dev daemon-emulation test. It declares the
|
|
2
|
-
//
|
|
2
|
+
// daemon host imports directly (so it needs no toiljs globals lib) and records
|
|
3
3
|
// its activity into resident daemon wasm memory. Compiled with `--targetMode cold`
|
|
4
4
|
// by the test.
|
|
5
5
|
//
|
|
6
6
|
// onStart() -> increments `started` and stamps the lease epoch
|
|
7
7
|
// tick() @scheduled -> increments `tickFast` (1s interval)
|
|
8
8
|
// sixHourly() cron -> increments `tickCron` (0 */6 * * *)
|
|
9
|
+
//
|
|
10
|
+
// The imports live in the `daemon` wasm module with BARE names, as the edge
|
|
11
|
+
// registers them; they are not dotted names under `env`. toilscript's stdlib
|
|
12
|
+
// exposes the same surface as the ambient `Daemon` class (`~lib/daemon`), which
|
|
13
|
+
// real daemons should use instead of hand-declaring these.
|
|
9
14
|
|
|
10
15
|
// @ts-nocheck — this is AssemblyScript source compiled by toilscript, not TS.
|
|
11
16
|
|
|
12
|
-
@external("
|
|
17
|
+
@external("daemon", "is_leader")
|
|
13
18
|
declare function daemonIsLeader(): i32;
|
|
14
19
|
|
|
15
|
-
@external("
|
|
20
|
+
@external("daemon", "current_epoch")
|
|
16
21
|
declare function daemonCurrentEpoch(): i64;
|
|
17
22
|
|
|
18
|
-
@external("
|
|
23
|
+
@external("daemon", "task_count")
|
|
19
24
|
declare function daemonTaskCount(): i32;
|
|
20
25
|
|
|
21
26
|
let started: i32 = 0;
|
package/test/prerender.test.ts
CHANGED
|
@@ -3,9 +3,13 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
5
|
import ts from 'typescript';
|
|
6
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
extractStaticMetadata,
|
|
10
|
+
loadTypeScript,
|
|
11
|
+
loadTypeScriptSync,
|
|
12
|
+
} from '../src/compiler/prerender';
|
|
9
13
|
|
|
10
14
|
const written: string[] = [];
|
|
11
15
|
function tmp(source: string): string {
|
|
@@ -52,3 +56,61 @@ describe('extractStaticMetadata', () => {
|
|
|
52
56
|
expect(extractStaticMetadata(ts, file)).toEqual({ title: 'X' });
|
|
53
57
|
});
|
|
54
58
|
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Builds a throwaway project root whose resolvable `typescript` is `main`-ed at a module exporting
|
|
62
|
+
* only `version` — the shape TypeScript 7 ships, having moved the compiler API to
|
|
63
|
+
* `typescript/unstable/*`. Resolution succeeds, so a truthy check can't tell it apart from a usable
|
|
64
|
+
* compiler; only probing the API can.
|
|
65
|
+
*/
|
|
66
|
+
function rootWithApiLessTypeScript(): string {
|
|
67
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `toil-ts7-${process.pid}-`));
|
|
68
|
+
dirs.push(root);
|
|
69
|
+
const pkgDir = path.join(root, 'node_modules', 'typescript');
|
|
70
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(path.join(root, 'package.json'), '{"name":"fixture"}');
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(pkgDir, 'package.json'),
|
|
74
|
+
'{"name":"typescript","version":"7.0.2","main":"./version.cjs"}',
|
|
75
|
+
);
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(pkgDir, 'version.cjs'),
|
|
78
|
+
`module.exports = { version: '7.0.2', versionMajorMinor: '7.0' };\n`,
|
|
79
|
+
);
|
|
80
|
+
return root;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const dirs: string[] = [];
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
for (const d of dirs.splice(0)) fs.rmSync(d, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('loadTypeScript', () => {
|
|
89
|
+
it('returns null (not a half-built module) when typescript exposes no compiler API', async () => {
|
|
90
|
+
const root = rootWithApiLessTypeScript();
|
|
91
|
+
const warn = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
92
|
+
try {
|
|
93
|
+
// Both must degrade rather than hand back `{ version }`, whose first `ts.ScriptTarget`
|
|
94
|
+
// read would throw `Cannot read properties of undefined (reading 'Latest')` mid-build.
|
|
95
|
+
expect(loadTypeScriptSync(root)).toBeNull();
|
|
96
|
+
await expect(loadTypeScript(root)).resolves.toBeNull();
|
|
97
|
+
} finally {
|
|
98
|
+
warn.mockRestore();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns null when typescript is not installed at all', async () => {
|
|
103
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `toil-nots-${process.pid}-`));
|
|
104
|
+
dirs.push(root);
|
|
105
|
+
fs.writeFileSync(path.join(root, 'package.json'), '{"name":"fixture"}');
|
|
106
|
+
expect(loadTypeScriptSync(root)).toBeNull();
|
|
107
|
+
await expect(loadTypeScript(root)).resolves.toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns the compiler API when a usable typescript is installed', async () => {
|
|
111
|
+
// This repo's own root resolves its devDependency typescript, which has the classic API.
|
|
112
|
+
const root = path.resolve(__dirname, '..');
|
|
113
|
+
expect(loadTypeScriptSync(root)?.createSourceFile).toBeTypeOf('function');
|
|
114
|
+
expect((await loadTypeScript(root))?.ScriptTarget.Latest).toBeTypeOf('number');
|
|
115
|
+
});
|
|
116
|
+
});
|
package/test/update.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { buildRows, classifyBump, parseNcuJson } from '../src/cli/updates';
|
|
3
|
+
import { buildRows, classifyBump, parseNcuJson, withheldUpgrades } from '../src/cli/updates';
|
|
4
4
|
|
|
5
5
|
describe('classifyBump', () => {
|
|
6
6
|
it('classifies major / minor / patch from ranges', () => {
|
|
@@ -42,3 +42,17 @@ describe('buildRows', () => {
|
|
|
42
42
|
expect(rows[0]).toMatchObject({ name: 'foo', from: '?', to: '^2.0.0', bump: 'major' });
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
describe('withheldUpgrades', () => {
|
|
47
|
+
it('withholds an upgrade into typescript 7 (the native port has no compiler API)', () => {
|
|
48
|
+
expect(withheldUpgrades({ typescript: '^7.0.2', react: '^20.0.0' })).toEqual(['typescript']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('still offers typescript bumps inside the supported major', () => {
|
|
52
|
+
expect(withheldUpgrades({ typescript: '^6.1.0' })).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('ignores packages with no declared ceiling', () => {
|
|
56
|
+
expect(withheldUpgrades({ vite: '^9.0.0', eslint: '^11.0.0' })).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
});
|