diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 90da427483d7..ed0c5c47732a 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -97,6 +97,9 @@ const get_defaults = (prefix = '') => ({ moduleExtensions: ['.js', '.ts'], output: { preloadStrategy: 'modulepreload', bundleStrategy: 'split' }, outDir: join(prefix, '.svelte-kit'), + remoteFunctions: { + allowedPaths: [] + }, router: { type: 'pathname', resolution: 'client' diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 577ca4c9445d..948032370c41 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -264,6 +264,10 @@ const options = object( }) }), + remoteFunctions: object({ + allowedPaths: string_array([]) + }), + router: object({ type: list(['pathname', 'hash']), resolution: list(['client', 'server']) diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index a121ac189be0..91f48f672dfd 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -480,8 +480,10 @@ function create_remotes(config, cwd) { /** @type {import('types').ManifestData['remotes']} */ const remotes = []; + const externals = config.kit.remoteFunctions.allowedPaths.map((dir) => path.resolve(dir)); + // TODO could files live in other directories, including node_modules? - for (const dir of [config.kit.files.lib, config.kit.files.routes]) { + for (const dir of [config.kit.files.lib, config.kit.files.routes, ...externals]) { if (!fs.existsSync(dir)) continue; for (const file of walk(dir)) { diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index df3996a5bf69..82fcf2ea056f 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -657,6 +657,15 @@ export interface KitConfig { */ origin?: string; }; + remoteFunctions?: { + /** + * A list of external paths that are allowed to provide remote functions. + * By default, remote functions are only allowed inside the `routes` and `lib` folders. + * + * Accepts absolute paths or paths relative to the project root. + */ + allowedPaths?: string[]; + }; router?: { /** * What type of client-side router to use. diff --git a/packages/kit/src/exports/vite/dev/index.js b/packages/kit/src/exports/vite/dev/index.js index a5f211efa9af..1366f68b77eb 100644 --- a/packages/kit/src/exports/vite/dev/index.js +++ b/packages/kit/src/exports/vite/dev/index.js @@ -29,9 +29,10 @@ const vite_css_query_regex = /(?:\?|&)(?:raw|url|inline)(?:&|$)/; * @param {import('vite').ViteDevServer} vite * @param {import('vite').ResolvedConfig} vite_config * @param {import('types').ValidatedConfig} svelte_config + * @param {(manifest_data: import('types').ManifestData) => void} manifest_cb * @return {Promise void>>} */ -export async function dev(vite, vite_config, svelte_config) { +export async function dev(vite, vite_config, svelte_config, manifest_cb) { installPolyfills(); const async_local_storage = new AsyncLocalStorage(); @@ -108,7 +109,7 @@ export async function dev(vite, vite_config, svelte_config) { function update_manifest() { try { ({ manifest_data } = sync.create(svelte_config)); - + manifest_cb(manifest_data); if (manifest_error) { manifest_error = null; vite.ws.send({ type: 'full-reload' }); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 7486574c674d..c608a455c143 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -35,7 +35,7 @@ import { sveltekit_server } from './module_ids.js'; import { import_peer } from '../../utils/import.js'; -import { compact } from '../../utils/array.js'; +import { compact, conjoin } from '../../utils/array.js'; import { build_remotes, treeshake_prerendered_remotes } from './build/build_remote.js'; const cwd = process.cwd(); @@ -661,6 +661,24 @@ Tips: } } + if (!manifest_data.remotes.some((remote) => remote.hash === hashed)) { + const relative_path = path.relative(dev_server.config.root, id); + const fn_names = [...remotes.values()].flat().map((name) => `"${name}"`); + const has_multiple = fn_names.length !== 1; + console.warn( + colors + .bold() + .yellow( + `Remote function${has_multiple ? 's' : ''} ${conjoin(fn_names)} from ${relative_path} ${has_multiple ? 'are' : 'is'} not accessible by default.` + ) + ); + console.warn( + colors.yellow( + `To whitelist ${has_multiple ? 'them' : 'it'}, add "${path.dirname(relative_path)}" to \`kit.remoteFunctions.allowedPaths\` in \`svelte.config.js\`.` + ) + ); + } + let namespace = '__remote'; let uid = 1; while (remotes.has(namespace)) namespace = `__remote${uid++}`; @@ -835,7 +853,9 @@ Tips: * @see https://vitejs.dev/guide/api-plugin.html#configureserver */ async configureServer(vite) { - return await dev(vite, vite_config, svelte_config); + return await dev(vite, vite_config, svelte_config, (_manifest_data) => { + manifest_data = _manifest_data; + }); }, /** diff --git a/packages/kit/src/runtime/server/cookie.js b/packages/kit/src/runtime/server/cookie.js index 2e683543a534..9019c86f0517 100644 --- a/packages/kit/src/runtime/server/cookie.js +++ b/packages/kit/src/runtime/server/cookie.js @@ -1,4 +1,5 @@ import { parse, serialize } from 'cookie'; +import { conjoin } from '../../utils/array.js'; import { normalize_path, resolve } from '../../utils/url.js'; import { add_data_suffix } from '../pathname.js'; @@ -286,11 +287,3 @@ export function add_cookies_to_headers(headers, cookies) { } } } - -/** - * @param {string[]} array - */ -function conjoin(array) { - if (array.length <= 2) return array.join(' and '); - return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`; -} diff --git a/packages/kit/src/utils/array.js b/packages/kit/src/utils/array.js index 08f93845149b..1d6dc4b6abce 100644 --- a/packages/kit/src/utils/array.js +++ b/packages/kit/src/utils/array.js @@ -7,3 +7,12 @@ export function compact(arr) { return arr.filter(/** @returns {val is NonNullable} */ (val) => val != null); } + +/** + * Joins an array of strings with commas and 'and'. + * @param {string[]} array + */ +export function conjoin(array) { + if (array.length <= 2) return array.join(' and '); + return `${array.slice(0, -1).join(', ')} and ${array.at(-1)}`; +} diff --git a/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js b/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js new file mode 100644 index 000000000000..5c883ceee850 --- /dev/null +++ b/packages/kit/test/apps/basics/src/external-not-accessible/external.remote.js @@ -0,0 +1,3 @@ +import { query } from '$app/server'; + +export const external_not_accessible = query(async () => 'external failure'); diff --git a/packages/kit/test/apps/basics/src/external-remotes/external.remote.js b/packages/kit/test/apps/basics/src/external-remotes/external.remote.js new file mode 100644 index 000000000000..ea5b438b138c --- /dev/null +++ b/packages/kit/test/apps/basics/src/external-remotes/external.remote.js @@ -0,0 +1,3 @@ +import { query } from '$app/server'; + +export const external = query(async () => 'external success'); diff --git a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte index a05f83efb528..fa54e28903cd 100644 --- a/packages/kit/test/apps/basics/src/routes/remote/+page.svelte +++ b/packages/kit/test/apps/basics/src/routes/remote/+page.svelte @@ -1,6 +1,8 @@

{data.echo_result}

@@ -65,3 +69,15 @@ + +

+ {#await external_result then result}{result}{/await} +

+ +

+ {#await external_failure then result} + {result} + {:catch error} + {error} + {/await} +

diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js index 2410ff83d57f..ac4aeaf51b7c 100644 --- a/packages/kit/test/apps/basics/svelte.config.js +++ b/packages/kit/test/apps/basics/svelte.config.js @@ -41,8 +41,13 @@ const config = { version: { name: 'TEST_VERSION' }, + router: { resolution: /** @type {'client' | 'server'} */ (process.env.ROUTER_RESOLUTION) || 'client' + }, + + remoteFunctions: { + allowedPaths: ['src/external-remotes'] } } }; diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js index c1fc7d0fb5b6..c6a2c0154c73 100644 --- a/packages/kit/test/apps/basics/test/client.test.js +++ b/packages/kit/test/apps/basics/test/client.test.js @@ -1834,4 +1834,11 @@ test.describe('remote functions', () => { await page.click('button:nth-of-type(4)'); await expect(page.locator('p')).toHaveText('success'); }); + + test('external remotes work', async ({ page }) => { + await page.goto('/remote'); + await expect(page.locator('#external-success')).toHaveText('external success'); + await expect(page.locator('#external-failure')).not.toHaveText('external failure'); + await expect(page.locator('#external-failure')).toHaveText('Failed to execute remote function'); + }); }); diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 7781c0ab58fa..4a188b3a5c77 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -634,6 +634,15 @@ declare module '@sveltejs/kit' { */ origin?: string; }; + remoteFunctions?: { + /** + * A list of external paths that are allowed to provide remote functions. + * By default, remote functions are only allowed inside the `routes` and `lib` folders. + * + * Accepts absolute paths or paths relative to the project root. + */ + allowedPaths?: string[]; + }; router?: { /** * What type of client-side router to use.