@ternkit/tern 0.0.0-stage → 0.4.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.
@@ -0,0 +1,101 @@
1
+ import type { Component } from 'svelte';
2
+ /** A lazily imported Svelte component module. */
3
+ export type ComponentLoader = () => Promise<{
4
+ default: Component<any>;
5
+ }>;
6
+ export interface LayoutEntry {
7
+ dir: string;
8
+ component: ComponentLoader | null;
9
+ hasLoad: boolean;
10
+ }
11
+ export interface ErrorEntry {
12
+ /** How many of the route's layouts wrap this error page. */
13
+ layouts: number;
14
+ component: ComponentLoader;
15
+ }
16
+ export interface RouteEntry {
17
+ layouts: number[];
18
+ page: ComponentLoader;
19
+ hasLoad: boolean;
20
+ errors: ErrorEntry[];
21
+ }
22
+ /** The client manifest generated by `tern gen` in .tern/generated/manifest.js. */
23
+ export interface ClientManifest {
24
+ layouts: LayoutEntry[];
25
+ routes: Record<string, RouteEntry>;
26
+ fallback: {
27
+ layouts: number[];
28
+ errors: ErrorEntry[];
29
+ };
30
+ }
31
+ /** What the server sends for a page, inline or from __data.json. */
32
+ export interface Payload {
33
+ type: 'data' | 'error' | 'redirect' | 'success' | 'failure';
34
+ route?: string;
35
+ params?: Record<string, string>;
36
+ status: number;
37
+ nodes?: (Record<string, unknown> | null)[];
38
+ failed?: number;
39
+ error?: {
40
+ message: string;
41
+ };
42
+ location?: string;
43
+ data?: unknown;
44
+ form?: unknown;
45
+ hasForm?: boolean;
46
+ /** What each node's load read, so the client knows when to rerun it. */
47
+ uses?: (NodeUses | null)[];
48
+ /** Nodes the server skipped because the client already has their data. */
49
+ reused?: number[];
50
+ }
51
+ export interface NodeUses {
52
+ url?: boolean;
53
+ params?: string[];
54
+ deps?: string[];
55
+ parent?: boolean;
56
+ }
57
+ /** The result of submitting a form action with `use:enhance`. */
58
+ export type ActionResult<Success = Record<string, unknown>, Failure = Record<string, unknown>> = {
59
+ type: 'success';
60
+ status: number;
61
+ data?: Success;
62
+ } | {
63
+ type: 'failure';
64
+ status: number;
65
+ data?: Failure;
66
+ } | {
67
+ type: 'redirect';
68
+ status: number;
69
+ location: string;
70
+ } | {
71
+ type: 'error';
72
+ status: number;
73
+ error: {
74
+ message: string;
75
+ };
76
+ };
77
+ export interface NavigationTarget {
78
+ url: URL;
79
+ params: Record<string, string> | null;
80
+ route: {
81
+ id: string | null;
82
+ };
83
+ }
84
+ export type NavigationType = 'enter' | 'link' | 'goto' | 'popstate' | 'form' | 'leave';
85
+ export interface Navigation {
86
+ from: NavigationTarget | null;
87
+ to: NavigationTarget | null;
88
+ type: NavigationType;
89
+ }
90
+ export interface GotoOptions {
91
+ /** Replace the current history entry instead of adding one. */
92
+ replaceState?: boolean;
93
+ /** Keep the scroll position instead of scrolling to the top. */
94
+ noScroll?: boolean;
95
+ /** Keep focus on the currently focused element. */
96
+ keepFocus?: boolean;
97
+ /** Rerun every load, even for the current URL. */
98
+ invalidateAll?: boolean;
99
+ /** State to store in the history entry, available as page.state. */
100
+ state?: Record<string, unknown>;
101
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import type { Component } from 'svelte';
2
+ /** The server manifest generated by `tern gen` in .tern/generated/server.js. */
3
+ export interface ServerManifest {
4
+ layouts: {
5
+ component: Component<any> | null;
6
+ }[];
7
+ routes: Record<string, {
8
+ layouts: number[];
9
+ page: Component<any>;
10
+ errors: {
11
+ layouts: number;
12
+ component: Component<any>;
13
+ }[];
14
+ }>;
15
+ fallback: {
16
+ layouts: number[];
17
+ errors: {
18
+ layouts: number;
19
+ component: Component<any>;
20
+ }[];
21
+ };
22
+ }
23
+ /** The result of a server render, as JSON for the Go side. */
24
+ export interface RenderResult {
25
+ head?: string;
26
+ body?: string;
27
+ /** True when the payload is not something the server renders. */
28
+ skip?: boolean;
29
+ }
30
+ /**
31
+ * Creates the function the Go server calls to render a page. It takes the
32
+ * same payload the client receives, so the server and the client render
33
+ * exactly the same tree and hydration matches.
34
+ */
35
+ export declare function createRenderer(manifest: ServerManifest): (payload: string, href: string) => string;
@@ -0,0 +1,40 @@
1
+ import { render } from 'svelte/server';
2
+ import DefaultError from '../runtime/DefaultError.svelte';
3
+ import Passthrough from '../runtime/Passthrough.svelte';
4
+ import Root from '../runtime/Root.svelte';
5
+ import { revive } from '../runtime/deferred.js';
6
+ import { plan } from '../runtime/select.js';
7
+ import { applyPayloadToPage } from '../runtime/state.svelte.js';
8
+ /**
9
+ * Creates the function the Go server calls to render a page. It takes the
10
+ * same payload the client receives, so the server and the client render
11
+ * exactly the same tree and hydration matches.
12
+ */
13
+ export function createRenderer(manifest) {
14
+ const layout = (i) => manifest.layouts[i]?.component ?? Passthrough;
15
+ return (payloadJSON, href) => {
16
+ const p = JSON.parse(payloadJSON);
17
+ // Deferred values are still loading: render their pending state.
18
+ p.nodes = revive(p.nodes, () => new Promise(() => { }));
19
+ const what = plan(manifest, p);
20
+ let components;
21
+ switch (what.kind) {
22
+ case 'not-a-page':
23
+ return JSON.stringify({ skip: true });
24
+ case 'page':
25
+ components = [...what.layouts.map(layout), manifest.routes[what.route].page];
26
+ break;
27
+ case 'error': {
28
+ const errors = what.route ? manifest.routes[what.route].errors : manifest.fallback.errors;
29
+ components = [...what.layouts.map(layout), errors[what.errorIndex].component];
30
+ break;
31
+ }
32
+ case 'default-error':
33
+ components = [DefaultError];
34
+ }
35
+ applyPayloadToPage(p, new URL(href), {}, what.data);
36
+ const form = p.hasForm ? (p.form ?? null) : null;
37
+ const out = render(Root, { props: { components, data: what.data, form } });
38
+ return JSON.stringify({ head: out.head, body: out.body });
39
+ };
40
+ }
@@ -0,0 +1,100 @@
1
+ type Opts = Record<string, unknown>;
2
+ declare class NumberFormat {
3
+ #private;
4
+ constructor(_locale?: unknown, options?: Opts);
5
+ format(value: number | bigint): string;
6
+ formatToParts(value: number): {
7
+ type: string;
8
+ value: string;
9
+ }[];
10
+ resolvedOptions(): {
11
+ locale: string;
12
+ numberingSystem: string;
13
+ };
14
+ static supportedLocalesOf(): string[];
15
+ }
16
+ declare class DateTimeFormat {
17
+ #private;
18
+ constructor(_locale?: unknown, options?: Opts);
19
+ format(value?: Date | number): string;
20
+ formatToParts(value?: Date | number): {
21
+ type: string;
22
+ value: string;
23
+ }[];
24
+ formatRange(a: Date | number, b: Date | number): string;
25
+ resolvedOptions(): {
26
+ locale: string;
27
+ calendar: string;
28
+ numberingSystem: string;
29
+ timeZone: string;
30
+ };
31
+ static supportedLocalesOf(): string[];
32
+ }
33
+ declare class PluralRules {
34
+ select(n: number): "one" | "other";
35
+ resolvedOptions(): {
36
+ locale: string;
37
+ pluralCategories: string[];
38
+ };
39
+ static supportedLocalesOf(): string[];
40
+ }
41
+ declare class Collator {
42
+ #private;
43
+ constructor(_locale?: unknown, options?: Opts);
44
+ compare: (a: string, b: string) => number;
45
+ resolvedOptions(): {
46
+ locale: string;
47
+ usage: string;
48
+ sensitivity: string;
49
+ };
50
+ static supportedLocalesOf(): string[];
51
+ }
52
+ declare class RelativeTimeFormat {
53
+ format(value: number, unit: string): string;
54
+ static supportedLocalesOf(): string[];
55
+ }
56
+ declare class ListFormat {
57
+ #private;
58
+ constructor(_locale?: unknown, options?: Opts);
59
+ format(list: Iterable<string>): string;
60
+ static supportedLocalesOf(): string[];
61
+ }
62
+ declare class Locale {
63
+ baseName: string;
64
+ language: string;
65
+ region?: string;
66
+ constructor(tag?: string);
67
+ get weekInfo(): {
68
+ firstDay: number;
69
+ weekend: number[];
70
+ minimalDays: number;
71
+ };
72
+ getWeekInfo(): {
73
+ firstDay: number;
74
+ weekend: number[];
75
+ minimalDays: number;
76
+ };
77
+ maximize(): this;
78
+ minimize(): this;
79
+ toString(): string;
80
+ }
81
+ declare class Segmenter {
82
+ segment(input: string): {
83
+ segment: string;
84
+ index: number;
85
+ input: string;
86
+ }[];
87
+ }
88
+ export declare const IntlShim: {
89
+ NumberFormat: typeof NumberFormat;
90
+ DateTimeFormat: typeof DateTimeFormat;
91
+ PluralRules: typeof PluralRules;
92
+ Collator: typeof Collator;
93
+ RelativeTimeFormat: typeof RelativeTimeFormat;
94
+ ListFormat: typeof ListFormat;
95
+ Locale: typeof Locale;
96
+ Segmenter: typeof Segmenter;
97
+ getCanonicalLocales: (l?: string | string[]) => string[];
98
+ supportedValuesOf: () => never[];
99
+ };
100
+ export {};
@@ -0,0 +1,222 @@
1
+ // A small Intl for the server renderer. The embedded engine (QuickJS) has
2
+ // no Intl, and many libraries create formatters when they load. This shim
3
+ // formats like en-US, which is good enough for server-rendered HTML: the
4
+ // browser hydrates with the user's real Intl and locale.
5
+ const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
6
+ const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
7
+ function group(int) {
8
+ return int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
9
+ }
10
+ class NumberFormat {
11
+ #o;
12
+ constructor(_locale, options = {}) {
13
+ this.#o = options;
14
+ }
15
+ format(value) {
16
+ let n = Number(value);
17
+ const o = this.#o;
18
+ if (!Number.isFinite(n))
19
+ return Number.isNaN(n) ? 'NaN' : n > 0 ? '∞' : '-∞';
20
+ let suffix = '';
21
+ if (o.style === 'percent') {
22
+ n *= 100;
23
+ suffix = '%';
24
+ }
25
+ if (o.notation === 'compact') {
26
+ const abs = Math.abs(n);
27
+ for (const [limit, s] of [[1e12, 'T'], [1e9, 'B'], [1e6, 'M'], [1e3, 'K']]) {
28
+ if (abs >= limit) {
29
+ n /= limit;
30
+ suffix = s + suffix;
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ const isCurrency = o.style === 'currency';
36
+ const max = o.maximumFractionDigits ?? (isCurrency ? 2 : o.notation === 'compact' ? 1 : 3);
37
+ const min = o.minimumFractionDigits ?? (isCurrency ? 2 : 0);
38
+ let s = Math.abs(n).toFixed(Math.max(min, Math.min(max, 20)));
39
+ if (s.includes('.')) {
40
+ let [int, frac = ''] = s.split('.');
41
+ while (frac.length > min && frac.endsWith('0'))
42
+ frac = frac.slice(0, -1);
43
+ s = frac ? `${int}.${frac}` : int;
44
+ }
45
+ const [int, frac] = s.split('.');
46
+ s = (o.useGrouping === false ? int : group(int)) + (frac ? `.${frac}` : '');
47
+ if (isCurrency) {
48
+ const code = String(o.currency ?? 'USD').toUpperCase();
49
+ s = (code === 'USD' ? '$' : code === 'EUR' ? '€' : code === 'GBP' ? '£' : `${code} `) + s;
50
+ }
51
+ return (n < 0 ? '-' : '') + s + suffix;
52
+ }
53
+ formatToParts(value) {
54
+ return [{ type: 'literal', value: this.format(value) }];
55
+ }
56
+ resolvedOptions() {
57
+ return { locale: 'en-US', numberingSystem: 'latn', ...this.#o };
58
+ }
59
+ static supportedLocalesOf() {
60
+ return ['en-US'];
61
+ }
62
+ }
63
+ class DateTimeFormat {
64
+ #o;
65
+ constructor(_locale, options = {}) {
66
+ this.#o = options;
67
+ }
68
+ format(value) {
69
+ const d = value === undefined ? new Date() : new Date(value);
70
+ if (Number.isNaN(d.getTime()))
71
+ return 'Invalid Date';
72
+ const o = this.#o;
73
+ const utc = o.timeZone === 'UTC';
74
+ const get = {
75
+ y: utc ? d.getUTCFullYear() : d.getFullYear(),
76
+ m: utc ? d.getUTCMonth() : d.getMonth(),
77
+ d: utc ? d.getUTCDate() : d.getDate(),
78
+ w: utc ? d.getUTCDay() : d.getDay(),
79
+ h: utc ? d.getUTCHours() : d.getHours(),
80
+ min: utc ? d.getUTCMinutes() : d.getMinutes()
81
+ };
82
+ const hasDate = o.year || o.month || o.day || o.weekday || o.dateStyle;
83
+ const hasTime = o.hour || o.minute || o.timeStyle;
84
+ const parts = [];
85
+ if (hasDate || !hasTime) {
86
+ const month = o.month ?? (o.dateStyle ? 'short' : 'numeric');
87
+ if (o.weekday)
88
+ parts.push((o.weekday === 'long' ? DAYS[get.w] : DAYS[get.w].slice(0, 3)) + ',');
89
+ if (month === 'numeric' || month === '2-digit') {
90
+ parts.push(`${get.m + 1}/${get.d}/${get.y}`);
91
+ }
92
+ else {
93
+ const name = month === 'long' ? MONTHS[get.m] : MONTHS[get.m].slice(0, 3);
94
+ const day = o.day !== undefined || o.dateStyle ? ` ${get.d}` : '';
95
+ const year = o.year !== undefined || o.dateStyle ? `, ${get.y}` : '';
96
+ parts.push(name + day + (day ? year : year.replace(', ', ' ')));
97
+ }
98
+ }
99
+ if (hasTime) {
100
+ const h12 = o.hour12 !== false;
101
+ const hour = h12 ? get.h % 12 || 12 : get.h;
102
+ const mm = String(get.min).padStart(2, '0');
103
+ parts.push(`${hour}:${mm}${h12 ? (get.h < 12 ? ' AM' : ' PM') : ''}`);
104
+ }
105
+ return parts.join(' ');
106
+ }
107
+ formatToParts(value) {
108
+ return [{ type: 'literal', value: this.format(value) }];
109
+ }
110
+ formatRange(a, b) {
111
+ return `${this.format(a)} – ${this.format(b)}`;
112
+ }
113
+ resolvedOptions() {
114
+ return { locale: 'en-US', calendar: 'gregory', numberingSystem: 'latn', timeZone: 'UTC', ...this.#o };
115
+ }
116
+ static supportedLocalesOf() {
117
+ return ['en-US'];
118
+ }
119
+ }
120
+ class PluralRules {
121
+ select(n) {
122
+ return n === 1 ? 'one' : 'other';
123
+ }
124
+ resolvedOptions() {
125
+ return { locale: 'en-US', pluralCategories: ['one', 'other'] };
126
+ }
127
+ static supportedLocalesOf() {
128
+ return ['en-US'];
129
+ }
130
+ }
131
+ class Collator {
132
+ #numeric;
133
+ constructor(_locale, options = {}) {
134
+ this.#numeric = options.numeric === true;
135
+ }
136
+ compare = (a, b) => {
137
+ if (this.#numeric) {
138
+ const na = Number(a);
139
+ const nb = Number(b);
140
+ if (!Number.isNaN(na) && !Number.isNaN(nb))
141
+ return na - nb;
142
+ }
143
+ const x = String(a).toLowerCase();
144
+ const y = String(b).toLowerCase();
145
+ return x < y ? -1 : x > y ? 1 : 0;
146
+ };
147
+ resolvedOptions() {
148
+ return { locale: 'en-US', usage: 'sort', sensitivity: 'variant' };
149
+ }
150
+ static supportedLocalesOf() {
151
+ return ['en-US'];
152
+ }
153
+ }
154
+ class RelativeTimeFormat {
155
+ format(value, unit) {
156
+ const u = unit.replace(/s$/, '');
157
+ const n = Math.abs(value);
158
+ const label = `${n} ${u}${n === 1 ? '' : 's'}`;
159
+ return value < 0 ? `${label} ago` : `in ${label}`;
160
+ }
161
+ static supportedLocalesOf() {
162
+ return ['en-US'];
163
+ }
164
+ }
165
+ class ListFormat {
166
+ #type;
167
+ constructor(_locale, options = {}) {
168
+ this.#type = options.type === 'disjunction' ? 'or' : 'and';
169
+ }
170
+ format(list) {
171
+ const items = [...list];
172
+ if (items.length < 3)
173
+ return items.join(` ${this.#type} `);
174
+ return `${items.slice(0, -1).join(', ')}, ${this.#type} ${items[items.length - 1]}`;
175
+ }
176
+ static supportedLocalesOf() {
177
+ return ['en-US'];
178
+ }
179
+ }
180
+ class Locale {
181
+ baseName;
182
+ language;
183
+ region;
184
+ constructor(tag = 'en-US') {
185
+ this.baseName = String(tag);
186
+ const [language, region] = this.baseName.split(/[-_]/);
187
+ this.language = language || 'en';
188
+ this.region = region;
189
+ }
190
+ get weekInfo() {
191
+ return this.getWeekInfo();
192
+ }
193
+ getWeekInfo() {
194
+ return { firstDay: 7, weekend: [6, 7], minimalDays: 1 };
195
+ }
196
+ maximize() {
197
+ return this;
198
+ }
199
+ minimize() {
200
+ return this;
201
+ }
202
+ toString() {
203
+ return this.baseName;
204
+ }
205
+ }
206
+ class Segmenter {
207
+ segment(input) {
208
+ return [...input].map((segment, index) => ({ segment, index, input }));
209
+ }
210
+ }
211
+ export const IntlShim = {
212
+ NumberFormat,
213
+ DateTimeFormat,
214
+ PluralRules,
215
+ Collator,
216
+ RelativeTimeFormat,
217
+ ListFormat,
218
+ Locale,
219
+ Segmenter,
220
+ getCanonicalLocales: (l) => (l === undefined ? [] : Array.isArray(l) ? l : [l]),
221
+ supportedValuesOf: () => []
222
+ };
@@ -0,0 +1,2 @@
1
+ import 'core-js/actual/url';
2
+ import 'core-js/actual/url-search-params';
@@ -0,0 +1,47 @@
1
+ // Web APIs the server-side renderer needs. Tern renders in an embedded
2
+ // JavaScript engine (QuickJS) that only implements the language itself.
3
+ // Each polyfill is skipped where the API already exists, such as in Node
4
+ // during development.
5
+ import 'core-js/actual/url';
6
+ import 'core-js/actual/url-search-params';
7
+ import { IntlShim } from './intl.js';
8
+ const g = globalThis;
9
+ // Some libraries read process.env.SOMETHING at runtime. Give them an empty
10
+ // environment instead of a ReferenceError.
11
+ if (typeof g.process === 'undefined') {
12
+ g.process = { env: { NODE_ENV: 'production' }, browser: false, version: '', versions: {} };
13
+ }
14
+ if (typeof g.Intl === 'undefined') {
15
+ g.Intl = IntlShim;
16
+ }
17
+ if (typeof g.queueMicrotask !== 'function') {
18
+ g.queueMicrotask = (fn) => void Promise.resolve().then(fn);
19
+ }
20
+ if (typeof g.console === 'undefined') {
21
+ const format = (args) => args
22
+ .map((a) => {
23
+ if (typeof a === 'string')
24
+ return a;
25
+ if (a instanceof Error)
26
+ return a.stack ?? a.message;
27
+ try {
28
+ return JSON.stringify(a);
29
+ }
30
+ catch {
31
+ return String(a);
32
+ }
33
+ })
34
+ .join(' ');
35
+ const log = (level) => (...args) => {
36
+ if (typeof __tern_log === 'function')
37
+ __tern_log(level, format(args));
38
+ };
39
+ g.console = {
40
+ log: log('info'),
41
+ info: log('info'),
42
+ debug: log('debug'),
43
+ warn: log('warn'),
44
+ error: log('error'),
45
+ trace: log('debug')
46
+ };
47
+ }
@@ -0,0 +1,23 @@
1
+ import { type Options as SvelteOptions } from '@sveltejs/vite-plugin-svelte';
2
+ import type { Plugin } from 'vite';
3
+ export interface TernOptions {
4
+ /** Options passed to @sveltejs/vite-plugin-svelte. */
5
+ svelte?: Partial<SvelteOptions>;
6
+ }
7
+ /**
8
+ * The Vite plugin for Tern apps. It includes the Svelte plugin, so it is
9
+ * the only plugin a Tern app needs:
10
+ *
11
+ * ```js
12
+ * // vite.config.js
13
+ * import { tern } from '@ternkit/tern/vite';
14
+ * import { defineConfig } from 'vite';
15
+ *
16
+ * export default defineConfig({ plugins: [tern()] });
17
+ * ```
18
+ *
19
+ * `vite build` builds the client; `vite build --ssr` builds the server
20
+ * renderer that `tern build` embeds in the Go binary.
21
+ */
22
+ export declare function tern(options?: TernOptions): Plugin[];
23
+ export default tern;