@zerotal/core 1.11.2 → 1.13.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/CHANGELOG.md +26 -0
- package/api-surface.md +1 -1
- package/package.json +1 -1
- package/src/command/CommandRunner.ts +3 -1
- package/src/command/builtin/DevCommand.ts +4 -3
- package/src/command/builtin/ServeCommand.ts +26 -2
- package/src/lock/LockManager.ts +6 -7
- package/src/lock/drivers/LockDriver.ts +9 -4
- package/src/upgrade/codemods/client-tagged-template.ts +108 -0
- package/src/upgrade/codemods/index.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,32 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.13.0] — 2026-08-31
|
|
12
|
+
|
|
13
|
+
### Changed — **BREAKING**
|
|
14
|
+
|
|
15
|
+
- **`LockDriver.extend()` is required.** Only affects a custom driver; all three built-in
|
|
16
|
+
ones implement it.
|
|
17
|
+
|
|
18
|
+
It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and the
|
|
19
|
+
fallback was correct only by coincidence — `acquire` happens to be an owner-guarded refresh
|
|
20
|
+
on every built-in driver, and nothing in the interface said it had to be. A third-party
|
|
21
|
+
driver whose `acquire` takes a _free_ lock, which is the ordinary reading of the word, would
|
|
22
|
+
have had `refresh()` silently take a lock another holder owned. That is the one thing a lock
|
|
23
|
+
exists to prevent, and the contract was quietly assuming its way past it.
|
|
24
|
+
|
|
25
|
+
- **`routes:types` and `serve --dev` are retired**, in favour of `route:types` and `dev`.
|
|
26
|
+
`zt upgrade` rewrites both.
|
|
27
|
+
|
|
28
|
+
`serve --dev` **fails with a message** rather than being ignored. The flag is still declared
|
|
29
|
+
for that reason alone: flag parsing runs non-strict, so deleting it would have left
|
|
30
|
+
`serve --dev` starting a plain server with no watcher and no explanation — a retired flag
|
|
31
|
+
that silently changes what a command does is worse than one that is still there.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **The `client-tagged-template` codemod**, for the Flow removal above.
|
|
36
|
+
|
|
11
37
|
## [1.11.1] — 2026-08-31
|
|
12
38
|
|
|
13
39
|
### Added
|
package/api-surface.md
CHANGED
|
@@ -3034,7 +3034,7 @@ interface LockDriver = {
|
|
|
3034
3034
|
acquire: (key: string, owner: string, ttlSeconds: number) => Promise<boolean>
|
|
3035
3035
|
dispose?: () => void
|
|
3036
3036
|
exists: (key: string) => Promise<boolean>
|
|
3037
|
-
extend
|
|
3037
|
+
extend: (key: string, owner: string, ttlSeconds: number) => Promise<boolean>
|
|
3038
3038
|
forceRelease: (key: string) => Promise<void>
|
|
3039
3039
|
release: (key: string, owner: string) => Promise<boolean>
|
|
3040
3040
|
}
|
package/package.json
CHANGED
|
@@ -392,7 +392,9 @@ export class CommandRunner {
|
|
|
392
392
|
|
|
393
393
|
// route:types reads the same booted router, so it is available wherever
|
|
394
394
|
// route:list is — including in CI containers that only ever run `--check`.
|
|
395
|
-
|
|
395
|
+
//
|
|
396
|
+
// The `routes:types` alias was retired in 1.13.0; `zt upgrade` rewrites it.
|
|
397
|
+
this.register(RouteTypesCommand);
|
|
396
398
|
|
|
397
399
|
// Non-web commands (console, worker, test).
|
|
398
400
|
if (this._app._env !== "web") {
|
|
@@ -80,9 +80,10 @@ export class DevCommand extends ServeCommand {
|
|
|
80
80
|
];
|
|
81
81
|
|
|
82
82
|
override async run(): Promise<void> {
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
this
|
|
83
|
+
// Set the parent's own switch rather than the `dev` flag, which is now
|
|
84
|
+
// reserved for refusing `serve --dev` by name. Same supervisor, same port
|
|
85
|
+
// resolution, same banner — this subclasses so the two cannot drift.
|
|
86
|
+
this._dev = true;
|
|
86
87
|
await super.run();
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -48,9 +48,15 @@ export class ServeCommand extends Command {
|
|
|
48
48
|
default: 3000,
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
|
+
// Declared only so it can be refused by name. Removing it outright would be
|
|
52
|
+
// worse than keeping it: `parseFlagsAndArgs` runs with `strict: false`, so an
|
|
53
|
+
// undeclared `--dev` is parsed and dropped, and `serve --dev` would have gone
|
|
54
|
+
// on starting a plain server with no watcher and no message. A retired flag
|
|
55
|
+
// that silently changes what a command does is the failure this release is
|
|
56
|
+
// otherwise about.
|
|
51
57
|
name: "dev",
|
|
52
58
|
type: "boolean",
|
|
53
|
-
description: "
|
|
59
|
+
description: "Retired in 1.13.0 — use `zt dev`",
|
|
54
60
|
default: false,
|
|
55
61
|
},
|
|
56
62
|
{
|
|
@@ -74,8 +80,26 @@ export class ServeCommand extends Command {
|
|
|
74
80
|
},
|
|
75
81
|
];
|
|
76
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Set by {@link DevCommand} rather than by a flag, so `serve --dev` can be
|
|
85
|
+
* refused while `dev` keeps the supervisor it subclasses.
|
|
86
|
+
*
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
protected _dev = false;
|
|
90
|
+
|
|
77
91
|
async run(): Promise<void> {
|
|
78
|
-
|
|
92
|
+
if (this.flags["dev"] === true && !this._dev) {
|
|
93
|
+
this.error(
|
|
94
|
+
"`serve --dev` was retired in 1.13.0. Use `bun zt dev` — it is the same supervisor, " +
|
|
95
|
+
"the same processes and the same restart behaviour, plus the flags that only make " +
|
|
96
|
+
"sense with a deck on screen (--only, --without, --list, --stream).",
|
|
97
|
+
);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isDev = this._dev;
|
|
79
103
|
const isWorker = this.flags["dev-worker"] as boolean;
|
|
80
104
|
|
|
81
105
|
// ── DEV ORCHESTRATOR (Process 1) ────────────────────────────────────────
|
package/src/lock/LockManager.ts
CHANGED
|
@@ -135,13 +135,12 @@ export class ManagedLock {
|
|
|
135
135
|
if (!this._acquired) return false;
|
|
136
136
|
const ttl = ttlSeconds ?? this._ttl;
|
|
137
137
|
|
|
138
|
-
// `extend` is
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
: await this._driver.acquire(this._key, this._owner, ttl);
|
|
138
|
+
// `extend` is required on the contract since 1.13.0, so there is no fallback
|
|
139
|
+
// here any more. The one it replaced — `acquire(key, owner, ttl)` — worked only
|
|
140
|
+
// because every built-in driver happens to make `acquire` an owner-guarded
|
|
141
|
+
// refresh, which the interface never required and a third-party driver had no
|
|
142
|
+
// reason to know.
|
|
143
|
+
const extended = await this._driver.extend(this._key, this._owner, ttl);
|
|
145
144
|
|
|
146
145
|
if (!extended) {
|
|
147
146
|
this._acquired = false;
|
|
@@ -32,11 +32,16 @@ export interface LockDriver {
|
|
|
32
32
|
* returns `false` when the key is free or held by someone else, so a holder
|
|
33
33
|
* that lost the lock learns about it rather than extending a stranger's.
|
|
34
34
|
*
|
|
35
|
-
* **
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* **Required since 1.13.0.** It shipped optional in 1.5.0 with `acquire()` as a
|
|
36
|
+
* fallback, and the fallback was only ever correct by coincidence: `acquire` is
|
|
37
|
+
* an owner-guarded refresh on all three built-in drivers, and nothing in this
|
|
38
|
+
* interface said it had to be. A third-party driver whose `acquire` takes a free
|
|
39
|
+
* lock — the ordinary reading of the word — would have had `refresh()` silently
|
|
40
|
+
* steal a lock another holder owned, which is the one thing a lock exists to
|
|
41
|
+
* prevent. Requiring the method turns an assumption the contract never stated
|
|
42
|
+
* into something a driver has to answer.
|
|
38
43
|
*/
|
|
39
|
-
extend
|
|
44
|
+
extend(key: string, owner: string, ttlSeconds: number): Promise<boolean>;
|
|
40
45
|
|
|
41
46
|
/** Release background resources (timers, DB connections). */
|
|
42
47
|
dispose?(): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ledger #5 — `Component.client(…)` gives way to the `$` tagged template.
|
|
3
|
+
*
|
|
4
|
+
* This one is a security fix wearing an ergonomics change's clothes, and that is
|
|
5
|
+
* why it shipped in a minor rather than waiting for 2.0. `client()` takes a
|
|
6
|
+
* **string** and queues it to be evaluated in the browser, so the caller owns the
|
|
7
|
+
* escaping — and its own docblock had to say *never interpolate unescaped user
|
|
8
|
+
* input*, which is a warning about a footgun rather than a design. `$` is a tagged
|
|
9
|
+
* template, so every `${…}` is encoded as a JS literal before it reaches the page
|
|
10
|
+
* and the caller cannot get it wrong by forgetting.
|
|
11
|
+
*
|
|
12
|
+
* ## What this rewrites, and what it hands back
|
|
13
|
+
*
|
|
14
|
+
* A call whose argument is **one template literal or one plain string** is
|
|
15
|
+
* mechanical: the quotes become backticks and `client(` becomes `` $` ``. Nothing
|
|
16
|
+
* about the expression changes.
|
|
17
|
+
*
|
|
18
|
+
* A call whose argument is **anything else** is a handover. `client(expr)` where
|
|
19
|
+
* `expr` is a variable, a concatenation, or a function call is exactly the shape
|
|
20
|
+
* the security note was about — the string was built somewhere else, possibly out
|
|
21
|
+
* of user input, and turning it into `` $`${expr}` `` would change its meaning:
|
|
22
|
+
* the tag would encode the whole expression as a *string literal* rather than
|
|
23
|
+
* splicing it as code. That is a silent behaviour change, so a person decides.
|
|
24
|
+
*
|
|
25
|
+
* Getting this backwards is the failure worth avoiding. A codemod that
|
|
26
|
+
* mechanically wrapped every argument would leave an app that compiles, runs, and
|
|
27
|
+
* quietly does nothing where it used to run a script.
|
|
28
|
+
*/
|
|
29
|
+
import type { Change, Codemod, CodemodResult, Manual, SourceFile } from "../types.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `this.client(` followed by a single template literal with no substitution of its
|
|
33
|
+
* own, to the closing `)`.
|
|
34
|
+
*
|
|
35
|
+
* Backtick-delimited, containing no backtick and no `${`, so the match cannot run
|
|
36
|
+
* past the end of the literal.
|
|
37
|
+
*
|
|
38
|
+
* A **bare `$` is allowed, and has to be.** `$refs`, `$el` and the `$flow` magics
|
|
39
|
+
* are the whole reason anyone calls this — the method's own docblock example is
|
|
40
|
+
* `` this.client(`$refs.titleInput.focus()`) ``. An earlier draft excluded `$`
|
|
41
|
+
* outright to keep `${` out, which skipped every realistic call site while passing
|
|
42
|
+
* a test suite written around `alert(1)`.
|
|
43
|
+
*/
|
|
44
|
+
const PLAIN_TEMPLATE = /\bthis\.client\(\s*`((?:[^`\\$]|\$(?!\{))*)`\s*\)/g;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `this.client('…')` / `this.client("…")` with no escape, no backtick and no `${`.
|
|
48
|
+
*
|
|
49
|
+
* `${` matters more here than in the template case: promoting a quoted string to a
|
|
50
|
+
* template literal turns a literal `${…}` into an interpolation, which is a change
|
|
51
|
+
* in meaning rather than in syntax.
|
|
52
|
+
*/
|
|
53
|
+
const PLAIN_STRING = /\bthis\.client\(\s*(['"])((?:(?!\1)(?:[^\\`$]|\$(?!\{)))*)\1\s*\)/g;
|
|
54
|
+
|
|
55
|
+
/** Any remaining `this.client(`, for the handover pass. */
|
|
56
|
+
const ANY_CALL = /\bthis\.client\(/;
|
|
57
|
+
|
|
58
|
+
export const clientTaggedTemplate: Codemod = {
|
|
59
|
+
version: "1.13.0",
|
|
60
|
+
name: "client-tagged-template",
|
|
61
|
+
description: "Rewrite this.client(…) to the $ tagged template, which encodes interpolations",
|
|
62
|
+
ledger: 5,
|
|
63
|
+
|
|
64
|
+
run(files: SourceFile[]): CodemodResult {
|
|
65
|
+
const changes: Change[] = [];
|
|
66
|
+
const manual: Manual[] = [];
|
|
67
|
+
|
|
68
|
+
for (const { file, contents } of files) {
|
|
69
|
+
let next = contents;
|
|
70
|
+
const notes: string[] = [];
|
|
71
|
+
|
|
72
|
+
const templateHits = next.match(PLAIN_TEMPLATE)?.length ?? 0;
|
|
73
|
+
if (templateHits > 0) {
|
|
74
|
+
next = next.replace(PLAIN_TEMPLATE, (_m, body: string) => `this.$\`${body}\``);
|
|
75
|
+
notes.push(`${templateHits} × \`client(\\\`…\\\`)\` → \`$\\\`…\\\`\``);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stringHits = next.match(PLAIN_STRING)?.length ?? 0;
|
|
79
|
+
if (stringHits > 0) {
|
|
80
|
+
// The body carried no backtick or `${`, so promoting the quotes cannot
|
|
81
|
+
// change what the expression means.
|
|
82
|
+
next = next.replace(PLAIN_STRING, (_m, _q: string, body: string) => `this.$\`${body}\``);
|
|
83
|
+
notes.push(`${stringHits} × \`client("…")\` → \`$\\\`…\\\`\``);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Whatever is left took an argument this cannot read.
|
|
87
|
+
next.split("\n").forEach((line, i) => {
|
|
88
|
+
if (!ANY_CALL.test(line)) return;
|
|
89
|
+
manual.push({
|
|
90
|
+
file,
|
|
91
|
+
line: i + 1,
|
|
92
|
+
text: line.trim(),
|
|
93
|
+
reason:
|
|
94
|
+
"`client()` with an argument that is not a single literal — a variable, a " +
|
|
95
|
+
"concatenation, a call. This is the shape the security note was about, so it is " +
|
|
96
|
+
"worth reading rather than rewriting: if the string is built from user input, the " +
|
|
97
|
+
"fix is to interpolate through `$` so the value is encoded, not to wrap the " +
|
|
98
|
+
"finished string. Wrapping it as `` $`${expr}` `` would encode the whole thing as a " +
|
|
99
|
+
"string literal and stop running it as code.",
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (next !== contents) changes.push({ file, summary: notes.join(", "), contents: next });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { changes, manual };
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Codemod } from "../types.ts";
|
|
11
11
|
import { deprecatedAliases } from "./deprecated-aliases.ts";
|
|
12
|
+
import { clientTaggedTemplate } from "./client-tagged-template.ts";
|
|
12
13
|
|
|
13
|
-
export const CODEMODS: Codemod[] = [deprecatedAliases];
|
|
14
|
+
export const CODEMODS: Codemod[] = [clientTaggedTemplate, deprecatedAliases];
|
|
14
15
|
|
|
15
|
-
export { deprecatedAliases };
|
|
16
|
+
export { deprecatedAliases, clientTaggedTemplate };
|