pull/12335/head
Rich Harris 2 years ago
commit fa4d3730f9

@ -521,6 +521,7 @@
"small-sheep-type", "small-sheep-type",
"small-spiders-fail", "small-spiders-fail",
"smart-cherries-leave", "smart-cherries-leave",
"smart-fans-crash",
"smart-grapes-mate", "smart-grapes-mate",
"smart-parents-swim", "smart-parents-swim",
"smart-spiders-fetch", "smart-spiders-fetch",
@ -647,6 +648,7 @@
"violet-mugs-behave", "violet-mugs-behave",
"violet-pigs-jam", "violet-pigs-jam",
"warm-cherries-shake", "warm-cherries-shake",
"warm-waves-reply",
"weak-drinks-speak", "weak-drinks-speak",
"weak-frogs-bow", "weak-frogs-bow",
"weak-terms-destroy", "weak-terms-destroy",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly compile $effect.root in svelte modules

@ -2,4 +2,4 @@
'svelte': patch 'svelte': patch
--- ---
fix: ensure component always returns an object fix: ensure `bind:this` works with component with no return value

@ -1,6 +1,38 @@
import svelte_config from '@sveltejs/eslint-config'; import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube'; import lube from 'eslint-plugin-lube';
const no_compiler_imports = {
meta: {
type: /** @type {const} */ ('problem'),
docs: {
description:
'Enforce that there are no imports to the compiler in runtime code. ' +
'This prevent accidental inclusion of the compiler runtime and ' +
"ensures that TypeScript does not pick up more ambient types (for example from Node) that shouldn't be available in the browser."
}
},
create(context) {
return {
Program: () => {
// Do a simple string search because ESLint doesn't provide a way to check JSDoc comments.
// The string search could in theory yield false positives, but in practice it's unlikely.
const text = context.sourceCode.getText();
const idx = Math.max(text.indexOf('../compiler/'), text.indexOf('#compiler'));
if (idx !== -1) {
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(idx),
end: context.sourceCode.getLocFromIndex(idx + 12)
},
message:
'References to compiler code are forbidden in runtime code (both for type and value imports)'
});
}
}
};
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */ /** @type {import('eslint').Linter.FlatConfig[]} */
export default [ export default [
...svelte_config, ...svelte_config,
@ -11,7 +43,8 @@ export default [
} }
}, },
plugins: { plugins: {
lube lube,
custom: { rules: { no_compiler_imports } }
}, },
rules: { rules: {
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
@ -37,6 +70,13 @@ export default [
'no-console': 'off' 'no-console': 'off'
} }
}, },
{
files: ['packages/svelte/src/**/*'],
ignores: ['packages/svelte/src/compiler/**/*'],
rules: {
'custom/no_compiler_imports': 'error'
}
},
{ {
ignores: [ ignores: [
'**/*.d.ts', '**/*.d.ts',

@ -1,5 +1,13 @@
# svelte # svelte
## 5.0.0-next.175
### Patch Changes
- fix: correctly compile $effect.root in svelte modules ([#12315](https://github.com/sveltejs/svelte/pull/12315))
- fix: ensure `bind:this` works with component with no return value ([#12290](https://github.com/sveltejs/svelte/pull/12290))
## 5.0.0-next.174 ## 5.0.0-next.174
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.174", "version": "5.0.0-next.175",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -106,7 +106,7 @@
"scripts": { "scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", "build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw", "dev": "node scripts/process-messages && rollup -cw",
"check": "tsc && cd ./tests/types && tsc", "check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch", "check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json", "generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",

@ -3,7 +3,7 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js'; import { regex_whitespace } from '../patterns.js';
import { reserved } from './utils/names.js'; import { reserved } from '../../../constants.js';
import full_char_code_at from './utils/full_char_code_at.js'; import full_char_code_at from './utils/full_char_code_at.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment } from './utils/create.js';

@ -1,6 +1,6 @@
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
/** @import * as Compiler from '#compiler' */ /** @import * as Compiler from '#compiler' */
import { is_void } from '../utils/names.js'; import { is_void } from '../../../../constants.js';
import read_expression from '../read/expression.js'; import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js'; import { read_script } from '../read/script.js';
import read_style from '../read/style.js'; import read_style from '../read/style.js';

@ -1,74 +0,0 @@
export const reserved = [
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const void_element_names = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
}

@ -339,9 +339,6 @@ export function client_component(source, analysis, options) {
? b.return(b.call('$.pop', b.object(component_returned_object))) ? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop')) : b.stmt(b.call('$.pop'))
); );
} else {
// Always return an object, so that `bind:this` on this component will not be falsy
component_block.body.push(b.return(b.object(component_returned_object)));
} }
if (analysis.uses_rest_props) { if (analysis.uses_rest_props) {

@ -8,7 +8,7 @@ import type {
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { Location } from 'locate-character'; import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState { export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>; readonly private_state: Map<string, StateField>;
@ -24,10 +24,6 @@ export interface ClientTransformState extends TransformState {
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>; readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
} }
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export interface ComponentClientTransformState extends ClientTransformState { export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis; readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions; readonly options: ValidatedCompileOptions;

@ -1052,7 +1052,7 @@ function serialize_bind_this(bind_this, context, node) {
} }
/** /**
* @param {import('../types.js').SourceLocation[]} locations * @param {import('#shared').SourceLocation[]} locations
*/ */
function serialize_locations(locations) { function serialize_locations(locations) {
return b.array( return b.array(
@ -1910,7 +1910,7 @@ export const template_visitors = {
state.init.push(b.stmt(b.call('$.transition', ...args))); state.init.push(b.stmt(b.call('$.transition', ...args)));
}, },
RegularElement(node, context) { RegularElement(node, context) {
/** @type {import('../types.js').SourceLocation} */ /** @type {import('#shared').SourceLocation} */
let location = [-1, -1]; let location = [-1, -1];
if (context.state.options.dev) { if (context.state.options.dev) {
@ -2138,7 +2138,7 @@ export const template_visitors = {
context.state.template.push('>'); context.state.template.push('>');
/** @type {import('../types.js').SourceLocation[]} */ /** @type {import('#shared').SourceLocation[]} */
const child_locations = []; const child_locations = [];
/** @type {import('../types').ComponentClientTransformState} */ /** @type {import('../types').ComponentClientTransformState} */

@ -411,6 +411,14 @@ const global_visitors = {
return b.literal(false); return b.literal(false);
} }
if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg))
);
// Just call the function directly
return b.call(args[0]);
}
if (rune === '$state.snapshot') { if (rune === '$state.snapshot') {
return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0])); return /** @type {import('estree').Expression} */ (context.visit(node.arguments[0]));
} }
@ -571,7 +579,7 @@ const javascript_visitors_runes = {
for (const declarator of node.declarations) { for (const declarator of node.declarations) {
const init = declarator.init; const init = declarator.init;
const rune = get_rune(init, state.scope); const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.tracking' || rune === '$inspect') { if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
continue; continue;
} }

@ -294,3 +294,78 @@ export function is_capture_event(name, mode = 'exclude-on') {
? name !== 'gotpointercapture' && name !== 'lostpointercapture' ? name !== 'gotpointercapture' && name !== 'lostpointercapture'
: name !== 'ongotpointercapture' && name !== 'onlostpointercapture'; : name !== 'ongotpointercapture' && name !== 'onlostpointercapture';
} }
export const reserved = [
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const void_element_names = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
}

@ -1,4 +1,4 @@
/** @import { SourceLocation } from '../../../compiler/phases/3-transform/client/types.js' */ /** @import { SourceLocation } from '#shared' */
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js'; import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js'; import { hydrating } from '../dom/hydration.js';

@ -15,14 +15,14 @@ function is_bound_this(bound_value, element_or_component) {
} }
/** /**
* @param {Element} element_or_component * @param {any} element_or_component
* @param {(value: unknown, ...parts: unknown[]) => void} update * @param {(value: unknown, ...parts: unknown[]) => void} update
* @param {(...parts: unknown[]) => unknown} get_value * @param {(...parts: unknown[]) => unknown} get_value
* @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block, * @param {() => unknown[]} [get_parts] Set if the this binding is used inside an each block,
* returns all the parts of the each block context that are used in the expression * returns all the parts of the each block context that are used in the expression
* @returns {void} * @returns {void}
*/ */
export function bind_this(element_or_component, update, get_value, get_parts) { export function bind_this(element_or_component = {}, update, get_value, get_parts) {
effect(() => { effect(() => {
/** @type {unknown[]} */ /** @type {unknown[]} */
var old_parts; var old_parts;

@ -224,10 +224,11 @@ function handle_error(error, effect, component_context) {
let current_context = component_context; let current_context = component_context;
while (current_context !== null) { while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.filename; var filename = current_context.function?.filename;
if (filename) { if (filename) {
const file = filename.split('/').at(-1); const file = filename.split('/').pop();
component_stack.push(file); component_stack.push(file);
} }

@ -2,3 +2,7 @@ export type Store<V> = {
subscribe: (run: (value: V) => void) => () => void; subscribe: (run: (value: V) => void) => () => void;
set(value: V): void; set(value: V): void;
}; };
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];

@ -1,4 +1,4 @@
import { is_void } from '../../compiler/phases/1-parse/utils/names.js'; import { is_void } from '../../constants.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.174'; export const VERSION = '5.0.0-next.175';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,30 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: '<button>0</button><button>0</button><button>cleanup</button>',
async test({ assert, target, logs }) {
const [b1, b2, b3] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(logs, [0, 1]);
flushSync(() => {
b3.click();
});
assert.deepEqual(logs, [0, 1, 'cleanup 1', 'cleanup 2']);
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(logs, [0, 1, 'cleanup 1', 'cleanup 2']);
}
});

@ -0,0 +1,11 @@
<script>
import { with_root } from './root.svelte.js';
let x = $state(0);
let y = $state(0);
const cleanup = with_root(() => x)
</script>
<button onclick={() => x++}>{x}</button>
<button onclick={() => y++}>{y}</button>
<button onclick={cleanup}>cleanup</button>

@ -0,0 +1,20 @@
export function with_root(get_x) {
const cleanup = $effect.root(() => {
$effect(() => {
console.log(get_x());
});
const nested_cleanup = $effect.root(() => {
return () => {
console.log('cleanup 2');
};
});
return () => {
console.log('cleanup 1');
nested_cleanup();
};
});
return cleanup;
}

@ -30,5 +30,4 @@ export default function Bind_component_snippet($$anchor) {
$.template_effect(() => $.set_text(text, ` value: ${$.get(value) ?? ""}`)); $.template_effect(() => $.set_text(text, ` value: ${$.get(value) ?? ""}`));
$.append($$anchor, fragment_1); $.append($$anchor, fragment_1);
return {};
} }

@ -7,5 +7,4 @@ export default function Bind_this($$anchor) {
$.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo);
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }

@ -42,5 +42,4 @@ export default function Main($$anchor) {
}); });
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }

@ -13,5 +13,4 @@ export default function Each_string_template($$anchor) {
}); });
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }

@ -26,5 +26,4 @@ export default function Function_prop_no_getter($$anchor) {
}); });
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }

@ -8,5 +8,4 @@ export default function Hello_world($$anchor) {
$.reset(h1); $.reset(h1);
$.append($$anchor, h1); $.append($$anchor, h1);
return {};
} }

@ -8,7 +8,6 @@ function Hmr($$anchor) {
$.reset(h1); $.reset(h1);
$.append($$anchor, h1); $.append($$anchor, h1);
return {};
} }
if (import.meta.hot) { if (import.meta.hot) {

@ -31,7 +31,6 @@ export default function State_proxy_literal($$anchor) {
$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));
$.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value)); $.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }
$.delegate(["click"]); $.delegate(["click"]);

@ -8,5 +8,4 @@ export default function Svelte_element($$anchor, $$props) {
$.element(node, tag, false); $.element(node, tag, false);
$.append($$anchor, fragment); $.append($$anchor, fragment);
return {};
} }

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// Ensure we don't use any methods that are not available in all browser for a long period of time
// so that people don't need to polyfill them. Example array.at(...) was introduced to Safari only in 2022
"target": "es2021",
"lib": ["es2021", "DOM", "DOM.Iterable"],
"types": [] // prevent automatic inclusion of @types/node
},
"include": ["./src/"],
// Compiler is allowed to use more recent methods; people using it in the browser are expected to know
// how to polyfill missing methods. Also make sure to not include test files as these include Vitest
// which then loads node globals.
"exclude": ["./src/compiler/**/*", "./src/**/*.test.ts"]
}

@ -1,13 +0,0 @@
import { mount, hydrate, unmount } from 'svelte';
import App from './src/main.svelte';
const root = document.getElementById('root')!;
const render = root.firstChild?.nextSibling ? hydrate : mount;
const component = render(App, {
target: document.getElementById('root')!,
recover: false
});
// @ts-ignore
window.unmount = () => unmount(component);

@ -1,64 +0,0 @@
// @ts-check
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import polka from 'polka';
import { createServer as createViteServer } from 'vite';
const PORT = process.env.PORT || '5173';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.env.NODE_ENV = 'development';
async function createServer() {
const app = polka();
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
});
app.use(vite.middlewares);
app.use('*', async (req, res) => {
if (req.originalUrl !== '/') {
const file = path.resolve('./dist' + req.originalUrl);
if (fs.existsSync(file)) {
res.writeHead(200, {
'Content-Type': 'application/javascript'
});
res.end(fs.createReadStream(file));
} else {
res.writeHead(404);
res.end('not found');
}
return;
}
const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
const transformed_template = await vite.transformIndexHtml(req.originalUrl, template);
const { render } = await vite.ssrLoadModule('svelte/server');
const { default: App } = await vite.ssrLoadModule('./src/main.svelte');
const { head, body } = render(App);
const html = transformed_template
.replace(`<!--ssr-head-->`, head)
.replace(`<!--ssr-body-->`, body);
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html);
});
return { app, vite };
}
createServer()
.then(({ app }) =>
app.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
})
)
.catch((err) => {
console.error('Error Starting Server:\n', err);
process.exit(1);
});

@ -1,22 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import express from 'express';
import { head, body } from './server/entry-server.js';
const rendered = fs
.readFileSync(path.resolve('./dist/client/index.html'), 'utf-8')
.replace(`<!--ssr-html-->`, body)
.replace(`<!--ssr-head-->`, head);
express()
.use('*', async (req, res) => {
if (req.originalUrl !== '/') {
res.sendFile(path.resolve('./dist/client' + req.originalUrl));
return;
}
res.status(200).set({ 'Content-Type': 'text/html' }).end(rendered);
})
.listen('3000');
console.log('listening on http://localhost:3000');

@ -8,6 +8,20 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"><!--ssr-body--></div> <div id="root"><!--ssr-body--></div>
<script type="module" src="/demo-client.ts"></script>
<script type="module">
import { mount, hydrate, unmount } from 'svelte';
import App from '/src/main.svelte';
const root = document.getElementById('root');
const render = root.firstChild?.nextSibling ? hydrate : mount;
const component = render(App, {
target: document.getElementById('root')
});
// @ts-ignore
window.unmount = () => unmount(component);
</script>
</body> </body>
</html> </html>

@ -6,9 +6,9 @@
"scripts": { "scripts": {
"prepare": "node scripts/create-app-svelte.js", "prepare": "node scripts/create-app-svelte.js",
"dev": "vite --host", "dev": "vite --host",
"ssr": "node ./demo-server.js", "ssr": "node ./ssr-dev.js",
"build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr demo-server.ts", "build": "vite build --outDir dist/client && vite build --outDir dist/server --ssr ssr-prod.js",
"prod": "npm run build && node dist", "prod": "npm run build && node dist/server/ssr-prod",
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {

@ -0,0 +1,36 @@
// @ts-check
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import polka from 'polka';
import { createServer as createViteServer } from 'vite';
import { render } from 'svelte/server';
const PORT = process.env.PORT || '5173';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.env.NODE_ENV = 'development';
const vite = await createViteServer({
server: { middlewareMode: true },
appType: 'custom'
});
polka()
.use(vite.middlewares)
.use(async (req, res) => {
const template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');
const transformed_template = await vite.transformIndexHtml(req.url, template);
const { default: App } = await vite.ssrLoadModule('./src/main.svelte');
const { head, body } = render(App);
const html = transformed_template
.replace(`<!--ssr-head-->`, head)
.replace(`<!--ssr-body-->`, body);
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html);
})
.listen(PORT, () => {
console.log(`http://localhost:${PORT}`);
});

@ -0,0 +1,41 @@
import fs from 'node:fs';
import path from 'node:path';
import polka from 'polka';
import { render } from 'svelte/server';
import App from './src/main.svelte';
const { head, body } = render(App);
const rendered = fs
.readFileSync(path.resolve('./dist/client/index.html'), 'utf-8')
.replace(`<!--ssr-body-->`, body)
.replace(`<!--ssr-head-->`, head);
const types = {
'.js': 'application/javascript',
'.css': 'text/css'
};
polka()
.use((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'content-type': 'text/html' });
res.end(rendered);
return;
}
const file = path.resolve('./dist/client' + req.url);
if (fs.existsSync(file)) {
const type = types[path.extname(req.url)] ?? 'application/octet-stream';
res.writeHead(200, { 'content-type': type });
fs.createReadStream(file).pipe(res);
return;
}
res.writeHead(404);
res.end('not found');
})
.listen('3000');
console.log('listening on http://localhost:3000');

@ -13,5 +13,5 @@
"allowJs": true, "allowJs": true,
"checkJs": true "checkJs": true
}, },
"include": ["./src", "demo-client.ts", "demo-server.ts"] "include": ["./src", "ssr-dev.js", "ssr-prod.js"]
} }

Loading…
Cancel
Save