@zerotal/core 1.8.1 → 1.9.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 +87 -0
- package/api-surface.md +56 -355
- package/package.json +2 -1
- package/src/application/Application.ts +6 -0
- package/src/application/currentApp.ts +7 -1
- package/src/command/OutputWriter.ts +5 -1
- package/src/command/builtin/DeployCommand.ts +72 -1
- package/src/command/builtin/TestCommand.ts +9 -2
- package/src/command/startZerotal.ts +34 -0
- package/src/config/ConfigLoader.ts +5 -1
- package/src/config/DeployConfig.ts +33 -0
- package/src/config/index.ts +6 -1
- package/src/config/registry.ts +5 -1
- package/src/config/validation.ts +10 -2
- package/src/conventions/ConventionLoader.ts +5 -1
- package/src/dev/CssPlugins.ts +11 -1
- package/src/dev/DevBuildHook.ts +10 -2
- package/src/dev/DevDeck.ts +6 -0
- package/src/dev/DevOrchestrator.ts +2 -0
- package/src/dev/DevProcess.ts +17 -3
- package/src/dev/DevReloadMiddleware.ts +7 -1
- package/src/dev/DevSupervisor.ts +22 -4
- package/src/dev/bootBuild.ts +9 -1
- package/src/dev/reloadClient.ts +2 -0
- package/src/dev/startDevMode.ts +7 -1
- package/src/doctor/AppDoctor.ts +43 -1
- package/src/doctor/throttleIdentity.ts +114 -0
- package/src/errors/RuntimeMismatchError.ts +21 -0
- package/src/errors/index.ts +1 -0
- package/src/events/Emitter.ts +5 -1
- package/src/helpers/html.ts +2 -0
- package/src/helpers/markdown.ts +7 -1
- package/src/helpers/pageElements.ts +2 -0
- package/src/http/HttpClient.ts +5 -1
- package/src/http/Uri.ts +5 -1
- package/src/http/negotiate.ts +5 -1
- package/src/http/originGuard.ts +2 -0
- package/src/http/sniffContentType.ts +7 -1
- package/src/macros/config.macro.ts +2 -0
- package/src/metrics/HttpMetrics.ts +10 -2
- package/src/middleware/BaseMiddleware.ts +20 -0
- package/src/pipeline/ContextRegistry.ts +12 -2
- package/src/pipeline/HttpContext.ts +4 -0
- package/src/pipeline/currentPage.ts +4 -0
- package/src/pipeline/types.ts +2 -0
- package/src/router/FileRouter.ts +20 -2
- package/src/router/Route.ts +3 -0
- package/src/router/Router.ts +2 -0
- package/src/router/domain.ts +1 -0
- package/src/router/registry.ts +12 -2
- package/src/shared/format.ts +135 -0
- package/src/shared/index.ts +34 -0
- package/src/storage/StorageFilesMiddleware.ts +2 -0
- package/src/support/cookie.ts +7 -1
- package/src/support/runtime.ts +136 -0
- package/src/view/FileRouteResolver.ts +2 -0
- package/src/view/jsx-runtime.ts +4 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting both sides of the wire can agree on.
|
|
3
|
+
*
|
|
4
|
+
* The problem this solves is not that formatting is hard. `Intl` is in every
|
|
5
|
+
* runtime the framework targets and does the work. The problem is that a server
|
|
6
|
+
* helper and a browser helper are two places to make the same decision, and a
|
|
7
|
+
* total that reads `R 39 147` on screen and `R39,147.00` on the invoice looks like
|
|
8
|
+
* two different numbers to the person paying it.
|
|
9
|
+
*
|
|
10
|
+
* So these are deliberately thin — they fix the options, not the arithmetic. The
|
|
11
|
+
* value is that there is one definition of "how this app writes money" and both
|
|
12
|
+
* the controller and the component import it.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is pure and dependency-free, and this module is reachable from
|
|
15
|
+
* `zerotal/shared`, so it bundles into a browser build without pulling the
|
|
16
|
+
* framework in behind it.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** How a value should be written. */
|
|
22
|
+
export interface FormatOptions {
|
|
23
|
+
/**
|
|
24
|
+
* BCP-47 locale. Defaults to the runtime's — which on a server is the machine's
|
|
25
|
+
* and in a browser is the reader's, and those are not the same. Pass one
|
|
26
|
+
* explicitly wherever the two sides must match, and they usually must.
|
|
27
|
+
*/
|
|
28
|
+
locale?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** How money should be written. */
|
|
32
|
+
export interface MoneyOptions extends FormatOptions {
|
|
33
|
+
/** ISO 4217 code — `ZAR`, `USD`, `EUR`. */
|
|
34
|
+
currency: string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether the amount is in the currency's minor unit (cents), which is how a
|
|
37
|
+
* database column that must not lose a cent stores it. Default `true`, because
|
|
38
|
+
* an app that stores money in a float has a different problem.
|
|
39
|
+
*/
|
|
40
|
+
minorUnits?: boolean;
|
|
41
|
+
/** Digits after the decimal point. Defaults to whatever the currency uses. */
|
|
42
|
+
fractionDigits?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write an amount of money.
|
|
47
|
+
*
|
|
48
|
+
* @param amount - The amount, in minor units unless `minorUnits: false`.
|
|
49
|
+
* @param options - Currency, and how to write it.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* formatMoney(3_914_700, { currency: "ZAR", locale: "en-ZA" }); // "R 39 147,00"
|
|
54
|
+
* formatMoney(39_147, { currency: "USD", minorUnits: false }); // "$39,147.00"
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function formatMoney(amount: number, options: MoneyOptions): string {
|
|
58
|
+
const { currency, locale, minorUnits = true, fractionDigits } = options;
|
|
59
|
+
const value = minorUnits ? amount / 100 : amount;
|
|
60
|
+
return new Intl.NumberFormat(locale, {
|
|
61
|
+
style: "currency",
|
|
62
|
+
currency,
|
|
63
|
+
...(fractionDigits !== undefined
|
|
64
|
+
? { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }
|
|
65
|
+
: {}),
|
|
66
|
+
}).format(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** How a plain number should be written. */
|
|
70
|
+
export interface NumberOptions extends FormatOptions {
|
|
71
|
+
/** Smallest number of digits after the decimal point. */
|
|
72
|
+
minimumFractionDigits?: number;
|
|
73
|
+
/** Largest number of digits after the decimal point. */
|
|
74
|
+
maximumFractionDigits?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Write a number with the reader's group and decimal separators.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* formatNumber(39147.5, { locale: "en-GB", maximumFractionDigits: 1 }); // "39,147.5"
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function formatNumber(value: number, options: NumberOptions = {}): string {
|
|
86
|
+
const { locale, ...rest } = options;
|
|
87
|
+
return new Intl.NumberFormat(locale, rest).format(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** How a date should be written. */
|
|
91
|
+
export interface DateOptions extends FormatOptions {
|
|
92
|
+
/** Length of the date part. Omit for none. */
|
|
93
|
+
dateStyle?: "full" | "long" | "medium" | "short";
|
|
94
|
+
/** Length of the time part. Omit for none. */
|
|
95
|
+
timeStyle?: "full" | "long" | "medium" | "short";
|
|
96
|
+
/**
|
|
97
|
+
* IANA zone — `Africa/Johannesburg`, `UTC`.
|
|
98
|
+
*
|
|
99
|
+
* Worth passing on the server. A machine set to UTC and a reader in Cape Town
|
|
100
|
+
* disagree about which day an 11pm booking happened on, and that is the shape
|
|
101
|
+
* the bug takes: not a wrong time, a wrong date.
|
|
102
|
+
*/
|
|
103
|
+
timeZone?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Write a date or timestamp.
|
|
108
|
+
*
|
|
109
|
+
* @param value - A `Date`, an epoch-milliseconds number, or an ISO string.
|
|
110
|
+
* @param options - Which parts to write, and in whose zone.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* formatDate("2026-08-28T21:00:00Z", {
|
|
115
|
+
* locale: "en-ZA",
|
|
116
|
+
* dateStyle: "medium",
|
|
117
|
+
* timeZone: "Africa/Johannesburg",
|
|
118
|
+
* });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function formatDate(value: Date | number | string, options: DateOptions = {}): string {
|
|
122
|
+
const { locale, ...rest } = options;
|
|
123
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
124
|
+
if (Number.isNaN(date.getTime())) return "";
|
|
125
|
+
// `Intl` given neither style writes the date and nothing else, which is the
|
|
126
|
+
// useful default for a helper named `formatDate`. Spread rather than replaced:
|
|
127
|
+
// the default has to keep `timeZone`, or a caller who passed a zone and no style
|
|
128
|
+
// silently gets the machine's — which is the exact wrong-day bug the option is
|
|
129
|
+
// there to prevent.
|
|
130
|
+
const noStyle = rest.dateStyle === undefined && rest.timeStyle === undefined;
|
|
131
|
+
return new Intl.DateTimeFormat(locale, {
|
|
132
|
+
...rest,
|
|
133
|
+
...(noStyle ? { dateStyle: "medium" as const } : {}),
|
|
134
|
+
}).format(date);
|
|
135
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The helpers that are safe to import from a browser bundle.
|
|
3
|
+
*
|
|
4
|
+
* Every function reachable from this module is pure: no `node:` imports, no `Bun`
|
|
5
|
+
* globals, no config, no container, no request context. Importing it from a
|
|
6
|
+
* component pulls in these functions and nothing else — the framework does not
|
|
7
|
+
* come with them.
|
|
8
|
+
*
|
|
9
|
+
* **Why this exists.** Without it, an app that wants the framework's `pluralize`
|
|
10
|
+
* on a page has two options, and both are bad. Importing `zerotal` into the client
|
|
11
|
+
* bundle drags the server in. Writing a second implementation means maintaining
|
|
12
|
+
* the same rule twice — and the second copy is always the worse one, because the
|
|
13
|
+
* irregulars and the inflect-the-last-word behaviour are exactly what somebody
|
|
14
|
+
* re-deriving it by hand leaves out. `"supplier line"` pluralises to
|
|
15
|
+
* `"supplier lines"`; the naive rule gives `"suppliers line"`.
|
|
16
|
+
*
|
|
17
|
+
* The same argument applies to money. Two formatters that must agree are a bug
|
|
18
|
+
* waiting for a rounding difference, and the person who finds it is the one paying
|
|
19
|
+
* the invoice.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // resources/js/pages/Trips/Index.tsx — a browser bundle
|
|
24
|
+
* import { pluralize, formatMoney } from "zerotal/shared";
|
|
25
|
+
*
|
|
26
|
+
* <p>{trips.length} {pluralize("trip")} — {formatMoney(total, { currency: "ZAR" })}</p>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @packageDocumentation
|
|
30
|
+
*/
|
|
31
|
+
export { pluralize, singularize, snakeCase, camelCase, tableNameFor } from "../support/str.ts";
|
|
32
|
+
export { Str } from "../helpers/str.ts";
|
|
33
|
+
export { formatMoney, formatNumber, formatDate } from "./format.ts";
|
|
34
|
+
export type { FormatOptions, MoneyOptions, NumberOptions, DateOptions } from "./format.ts";
|
|
@@ -132,6 +132,8 @@ export class StorageFilesMiddleware extends BaseMiddleware<StorageFilesOptions>
|
|
|
132
132
|
* accepts a request rather than the first time someone guesses a path.
|
|
133
133
|
*
|
|
134
134
|
* @throws {@link UnsafePublicMountError}
|
|
135
|
+
*
|
|
136
|
+
* @internal
|
|
135
137
|
*/
|
|
136
138
|
export function mountsFrom(config: StorageConfigShape): Mount[] {
|
|
137
139
|
const mounts: Mount[] = [];
|
package/src/support/cookie.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface CookieOptions {
|
|
|
35
35
|
* @example
|
|
36
36
|
* buildCookie({ name: "session", value, maxAge: 86400, secure: true });
|
|
37
37
|
* // "session=…; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400; Secure"
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
38
40
|
*/
|
|
39
41
|
export function buildCookie(options: CookieOptions): string {
|
|
40
42
|
const parts = [`${options.name}=${options.value}`, `Path=${options.path ?? "/"}`];
|
|
@@ -59,7 +61,11 @@ export function readCookie(request: Request, name: string): string | undefined {
|
|
|
59
61
|
return parseCookieHeader(request.headers.get("Cookie") ?? "", name);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Extract a single cookie value from a raw `Cookie` header string.
|
|
66
|
+
*
|
|
67
|
+
* @internal
|
|
68
|
+
*/
|
|
63
69
|
export function parseCookieHeader(header: string, name: string): string | undefined {
|
|
64
70
|
for (const part of header.split(";")) {
|
|
65
71
|
const eqIdx = part.indexOf("=");
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One project, one Bun.
|
|
3
|
+
*
|
|
4
|
+
* `engines.bun` is a floor, and nothing enforces it. A project can therefore end
|
|
5
|
+
* up with two runtimes in play at once — the shell's `bun` and the one sitting in
|
|
6
|
+
* `node_modules/bun`, put there by a transitive peer dependency nobody declared —
|
|
7
|
+
* and split its work between them: the server served by one, the suite run by the
|
|
8
|
+
* other. Nothing announces that. The suite stays green, and it is green about a
|
|
9
|
+
* runtime the app is not served by.
|
|
10
|
+
*
|
|
11
|
+
* What makes it expensive is that the difference is real but narrow. The SQLite
|
|
12
|
+
* bindings, `node:` compatibility and the test runner itself all differ between
|
|
13
|
+
* releases, so a handful of assertions happen to be runtime-sensitive and the rest
|
|
14
|
+
* are not. When two of them fail you go looking for a bug in the code they touch,
|
|
15
|
+
* because nothing in the failure says "different binary". And a suite that passes
|
|
16
|
+
* is not evidence: it only means no test happened to stand on a difference.
|
|
17
|
+
*
|
|
18
|
+
* This module is the check. It is not a pin — the version to agree on is whichever
|
|
19
|
+
* one the project installed, so `bun update bun` moves it and nothing here needs
|
|
20
|
+
* editing. What it enforces is that there is only one.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
|
|
27
|
+
/** Set to `1`/`true` to downgrade a runtime mismatch from a refusal to a warning. */
|
|
28
|
+
export const RUNTIME_MISMATCH_ESCAPE = "ZT_ALLOW_RUNTIME_MISMATCH";
|
|
29
|
+
|
|
30
|
+
/** A running runtime and the installed one it disagrees with. */
|
|
31
|
+
export interface RuntimeMismatch {
|
|
32
|
+
/** `Bun.version` — the binary this process is executing under. */
|
|
33
|
+
running: string;
|
|
34
|
+
/** The version in `node_modules/bun/package.json`. */
|
|
35
|
+
installed: string;
|
|
36
|
+
/** Absolute path of the manifest `installed` was read from. */
|
|
37
|
+
manifest: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The Bun version a project has installed, by walking up from `cwd` looking for
|
|
42
|
+
* `node_modules/bun/package.json`.
|
|
43
|
+
*
|
|
44
|
+
* Walking rather than reading one fixed path because hoisting decides where the
|
|
45
|
+
* package lands: in a workspace it is at the repo root, not beside the app.
|
|
46
|
+
*
|
|
47
|
+
* @param cwd - Directory to start from.
|
|
48
|
+
* @returns `{ version, manifest }`, or `null` when the project does not install
|
|
49
|
+
* Bun as a package — which is the common case and not a finding.
|
|
50
|
+
*/
|
|
51
|
+
export function installedBunVersion(cwd: string): { version: string; manifest: string } | null {
|
|
52
|
+
let dir = cwd;
|
|
53
|
+
// Bounded by the filesystem root: dirname("/") === "/" and dirname("C:\\") === "C:\\".
|
|
54
|
+
for (;;) {
|
|
55
|
+
const manifest = join(dir, "node_modules", "bun", "package.json");
|
|
56
|
+
try {
|
|
57
|
+
const { version } = JSON.parse(readFileSync(manifest, "utf8")) as { version?: string };
|
|
58
|
+
if (version) return { version, manifest };
|
|
59
|
+
} catch {
|
|
60
|
+
// Not here — keep climbing.
|
|
61
|
+
}
|
|
62
|
+
const parent = dirname(dir);
|
|
63
|
+
if (parent === dir) return null;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compare the running runtime against the installed one.
|
|
70
|
+
*
|
|
71
|
+
* The comparison is exact. A patch difference is still two binaries, and the
|
|
72
|
+
* failures this guards against do not respect semver — a change to the SQLite
|
|
73
|
+
* bindings is a patch release to Bun and a behaviour change to an app that stores
|
|
74
|
+
* money in SQLite. "Close enough" is the belief that cost the weeks.
|
|
75
|
+
*
|
|
76
|
+
* @param cwd - Project root to look under. Defaults to the working directory.
|
|
77
|
+
* @returns The mismatch, or `null` when the versions agree or nothing is installed
|
|
78
|
+
* to disagree with.
|
|
79
|
+
*/
|
|
80
|
+
export function runtimeMismatch(cwd: string = process.cwd()): RuntimeMismatch | null {
|
|
81
|
+
const running = typeof Bun === "undefined" ? "" : Bun.version;
|
|
82
|
+
if (!running) return null;
|
|
83
|
+
const installed = installedBunVersion(cwd);
|
|
84
|
+
if (!installed) return null;
|
|
85
|
+
if (installed.version === running) return null;
|
|
86
|
+
return { running, installed: installed.version, manifest: installed.manifest };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The message shown for a mismatch — the two versions, where the second came from,
|
|
91
|
+
* and the two ways out.
|
|
92
|
+
*
|
|
93
|
+
* @param mismatch - The disagreement to describe.
|
|
94
|
+
*/
|
|
95
|
+
export function runtimeMismatchMessage(mismatch: RuntimeMismatch): string {
|
|
96
|
+
return (
|
|
97
|
+
`Two Bun runtimes are in play. This process is Bun ${mismatch.running}, but the ` +
|
|
98
|
+
`project installs Bun ${mismatch.installed} (${mismatch.manifest}).\n\n` +
|
|
99
|
+
` Whichever one is not running this command is still running something else — ` +
|
|
100
|
+
`the server, the suite, a deploy step — and a test that passes under one is not ` +
|
|
101
|
+
`evidence about the other.\n\n` +
|
|
102
|
+
` Fix it by picking one:\n` +
|
|
103
|
+
` bun update bun # move the installed one to match your shell\n` +
|
|
104
|
+
` node_modules/.bin/bun # or run everything through the installed one\n\n` +
|
|
105
|
+
` To boot anyway, set ${RUNTIME_MISMATCH_ESCAPE}=1 — it downgrades this to a warning.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether the escape hatch is set. Deliberately an env var and not a config key:
|
|
111
|
+
* the situation it covers is a project mid-upgrade, where the thing you want is to
|
|
112
|
+
* get one command through, not to write the exception down.
|
|
113
|
+
*/
|
|
114
|
+
export function runtimeMismatchAllowed(): boolean {
|
|
115
|
+
const raw = (globalThis as { Bun?: { env: Record<string, string | undefined> } }).Bun?.env[
|
|
116
|
+
RUNTIME_MISMATCH_ESCAPE
|
|
117
|
+
];
|
|
118
|
+
return raw === "1" || raw === "true";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The Bun binary to spawn a child process with.
|
|
123
|
+
*
|
|
124
|
+
* `process.execPath` rather than `"bun"`, because `"bun"` is resolved by the OS
|
|
125
|
+
* against `PATH` and the parent process was not necessarily started from `PATH`.
|
|
126
|
+
* A command that exists to run *this app's* tests, spawning whichever binary the
|
|
127
|
+
* shell happens to offer, is how the suite ends up on a different runtime from the
|
|
128
|
+
* server — and it is invisible, because the child prints a version nobody reads.
|
|
129
|
+
*
|
|
130
|
+
* @returns An absolute path to the running binary, or `"bun"` when there is none to
|
|
131
|
+
* read (a compiled binary, an unusual embed) and PATH is all that is left.
|
|
132
|
+
*/
|
|
133
|
+
export function bunBinary(): string {
|
|
134
|
+
const path = process.execPath;
|
|
135
|
+
return path && path.length > 0 ? path : "bun";
|
|
136
|
+
}
|
|
@@ -23,6 +23,8 @@ function isClass(fn: Function): boolean {
|
|
|
23
23
|
* export that is a view component (or any function tagged via the view marker)
|
|
24
24
|
* is rendered to HTML, wrapped in the nearest `_layout`, and registered as a GET
|
|
25
25
|
* route. Call from a provider's `onRegister()` before file routes are scanned.
|
|
26
|
+
*
|
|
27
|
+
* @internal
|
|
26
28
|
*/
|
|
27
29
|
export function registerViewFileRouteResolver(): void {
|
|
28
30
|
enableFileRouteLayouts();
|
package/src/view/jsx-runtime.ts
CHANGED
|
@@ -7,12 +7,16 @@ import { escapeHtml as escHtml } from "../helpers/html.ts";
|
|
|
7
7
|
* letting the renderer distinguish page/layout components from ordinary
|
|
8
8
|
* functions. Registered via `Symbol.for` so the marker survives across module
|
|
9
9
|
* realms.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
10
12
|
*/
|
|
11
13
|
export const VIEW_COMPONENT_SYMBOL = Symbol.for("zerotal.view.component");
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* String-keyed twin of {@link VIEW_COMPONENT_SYMBOL}. Set alongside the symbol
|
|
15
17
|
* as a fallback for tooling or environments that can't read symbol-keyed props.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
16
20
|
*/
|
|
17
21
|
export const VIEW_COMPONENT_PROP = "__zerotalViewComponent";
|
|
18
22
|
|