@ternkit/tern 0.0.0-stage → 0.3.1
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/LICENSE +21 -0
- package/README.md +49 -2
- package/dist/index.d.ts +16 -0
- package/dist/index.js +14 -0
- package/dist/runtime/DefaultError.svelte +8 -0
- package/dist/runtime/DefaultError.svelte.d.ts +18 -0
- package/dist/runtime/Passthrough.svelte +8 -0
- package/dist/runtime/Passthrough.svelte.d.ts +8 -0
- package/dist/runtime/Root.svelte +28 -0
- package/dist/runtime/Root.svelte.d.ts +11 -0
- package/dist/runtime/client.d.ts +1 -0
- package/dist/runtime/client.js +2 -0
- package/dist/runtime/deferred.d.ts +29 -0
- package/dist/runtime/deferred.js +111 -0
- package/dist/runtime/forms.d.ts +39 -0
- package/dist/runtime/forms.js +91 -0
- package/dist/runtime/router.svelte.d.ts +14 -0
- package/dist/runtime/router.svelte.js +388 -0
- package/dist/runtime/select.d.ts +38 -0
- package/dist/runtime/select.js +45 -0
- package/dist/runtime/state.svelte.d.ts +51 -0
- package/dist/runtime/state.svelte.js +62 -0
- package/dist/runtime/types.d.ts +91 -0
- package/dist/runtime/types.js +1 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.js +40 -0
- package/dist/server/polyfills.d.ts +2 -0
- package/dist/server/polyfills.js +38 -0
- package/dist/vite/index.d.ts +23 -0
- package/dist/vite/index.js +110 -0
- package/package.json +70 -4
|
@@ -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,38 @@
|
|
|
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
|
+
const g = globalThis;
|
|
8
|
+
if (typeof g.queueMicrotask !== 'function') {
|
|
9
|
+
g.queueMicrotask = (fn) => void Promise.resolve().then(fn);
|
|
10
|
+
}
|
|
11
|
+
if (typeof g.console === 'undefined') {
|
|
12
|
+
const format = (args) => args
|
|
13
|
+
.map((a) => {
|
|
14
|
+
if (typeof a === 'string')
|
|
15
|
+
return a;
|
|
16
|
+
if (a instanceof Error)
|
|
17
|
+
return a.stack ?? a.message;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(a);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return String(a);
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.join(' ');
|
|
26
|
+
const log = (level) => (...args) => {
|
|
27
|
+
if (typeof __tern_log === 'function')
|
|
28
|
+
__tern_log(level, format(args));
|
|
29
|
+
};
|
|
30
|
+
g.console = {
|
|
31
|
+
log: log('info'),
|
|
32
|
+
info: log('info'),
|
|
33
|
+
debug: log('debug'),
|
|
34
|
+
warn: log('warn'),
|
|
35
|
+
error: log('error'),
|
|
36
|
+
trace: log('debug')
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -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;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Paths shared with the Go code generator; keep in sync with internal/codegen. */
|
|
4
|
+
const CLIENT_ENTRY = '.tern/generated/client.js';
|
|
5
|
+
const SERVER_ENTRY = '.tern/generated/server.js';
|
|
6
|
+
const CLIENT_OUT = '.tern/output/client';
|
|
7
|
+
const SERVER_OUT = '.tern/output/server';
|
|
8
|
+
/** The dev endpoint the Go app calls to server-render a page. */
|
|
9
|
+
const RENDER_PATH = '/__tern/render';
|
|
10
|
+
/**
|
|
11
|
+
* The Vite plugin for Tern apps. It includes the Svelte plugin, so it is
|
|
12
|
+
* the only plugin a Tern app needs:
|
|
13
|
+
*
|
|
14
|
+
* ```js
|
|
15
|
+
* // vite.config.js
|
|
16
|
+
* import { tern } from '@ternkit/tern/vite';
|
|
17
|
+
* import { defineConfig } from 'vite';
|
|
18
|
+
*
|
|
19
|
+
* export default defineConfig({ plugins: [tern()] });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* `vite build` builds the client; `vite build --ssr` builds the server
|
|
23
|
+
* renderer that `tern build` embeds in the Go binary.
|
|
24
|
+
*/
|
|
25
|
+
export function tern(options = {}) {
|
|
26
|
+
let root = process.cwd();
|
|
27
|
+
const plugin = {
|
|
28
|
+
name: 'tern',
|
|
29
|
+
config(config, env) {
|
|
30
|
+
root = path.resolve(config.root ?? process.cwd());
|
|
31
|
+
const shared = {
|
|
32
|
+
appType: 'custom',
|
|
33
|
+
resolve: { alias: { $lib: path.join(root, 'src/lib') } }
|
|
34
|
+
};
|
|
35
|
+
if (env.isSsrBuild) {
|
|
36
|
+
return {
|
|
37
|
+
...shared,
|
|
38
|
+
publicDir: false,
|
|
39
|
+
build: {
|
|
40
|
+
ssr: true,
|
|
41
|
+
outDir: SERVER_OUT,
|
|
42
|
+
emptyOutDir: true,
|
|
43
|
+
minify: true,
|
|
44
|
+
target: 'es2023',
|
|
45
|
+
rolldownOptions: {
|
|
46
|
+
input: path.join(root, SERVER_ENTRY),
|
|
47
|
+
output: { format: 'iife', name: '__tern_server', entryFileNames: 'server.js' }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
// One self-contained file: the renderer runs in an embedded engine
|
|
51
|
+
// without a module loader or Node built-ins.
|
|
52
|
+
ssr: { noExternal: true }
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const vitePort = Number(process.env.TERN_VITE_PORT) || undefined;
|
|
56
|
+
const appPort = Number(process.env.TERN_APP_PORT) || undefined;
|
|
57
|
+
return {
|
|
58
|
+
...shared,
|
|
59
|
+
publicDir: 'static',
|
|
60
|
+
build: {
|
|
61
|
+
outDir: CLIENT_OUT,
|
|
62
|
+
emptyOutDir: true,
|
|
63
|
+
manifest: true,
|
|
64
|
+
assetsDir: '_app/immutable',
|
|
65
|
+
rolldownOptions: { input: path.join(root, CLIENT_ENTRY) }
|
|
66
|
+
},
|
|
67
|
+
server: {
|
|
68
|
+
// Bind to IPv4 explicitly: the Go app proxies to 127.0.0.1, and
|
|
69
|
+
// Node may resolve "localhost" to ::1 only.
|
|
70
|
+
host: vitePort ? '127.0.0.1' : undefined,
|
|
71
|
+
port: vitePort,
|
|
72
|
+
strictPort: vitePort !== undefined,
|
|
73
|
+
// The browser talks to the Go app, which proxies HMR to Vite.
|
|
74
|
+
hmr: appPort ? { clientPort: appPort } : undefined,
|
|
75
|
+
fs: { allow: [root] }
|
|
76
|
+
},
|
|
77
|
+
optimizeDeps: {
|
|
78
|
+
entries: [CLIENT_ENTRY, 'src/**/*.svelte'],
|
|
79
|
+
exclude: ['@ternkit/tern']
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
configureServer(server) {
|
|
84
|
+
server.middlewares.use(RENDER_PATH, (req, res) => {
|
|
85
|
+
void renderInDev(server, req).then((body) => {
|
|
86
|
+
res.setHeader('Content-Type', 'application/json');
|
|
87
|
+
res.end(body);
|
|
88
|
+
}, (err) => {
|
|
89
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
90
|
+
server.ssrFixStacktrace(e);
|
|
91
|
+
res.statusCode = 500;
|
|
92
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
93
|
+
res.end(e.stack ?? e.message);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
async function renderInDev(server, req) {
|
|
99
|
+
if (req.method !== 'POST')
|
|
100
|
+
throw new Error('POST required');
|
|
101
|
+
const chunks = [];
|
|
102
|
+
for await (const chunk of req)
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
const { payload, href } = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
105
|
+
const mod = (await server.ssrLoadModule(path.join(root, SERVER_ENTRY)));
|
|
106
|
+
return mod.render(payload, href);
|
|
107
|
+
}
|
|
108
|
+
return [...svelte(options.svelte), plugin];
|
|
109
|
+
}
|
|
110
|
+
export default tern;
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ternkit/tern",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Client runtime and Vite plugin for Tern: Svelte on the client, Go on the server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Unfield/Tern.git",
|
|
10
|
+
"directory": "packages/tern"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/Unfield/Tern",
|
|
13
|
+
"bugs": "https://github.com/Unfield/Tern/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"svelte",
|
|
16
|
+
"go",
|
|
17
|
+
"framework",
|
|
18
|
+
"vite",
|
|
19
|
+
"router"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"svelte": "./dist/index.js",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/runtime/client.d.ts",
|
|
29
|
+
"svelte": "./dist/runtime/client.js",
|
|
30
|
+
"default": "./dist/runtime/client.js"
|
|
31
|
+
},
|
|
32
|
+
"./server": {
|
|
33
|
+
"types": "./dist/server/index.d.ts",
|
|
34
|
+
"svelte": "./dist/server/index.js",
|
|
35
|
+
"default": "./dist/server/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./polyfills": {
|
|
38
|
+
"types": "./dist/server/polyfills.d.ts",
|
|
39
|
+
"default": "./dist/server/polyfills.js"
|
|
40
|
+
},
|
|
41
|
+
"./vite": {
|
|
42
|
+
"types": "./dist/vite/index.d.ts",
|
|
43
|
+
"default": "./dist/vite/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"!dist/**/*.test.*"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "svelte-package -i src -o dist",
|
|
53
|
+
"prepare": "svelte-package -i src -o dist",
|
|
54
|
+
"typecheck": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"svelte": "^5.46.4",
|
|
58
|
+
"vite": "^8.0.0"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
62
|
+
"core-js": "^3.51.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@sveltejs/package": "^3.0.2",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
67
|
+
"svelte": "^5.57.3",
|
|
68
|
+
"svelte-check": "^4.7.8",
|
|
69
|
+
"typescript": "^6.0.0",
|
|
70
|
+
"vite": "^8.3.7"
|
|
71
|
+
}
|
|
72
|
+
}
|