@ternkit/tern 1.7.0 → 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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/builtin.js +2 -0
- package/dist/runtime/router.svelte.js +32 -11
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +4 -3
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +80 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -18,4 +18,4 @@ export { default as Image } from './image/Image.svelte';
|
|
|
18
18
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
19
19
|
export { passkeysSupported, registerPasskey, signInWithPasskey, PasskeyError } from './runtime/passkeys.js';
|
|
20
20
|
/** The version of the Tern client runtime. */
|
|
21
|
-
export const VERSION = '1.
|
|
21
|
+
export const VERSION = '1.9.0';
|
package/dist/runtime/builtin.js
CHANGED
|
@@ -16,6 +16,8 @@ export const builtin = {
|
|
|
16
16
|
"tern.accounts.error.passkey": "That passkey didn’t work.",
|
|
17
17
|
"tern.accounts.error.tooMany": "Too many attempts. Try again in a few minutes.",
|
|
18
18
|
"tern.accounts.error.wrongPassword": "That isn’t your current password.",
|
|
19
|
+
"tern.accounts.new_device.subject": "A new sign-in to your account",
|
|
20
|
+
"tern.accounts.new_device.text": "Hello {name},\n\nYour account was just signed in to from a device it hadn’t been used on before: {device}, from {ip}, on {time}.\n\nIf this was you, there’s nothing to do. If it wasn’t, change your password now and sign that device out in your account’s security settings.",
|
|
19
21
|
"tern.accounts.reset.button": "Choose a new password",
|
|
20
22
|
"tern.accounts.reset.subject": "Reset your password",
|
|
21
23
|
"tern.accounts.reset.text": "Hello {name},\n\nSomeone asked to reset your password. If it was you, choose a new one here within an hour:\n{link}\n\nIf it wasn’t you, ignore this email: your password stays as it is.",
|
|
@@ -46,8 +46,15 @@ export async function start(m) {
|
|
|
46
46
|
catch {
|
|
47
47
|
scrollPositions = {};
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// A page loaded into a new history entry (a link from elsewhere, the
|
|
50
|
+
// address bar) has no index yet. It takes its place in the tab's
|
|
51
|
+
// history, and starts at the top: a position saved for that place came
|
|
52
|
+
// from a page the browser has since left behind. A reload, or going
|
|
53
|
+
// back to the page, restores where the reader was.
|
|
54
|
+
const fresh = history.state?.ternIndex == null;
|
|
55
|
+
historyIndex = fresh ? history.length - 1 : history.state.ternIndex;
|
|
56
|
+
if (fresh) {
|
|
57
|
+
delete scrollPositions[historyIndex];
|
|
51
58
|
history.replaceState({ ...history.state, ternIndex: historyIndex, ternState: {} }, '');
|
|
52
59
|
}
|
|
53
60
|
history.scrollRestoration = 'manual';
|
|
@@ -70,14 +77,19 @@ export async function start(m) {
|
|
|
70
77
|
}
|
|
71
78
|
started = true;
|
|
72
79
|
await tick();
|
|
80
|
+
// tern dev puts the page's CSS in the HTML for the first paint; Vite's
|
|
81
|
+
// own style tags, which hot-reload, have replaced it by now.
|
|
82
|
+
if (import.meta.env?.DEV)
|
|
83
|
+
for (const el of document.querySelectorAll('style[data-tern-dev]'))
|
|
84
|
+
el.remove();
|
|
73
85
|
runAfter({ from: null, to: current(), type: 'enter' });
|
|
74
86
|
registerServiceWorker();
|
|
75
|
-
const saved = scrollPositions[historyIndex];
|
|
87
|
+
const saved = fresh ? undefined : scrollPositions[historyIndex];
|
|
76
88
|
if (saved) {
|
|
77
|
-
|
|
89
|
+
jump(saved.x, saved.y);
|
|
78
90
|
}
|
|
79
91
|
else if (url.hash) {
|
|
80
|
-
scrollToHash(url.hash);
|
|
92
|
+
scrollToHash(url.hash, true);
|
|
81
93
|
}
|
|
82
94
|
listen();
|
|
83
95
|
}
|
|
@@ -293,13 +305,13 @@ async function navigate(url, opts, type, redirects, popIndex, fromIndex) {
|
|
|
293
305
|
await tick();
|
|
294
306
|
if (type === 'popstate') {
|
|
295
307
|
const pos = popIndex != null ? scrollPositions[popIndex] : undefined;
|
|
296
|
-
|
|
308
|
+
jump(pos?.x ?? 0, pos?.y ?? 0);
|
|
297
309
|
}
|
|
298
310
|
else if (!opts.noScroll) {
|
|
299
311
|
if (url.hash)
|
|
300
|
-
scrollToHash(url.hash);
|
|
312
|
+
scrollToHash(url.hash, true);
|
|
301
313
|
else
|
|
302
|
-
|
|
314
|
+
jump(0, 0);
|
|
303
315
|
}
|
|
304
316
|
if (!opts.keepFocus) {
|
|
305
317
|
resetFocus();
|
|
@@ -393,13 +405,22 @@ function saveScroll() {
|
|
|
393
405
|
// Storage can be full or disabled; scroll restoration is best effort.
|
|
394
406
|
}
|
|
395
407
|
}
|
|
396
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Scrolls to a position at once. A page change is a jump, even on pages
|
|
410
|
+
* whose CSS asks for smooth scrolling: an animation from the old page's
|
|
411
|
+
* position would be cut short by the new page's layout, and stop halfway.
|
|
412
|
+
*/
|
|
413
|
+
function jump(x, y) {
|
|
414
|
+
scrollTo({ left: x, top: y, behavior: 'instant' });
|
|
415
|
+
}
|
|
416
|
+
/** Scrolls to an anchor: smoothly within the page if the CSS says so, at once when arriving on a new page. */
|
|
417
|
+
function scrollToHash(hash, instant = false) {
|
|
397
418
|
const id = decodeURIComponent(hash.slice(1));
|
|
398
419
|
const el = document.getElementById(id) ?? document.getElementsByName(id)[0];
|
|
399
420
|
if (el)
|
|
400
|
-
el.scrollIntoView();
|
|
421
|
+
el.scrollIntoView(instant ? { behavior: 'instant' } : undefined);
|
|
401
422
|
else
|
|
402
|
-
|
|
423
|
+
jump(0, 0);
|
|
403
424
|
}
|
|
404
425
|
/** Moves focus to the document and announces the new page to screen readers. */
|
|
405
426
|
function resetFocus() {
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Component } from 'svelte';
|
|
2
2
|
import { type I18nConfig } from '../runtime/i18n.svelte.js';
|
|
3
|
+
import type { Payload } from '../runtime/types.js';
|
|
3
4
|
/** The server manifest generated by `tern gen` in .tern/generated/server.js. */
|
|
4
5
|
export interface ServerManifest {
|
|
5
6
|
layouts: {
|
|
@@ -36,7 +37,7 @@ export interface RenderResult {
|
|
|
36
37
|
* exactly the same tree and hydration matches.
|
|
37
38
|
*/
|
|
38
39
|
export declare function createRenderer(manifest: ServerManifest): ((payload: string, href: string) => string) & {
|
|
39
|
-
raw: (payload: string, href: string) => RenderResult;
|
|
40
|
+
raw: (payload: string | Payload, href: string) => RenderResult;
|
|
40
41
|
};
|
|
41
42
|
export { createEmailRenderer } from './email.js';
|
|
42
43
|
export type { EmailModule, EmailResult } from './email.js';
|
package/dist/server/index.js
CHANGED
|
@@ -17,9 +17,10 @@ export function createRenderer(manifest) {
|
|
|
17
17
|
configureI18n(manifest.i18n ?? null);
|
|
18
18
|
setBase(manifest.base ?? '');
|
|
19
19
|
// raw returns the result as an object, for a native renderer to read
|
|
20
|
-
// without encoding it as JSON first.
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// without encoding it as JSON first. Native rendering also passes the
|
|
21
|
+
// payload as an object, made from Go values without the JSON.
|
|
22
|
+
const raw = (payload, href) => {
|
|
23
|
+
const p = typeof payload === 'string' ? JSON.parse(payload) : payload;
|
|
23
24
|
// Deferred values are still loading: render their pending state.
|
|
24
25
|
if (p.deferred)
|
|
25
26
|
p.nodes = revive(p.nodes, () => new Promise(() => { }));
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Options as SvelteOptions } from '@sveltejs/vite-plugin-svelte';
|
|
2
|
-
import type
|
|
2
|
+
import { type Plugin } from 'vite';
|
|
3
3
|
export interface TernOptions {
|
|
4
4
|
/** Options passed to @sveltejs/vite-plugin-svelte. */
|
|
5
5
|
svelte?: Partial<SvelteOptions>;
|
package/dist/vite/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { nativePlugin } from './native.js';
|
|
6
|
+
import { searchForWorkspaceRoot } from 'vite';
|
|
6
7
|
import { goScriptPlugin, goScriptPreprocessor } from './goscript.js';
|
|
7
8
|
import { imagePlugin } from './image.js';
|
|
8
9
|
/** Packages whose browser build works in the server renderer and whose default build needs Node. */
|
|
@@ -17,6 +18,9 @@ const SERVER_OUT = '.tern/output/server';
|
|
|
17
18
|
const RENDER_PATH = '/__tern/render';
|
|
18
19
|
/** The dev endpoint the Go app calls to render an email. */
|
|
19
20
|
const EMAIL_PATH = '/__tern/email';
|
|
21
|
+
// The CSS a page's components import, for tern dev to inline in the
|
|
22
|
+
// server-rendered page, so its first paint is styled.
|
|
23
|
+
const STYLES_PATH = '/__tern/dev/styles';
|
|
20
24
|
/**
|
|
21
25
|
* The Vite plugin for Tern apps. It includes the Svelte plugin, so it is
|
|
22
26
|
* the only plugin a Tern app needs:
|
|
@@ -121,7 +125,10 @@ export function tern(options = {}) {
|
|
|
121
125
|
strictPort: vitePort !== undefined,
|
|
122
126
|
// The browser talks to the Go app, which proxies HMR to Vite.
|
|
123
127
|
ws: appPort ? { clientPort: appPort } : undefined,
|
|
124
|
-
|
|
128
|
+
// The app, and the workspace it is in: in a pnpm or npm
|
|
129
|
+
// workspace, dependencies (and their fonts and images) live in
|
|
130
|
+
// the workspace's node_modules, outside the app.
|
|
131
|
+
fs: { allow: [root, searchForWorkspaceRoot(root)] }
|
|
125
132
|
},
|
|
126
133
|
optimizeDeps: {
|
|
127
134
|
entries: [CLIENT_ENTRY, 'src/**/*.svelte'],
|
|
@@ -157,6 +164,18 @@ export function tern(options = {}) {
|
|
|
157
164
|
res.end(e.stack ?? e.message);
|
|
158
165
|
});
|
|
159
166
|
});
|
|
167
|
+
server.middlewares.use(STYLES_PATH, (req, res) => {
|
|
168
|
+
void stylesInDev(server, req.url ?? '').then((css) => {
|
|
169
|
+
res.setHeader('Content-Type', 'text/css; charset=utf-8');
|
|
170
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
171
|
+
res.end(css);
|
|
172
|
+
}, (err) => {
|
|
173
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
174
|
+
res.statusCode = 500;
|
|
175
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
176
|
+
res.end(e.stack ?? e.message);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
160
179
|
server.middlewares.use(RENDER_PATH, (req, res) => {
|
|
161
180
|
void renderInDev(server, req).then((body) => {
|
|
162
181
|
res.setHeader('Content-Type', 'application/json');
|
|
@@ -181,6 +200,66 @@ export function tern(options = {}) {
|
|
|
181
200
|
const mod = (await server.ssrLoadModule(path.join(root, SERVER_ENTRY)));
|
|
182
201
|
return mod.render(payload, href);
|
|
183
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* The CSS the given components import, directly or through other
|
|
205
|
+
* modules, as the browser will load it: stylesheets and Svelte
|
|
206
|
+
* components' styles, in import order. It walks the client's module
|
|
207
|
+
* graph, transforming modules the browser hasn't asked for yet, so it
|
|
208
|
+
* works on the first load too.
|
|
209
|
+
*/
|
|
210
|
+
async function stylesInDev(server, url) {
|
|
211
|
+
const components = new URL(url, 'http://localhost').searchParams.getAll('c');
|
|
212
|
+
const client = server.environments.client;
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
const out = [];
|
|
215
|
+
const failed = [];
|
|
216
|
+
const isCSS = (id) => /\.(css|scss|sass|less|styl|stylus|pcss|postcss|sss)(\?|$)/.test(id) || /[?&]type=style\b/.test(id);
|
|
217
|
+
// Vite knows a stylesheet by its URL ending in .css, so the flag
|
|
218
|
+
// goes first in the query: /x.svelte?direct&svelte&type=style&lang.css.
|
|
219
|
+
const withQuery = (u, q) => {
|
|
220
|
+
const i = u.indexOf('?');
|
|
221
|
+
return i < 0 ? u + '?' + q : u.slice(0, i + 1) + q + '&' + u.slice(i + 1);
|
|
222
|
+
};
|
|
223
|
+
const visit = async (mod) => {
|
|
224
|
+
// The ?direct forms this function requests join the graph too:
|
|
225
|
+
// skip them, or each would be read again.
|
|
226
|
+
if (seen.has(mod.url) || seen.size > 5000 || /[?&](direct|inline)\b/.test(mod.url))
|
|
227
|
+
return;
|
|
228
|
+
seen.add(mod.url);
|
|
229
|
+
try {
|
|
230
|
+
if (isCSS(mod.url)) {
|
|
231
|
+
const res = await client.transformRequest(withQuery(mod.url, 'direct'));
|
|
232
|
+
if (res?.code)
|
|
233
|
+
out.push(res.code);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Dependencies' JavaScript has no stylesheets of the app's.
|
|
237
|
+
if (mod.url.includes('/node_modules/'))
|
|
238
|
+
return;
|
|
239
|
+
if (!mod.transformResult)
|
|
240
|
+
await client.transformRequest(mod.url);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
// The browser will report it; the page is styled when it loads.
|
|
244
|
+
failed.push(mod.url + ': ' + (err instanceof Error ? err.message.split('\n')[0] : String(err)));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// A copy: transforming adds to the graph while it is walked.
|
|
248
|
+
for (const dep of [...mod.importedModules])
|
|
249
|
+
await visit(dep);
|
|
250
|
+
};
|
|
251
|
+
for (const c of components) {
|
|
252
|
+
const file = path.join(root, c);
|
|
253
|
+
if (!file.startsWith(root + path.sep) || !fs.existsSync(file))
|
|
254
|
+
continue;
|
|
255
|
+
await client.transformRequest('/' + c.split(path.sep).join('/'));
|
|
256
|
+
for (const mod of client.moduleGraph.getModulesByFile(file) ?? [])
|
|
257
|
+
await visit(mod);
|
|
258
|
+
}
|
|
259
|
+
if (failed.length && process.env.TERN_DEBUG_STYLES)
|
|
260
|
+
console.warn('tern: styles not collected:\n' + failed.join('\n'));
|
|
261
|
+
return out.join('\n');
|
|
262
|
+
}
|
|
184
263
|
async function emailInDev(server, req) {
|
|
185
264
|
if (req.method !== 'POST')
|
|
186
265
|
throw new Error('POST required');
|