Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-remotes-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

fix: only treat files in node_modules as remote modules when the package has a peer dependency on @sveltejs/kit
2 changes: 1 addition & 1 deletion documentation/docs/20-core-concepts/60-remote-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default defineConfig({

## Overview

Remote functions are exported from a _remote module_, which is a file whose name includes a `remote` segment (e.g. `remote.ts` or `data.remote.ts`). They come in four flavours: `query`, `form`, `command` and `prerender`. On the client, the exported functions are transformed to `fetch` wrappers that invoke their counterparts on the server via a generated HTTP endpoint. Remote modules can be placed anywhere in your `src` directory (except inside a `server` directory — files here [cannot be imported into client-side code](server-only-modules)), and third-party libraries can provide them too.
Remote functions are exported from a _remote module_, which is a file whose name includes a `remote` segment (e.g. `remote.ts` or `data.remote.ts`). They come in four flavours: `query`, `form`, `command` and `prerender`. On the client, the exported functions are transformed to `fetch` wrappers that invoke their counterparts on the server via a generated HTTP endpoint. Remote modules can be placed anywhere in your `src` directory (except inside a `server` directory — files here [cannot be imported into client-side code](server-only-modules)), and third-party libraries that list `@sveltejs/kit` in their `peerDependencies` can provide them too.

## query

Expand Down
9 changes: 2 additions & 7 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ import * as sync from '../../../core/sync/sync.js';
import { get_mime_lookup, get_runtime_base } from '../../../core/utils.js';
import '../../../utils/mime.js'; // extend mrmime with additional types (affects sirv too)
import { compact } from '../../../utils/array.js';
import {
is_chrome_devtools_request,
log_response,
not_found,
remote_module_pattern
} from '../utils.js';
import { is_chrome_devtools_request, is_remote_module, log_response, not_found } from '../utils.js';
import { SCHEME } from '../../../utils/url.js';
import { check_feature } from '../../../utils/features.js';
import { escape_html } from '../../../utils/escape.js';
Expand Down Expand Up @@ -390,7 +385,7 @@ export async function dev(vite, vite_config, svelte_config, get_remotes, root, s
file.startsWith(svelte_config.kit.files.routes + path.sep) ||
file.startsWith(svelte_config.kit.files.assets + path.sep) ||
(params_file && file === params_file) ||
remote_module_pattern.test(file) ||
is_remote_module(file) ||
// in contrast to server hooks, client hooks are written to the client manifest
// and therefore need rebuilding when they are added/removed
file.startsWith(svelte_config.kit.files.hooks.client)
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { preview } from './preview/index.js';
import {
error_for_missing_config,
get_config_aliases,
is_remote_module,
normalize_id,
remote_module_pattern,
server_only_directory_pattern,
Expand Down Expand Up @@ -490,6 +491,7 @@ function kit({ svelte_config }) {
async handler(id, importer) {
const resolved = await this.resolve(id, importer, { skipSelf: true });
if (!resolved) return { id, external: true };
if (!is_remote_module(resolved.id)) return;
// a servable /@fs url; 'absolute' stops rolldown relativizing it in the deps bundle
return { id: to_fs(resolved.id), external: 'absolute' };
}
Expand Down Expand Up @@ -920,6 +922,8 @@ function kit({ svelte_config }) {
id: remote_module_pattern
},
async handler(code, id) {
if (!is_remote_module(id)) return;

const file = posixify(path.relative(root, id));
const remote = {
hash: hash(file),
Expand Down
40 changes: 40 additions & 0 deletions packages/kit/src/exports/vite/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'node:fs';
import path from 'node:path';
import { posixify } from '../../utils/os.js';
import { negotiate } from '../../utils/http.js';
Expand Down Expand Up @@ -151,6 +152,45 @@ export function normalize_id(id, aliases, cwd) {
}

export const remote_module_pattern = /[/.]remote(\.[^/]+)+$/;
/** @type {Map<string, boolean>} */
const remote_module_cache = new Map();

/**
* Whether `id` is a remote module. Files in node_modules only count if the
* package they belong to has a peer dependency on `@sveltejs/kit`
* @param {string} id
* @returns {boolean}
*/
export function is_remote_module(id) {
id = posixify(id);
if (!remote_module_pattern.test(id)) return false;
if (!id.includes('node_modules')) return true;

const directory = path.dirname(id);
const cached = remote_module_cache.get(directory);
if (cached !== undefined) return cached;

let current = directory;

while (true) {
try {
const package_json = JSON.parse(fs.readFileSync(path.join(current, 'package.json'), 'utf8'));

if (package_json.peerDependencies?.['@sveltejs/kit']) {
remote_module_cache.set(directory, true);
return true;
}
} catch {}

const parent = path.dirname(current);
if (path.basename(current) === 'node_modules' || parent === current) break;
current = parent;
}

remote_module_cache.set(directory, false);
return false;
}

export const server_only_module_pattern = /[/.]server(\.[^/]+)+$/;
export const server_only_directory_pattern = /\/server\//;

Expand Down
58 changes: 58 additions & 0 deletions packages/kit/src/exports/vite/utils.spec.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { expect, test } from 'vitest';
import { validate_config } from '../../core/config/index.js';
Expand All @@ -6,6 +8,7 @@ import { dedent } from '../../core/sync/utils.js';
import {
error_for_missing_config,
get_config_aliases,
is_remote_module,
remote_module_pattern,
server_only_directory_pattern,
server_only_module_pattern
Expand Down Expand Up @@ -62,6 +65,61 @@ test('recognizes remote module filenames', () => {
expect(remote_module_pattern.test('dir/remote/module.js')).toBe(false);
});

test('recognizes remote modules', () => {
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'sveltekit-remote-module-'));

/**
* @param {string} directory
* @param {Record<string, unknown>} contents
* @returns {void}
*/
function write_package_json(directory, contents) {
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify(contents));
}

try {
expect(is_remote_module(path.join(temp, 'src', 'remote.js'))).toBe(true);
expect(is_remote_module(path.join(temp, 'src', 'remotely.js'))).toBe(false);
expect(is_remote_module('C:\\app\\src\\remote.js')).toBe(true);

const plain = path.join(temp, 'node_modules', 'plain');
write_package_json(plain, {
dependencies: {
'@sveltejs/kit': '*'
}
});
expect(is_remote_module(path.join(plain, 'dist', 'remote.js'))).toBe(false);

const with_peer = path.join(temp, 'node_modules', 'with-peer');
write_package_json(with_peer, {
peerDependencies: {
'@sveltejs/kit': '*'
}
});
expect(is_remote_module(path.join(with_peer, 'dist', 'remote.js'))).toBe(true);

const with_stub = path.join(temp, 'node_modules', 'with-stub');
write_package_json(with_stub, {
peerDependencies: {
'@sveltejs/kit': '*'
}
});
write_package_json(path.join(with_stub, 'dist'), {});
expect(is_remote_module(path.join(with_stub, 'dist', 'jwks', 'remote.js'))).toBe(true);

const pnpm_package = path.join(temp, 'node_modules', '.pnpm', 'x@1', 'node_modules', 'x');
write_package_json(pnpm_package, {
peerDependencies: {
'@sveltejs/kit': '*'
}
});
expect(is_remote_module(path.join(pnpm_package, 'dist', 'remote.js'))).toBe(true);
} finally {
fs.rmSync(temp, { recursive: true, force: true });
}
});

test('error_for_missing_config - simple single level config', () => {
expect(() => error_for_missing_config('feature', 'adapter', 'true')).toThrow(
dedent`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { create_key_set } from './jwks/remote.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function create_key_set(url) {
return `key set for ${url}`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "e2e-test-dep-plain",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "index.js",
"svelte": "./index.js"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"private": true,
"type": "module",
"main": "index.js",
"svelte": "./index.js"
"svelte": "./index.js",
"peerDependencies": {
"@sveltejs/kit": "*"
}
}
1 change: 1 addition & 0 deletions packages/kit/test/apps/async/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"typescript": "catalog:",
"valibot": "catalog:",
"vite": "catalog:",
"e2e-test-dep-plain": "file:./_test_dependencies/plain-lib",
"e2e-test-dep-remote": "file:./_test_dependencies/remote-lib"
},
"imports": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// @ts-ignore
import { create_key_set } from 'e2e-test-dep-plain';

export function load() {
return { key_set: create_key_set('https://example.com/jwks') };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script>
let { data } = $props();
</script>

<p>{data.key_set}</p>
5 changes: 5 additions & 0 deletions packages/kit/test/apps/async/test/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ test.describe('remote functions', () => {
await page.getByRole('button', { name: 'call remote function' }).click();
await expect(page.locator('p')).toHaveText('lib says client');
});

test('packages can contain ordinary remote.js files', async ({ page }) => {
await page.goto('/plain-lib');
await expect(page.locator('p')).toHaveText('key set for https://example.com/jwks');
});
});

// have to run in serial because commands mutate in-memory data on the server (should fix this at some point)
Expand Down
16 changes: 14 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading