@ternkit/tern 0.4.0 → 1.0.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 CHANGED
@@ -7,7 +7,7 @@
7
7
  * </script>
8
8
  * ```
9
9
  */
10
- export { page, navigating } from './runtime/state.svelte.js';
10
+ export { page, navigating, env } from './runtime/state.svelte.js';
11
11
  export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
12
12
  export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
13
13
  export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.js';
@@ -15,4 +15,4 @@ export { enhance } from './runtime/forms.js';
15
15
  export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
16
16
  export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
17
17
  /** The version of the Tern client runtime. */
18
- export declare const VERSION = "0.4.0";
18
+ export declare const VERSION = "1.0.0";
package/dist/index.js CHANGED
@@ -7,9 +7,9 @@
7
7
  * </script>
8
8
  * ```
9
9
  */
10
- export { page, navigating } from './runtime/state.svelte.js';
10
+ export { page, navigating, env } from './runtime/state.svelte.js';
11
11
  export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
12
12
  export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
13
13
  export { enhance } from './runtime/forms.js';
14
14
  /** The version of the Tern client runtime. */
15
- export const VERSION = '0.4.0';
15
+ export const VERSION = '1.0.0';
@@ -81,7 +81,7 @@ export async function goto(href, opts = {}) {
81
81
  throw new Error('Tern: goto() was called before the app started');
82
82
  }
83
83
  const url = new URL(href, location.href);
84
- if (url.origin !== location.origin) {
84
+ if (!inApp(url)) {
85
85
  location.href = url.href;
86
86
  return;
87
87
  }
@@ -162,6 +162,7 @@ function setForm(form) {
162
162
  }
163
163
  function dataURL(url, reuse) {
164
164
  const path = url.pathname.replace(/\/$/, '');
165
+ // The base path is part of the pathname; the server strips it.
165
166
  let target = `${path}/__data.json${url.search}`;
166
167
  if (reuse) {
167
168
  const here = url.pathname + url.search;
@@ -240,7 +241,7 @@ async function navigate(url, opts, type, redirects, popIndex, fromIndex) {
240
241
  }
241
242
  if (payload.type === 'redirect' && payload.location) {
242
243
  const next = new URL(payload.location, url);
243
- if (redirects >= MAX_REDIRECTS || next.origin !== location.origin) {
244
+ if (redirects >= MAX_REDIRECTS || !inApp(next)) {
244
245
  location.href = next.href;
245
246
  return;
246
247
  }
@@ -410,12 +411,19 @@ function findAnchor(event) {
410
411
  }
411
412
  return null;
412
413
  }
414
+ /** Reports whether url belongs to this app (its origin and base path). */
415
+ function inApp(url) {
416
+ if (url.origin !== location.origin)
417
+ return false;
418
+ const base = manifest.base ?? '';
419
+ return !base || url.pathname === base || url.pathname.startsWith(base + '/');
420
+ }
413
421
  function anchorURL(a) {
414
422
  const href = a instanceof SVGAElement ? a.href.baseVal : a.href;
415
423
  if (!href)
416
424
  return null;
417
425
  const url = new URL(href, document.baseURI);
418
- if (url.origin !== location.origin || !/^https?:$/.test(url.protocol))
426
+ if (!inApp(url) || !/^https?:$/.test(url.protocol))
419
427
  return null;
420
428
  const target = a instanceof SVGAElement ? a.target.baseVal : a.target;
421
429
  if (target && target !== '_self')
@@ -476,7 +484,7 @@ function listen() {
476
484
  return;
477
485
  }
478
486
  const url = new URL(submitter?.hasAttribute('formaction') ? submitter.formAction : own.action);
479
- if (url.origin !== location.origin)
487
+ if (!inApp(url))
480
488
  return;
481
489
  event.preventDefault();
482
490
  url.search = new URLSearchParams(new FormData(form, submitter)).toString();
@@ -46,6 +46,17 @@ export declare const navigating: {
46
46
  readonly current: Navigation | null;
47
47
  };
48
48
  export declare function setNavigating(n: Navigation | null): void;
49
+ /**
50
+ * The server's PUBLIC_ environment variables, read when it started:
51
+ *
52
+ * ```js
53
+ * import { env } from '@ternkit/tern';
54
+ * fetch(`${env.PUBLIC_API_URL}/status`);
55
+ * ```
56
+ *
57
+ * Only variables whose names start with PUBLIC_ are sent to the browser.
58
+ */
59
+ export declare const env: Record<string, string | undefined>;
49
60
  /** Updates page state for a rendered payload. */
50
61
  export declare function applyPayloadToPage(p: Payload, url: URL, state: Record<string, unknown>, data: Record<string, unknown>[]): void;
51
62
  export {};
@@ -49,8 +49,24 @@ export const navigating = {
49
49
  export function setNavigating(n) {
50
50
  navigatingState.current = n;
51
51
  }
52
+ /**
53
+ * The server's PUBLIC_ environment variables, read when it started:
54
+ *
55
+ * ```js
56
+ * import { env } from '@ternkit/tern';
57
+ * fetch(`${env.PUBLIC_API_URL}/status`);
58
+ * ```
59
+ *
60
+ * Only variables whose names start with PUBLIC_ are sent to the browser.
61
+ */
62
+ export const env = {};
52
63
  /** Updates page state for a rendered payload. */
53
64
  export function applyPayloadToPage(p, url, state, data) {
65
+ if (p.env) {
66
+ for (const k of Object.keys(env))
67
+ delete env[k];
68
+ Object.assign(env, p.env);
69
+ }
54
70
  page.url = url;
55
71
  page.params = p.params ?? {};
56
72
  page.route = { id: p.route ?? null };
@@ -21,6 +21,8 @@ export interface RouteEntry {
21
21
  }
22
22
  /** The client manifest generated by `tern gen` in .tern/generated/manifest.js. */
23
23
  export interface ClientManifest {
24
+ /** The base path from tern.json, such as '/app', or ''. */
25
+ base?: string;
24
26
  layouts: LayoutEntry[];
25
27
  routes: Record<string, RouteEntry>;
26
28
  fallback: {
@@ -47,6 +49,8 @@ export interface Payload {
47
49
  uses?: (NodeUses | null)[];
48
50
  /** Nodes the server skipped because the client already has their data. */
49
51
  reused?: number[];
52
+ /** PUBLIC_ environment variables, on full page loads. */
53
+ env?: Record<string, string>;
50
54
  }
51
55
  export interface NodeUses {
52
56
  url?: boolean;
@@ -1,4 +1,5 @@
1
1
  import { svelte } from '@sveltejs/vite-plugin-svelte';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  /** Paths shared with the Go code generator; keep in sync with internal/codegen. */
4
5
  const CLIENT_ENTRY = '.tern/generated/client.js';
@@ -28,8 +29,10 @@ export function tern(options = {}) {
28
29
  name: 'tern',
29
30
  config(config, env) {
30
31
  root = path.resolve(config.root ?? process.cwd());
32
+ const basePath = readBasePath(root);
31
33
  const shared = {
32
34
  appType: 'custom',
35
+ base: basePath + '/',
33
36
  resolve: {
34
37
  alias: {
35
38
  $lib: path.join(root, 'src/lib'),
@@ -115,4 +118,14 @@ export function tern(options = {}) {
115
118
  }
116
119
  return [...svelte(options.svelte), plugin];
117
120
  }
121
+ /** Reads basePath from tern.json; the Go side validates it. */
122
+ function readBasePath(root) {
123
+ try {
124
+ const config = JSON.parse(fs.readFileSync(path.join(root, 'tern.json'), 'utf8'));
125
+ return config.basePath ?? '';
126
+ }
127
+ catch {
128
+ return '';
129
+ }
130
+ }
118
131
  export default tern;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ternkit/tern",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Client runtime and Vite plugin for Tern: Svelte on the client, Go on the server",
5
5
  "license": "MIT",
6
6
  "type": "module",