diff --git a/.changeset/chubby-shirts-run.md b/.changeset/chubby-shirts-run.md new file mode 100644 index 000000000000..a6f8c628f839 --- /dev/null +++ b/.changeset/chubby-shirts-run.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: make illegal server-only import errors actually useful diff --git a/packages/kit/src/exports/vite/graph_analysis/index.js b/packages/kit/src/exports/vite/graph_analysis/index.js deleted file mode 100644 index ecf7ee146769..000000000000 --- a/packages/kit/src/exports/vite/graph_analysis/index.js +++ /dev/null @@ -1,87 +0,0 @@ -import path from 'node:path'; -import { posixify } from '../../../utils/filesystem.js'; -import { normalize_id, strip_virtual_prefix } from '../utils.js'; -import { app_server, env_dynamic_private, env_static_private } from '../module_ids.js'; - -const ILLEGAL_IMPORTS = new Set([env_dynamic_private, env_static_private, app_server]); -const ILLEGAL_MODULE_NAME_PATTERN = /.*\.server\..+/; - -/** - * Checks if given id imports a module that is not allowed to be imported into client-side code. - * @param {string} id - * @param {{ - * cwd: string; - * node_modules: string; - * server: string; - * }} dirs - */ -export function is_illegal(id, dirs) { - if (ILLEGAL_IMPORTS.has(id)) return true; - if (!id.startsWith(dirs.cwd) || id.startsWith(dirs.node_modules)) return false; - return ILLEGAL_MODULE_NAME_PATTERN.test(path.basename(id)) || id.startsWith(dirs.server); -} - -/** - * Creates a guard that checks that no id imports a module that is not allowed to be imported into client-side code. - * @param {import('vite').Rollup.PluginContext} context - * @param {{ cwd: string; lib: string }} paths - */ -export function module_guard(context, { cwd, lib }) { - /** @type {Set} */ - const seen = new Set(); - - const dirs = { - // ids will be posixified, so we need to posixify these, too - cwd: posixify(cwd), - node_modules: posixify(path.join(cwd, 'node_modules')), - server: posixify(path.join(lib, 'server')) - }; - - /** - * @param {string} id - * @param {Array<{ id: string; dynamic: boolean }>} chain - */ - function follow(id, chain) { - if (seen.has(id)) return; - seen.add(id); - - if (is_illegal(id, dirs)) { - chain.shift(); // discard the entry point - id = normalize_id(id, lib, cwd); - - const pyramid = - chain.map(({ id, dynamic }, i) => { - id = normalize_id(id, lib, cwd); - - return `${' '.repeat(i * 2)}- ${strip_virtual_prefix(id)} ${ - dynamic ? 'dynamically imports' : 'imports' - }\n`; - }) + `${' '.repeat(chain.length)}- ${strip_virtual_prefix(id)}`; - - const message = `Cannot import ${strip_virtual_prefix( - id - )} into client-side code:\n${pyramid}`; - - throw new Error(message); - } - - const module = context.getModuleInfo(id); - - if (module) { - for (const child of module.importedIds) { - follow(child, [...chain, { id, dynamic: false }]); - } - - for (const child of module.dynamicallyImportedIds) { - follow(child, [...chain, { id, dynamic: true }]); - } - } - } - - return { - /** @param {string} id should be posixified */ - check: (id) => { - follow(id, []); - } - }; -} diff --git a/packages/kit/src/exports/vite/graph_analysis/index.spec.js b/packages/kit/src/exports/vite/graph_analysis/index.spec.js deleted file mode 100644 index 35ecd31feb7b..000000000000 --- a/packages/kit/src/exports/vite/graph_analysis/index.spec.js +++ /dev/null @@ -1,166 +0,0 @@ -import { assert, test } from 'vitest'; -import { module_guard } from './index.js'; -import * as module_ids from '../module_ids.js'; - -/** - * - * @param {Record} graph - * @param {string} [expected_error] - */ -function check(graph, expected_error) { - // @ts-expect-error - const context = /** @type {import('vite').Rollup.PluginContext} */ ({ - /** @param {string} id */ - getModuleInfo(id) { - return { - importedIds: [], - dynamicallyImportedIds: [], - ...graph[id] - }; - } - }); - - const guard = module_guard(context, { - cwd: '~', - lib: '~/src/lib' - }); - - if (expected_error) { - try { - guard.check('~/src/entry'); - throw new Error('Expected an error'); - } catch (e) { - // @ts-expect-error - assert.equal(e.message, expected_error.replace(/^\t+/gm, '')); - } - } else { - guard.check('~/src/entry'); - } -} - -test('throws an error when importing $env/static/private', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['\0virtual:env/static/private'] - } - }, - `Cannot import $env/static/private into client-side code: - - src/routes/+page.svelte imports - - $env/static/private` - ); -}); - -test('throws an error when dynamically importing $env/static/private', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - dynamicallyImportedIds: ['\0virtual:env/static/private'] - } - }, - `Cannot import $env/static/private into client-side code: - - src/routes/+page.svelte dynamically imports - - $env/static/private` - ); -}); - -test('throws an error when importing $env/dynamic/private', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['\0virtual:env/dynamic/private'] - } - }, - `Cannot import $env/dynamic/private into client-side code: - - src/routes/+page.svelte imports - - $env/dynamic/private` - ); -}); - -test('throws an error when dynamically importing $env/dynamic/private', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - dynamicallyImportedIds: ['\0virtual:env/dynamic/private'] - } - }, - `Cannot import $env/dynamic/private into client-side code: - - src/routes/+page.svelte dynamically imports - - $env/dynamic/private` - ); -}); - -// nginx does not process URLs containing the sequence `:$` -test('":$" is not in virtual module ids', () => { - assert.notInclude(Object.values(module_ids).join(''), ':$'); -}); - -test('throws an error when importing a .server.js module', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['~/src/routes/illegal.server.js'] - }, - '~/src/routes/illegal.server.js': {} - }, - `Cannot import src/routes/illegal.server.js into client-side code: - - src/routes/+page.svelte imports - - src/routes/illegal.server.js` - ); -}); - -test('throws an error when importing a $lib/server/**/*.js module', () => { - check( - { - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['~/src/lib/server/some/module.js'] - }, - '~/src/lib/server/some/module.js': {} - }, - `Cannot import $lib/server/some/module.js into client-side code: - - src/routes/+page.svelte imports - - $lib/server/some/module.js` - ); -}); - -test('ignores .server.js files in node_modules', () => { - check({ - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['~/node_modules/illegal.server.js'] - }, - '~/node_modules/illegal.server.js': {} - }); -}); - -test('ignores .server.js files outside the project root', () => { - check({ - '~/src/entry': { - importedIds: ['~/src/routes/+page.svelte'] - }, - '~/src/routes/+page.svelte': { - importedIds: ['/illegal.server.js'] - }, - '/illegal.server.js': {} - }); -}); diff --git a/packages/kit/src/exports/vite/graph_analysis/types.d.ts b/packages/kit/src/exports/vite/graph_analysis/types.d.ts deleted file mode 100644 index 1239fec437e1..000000000000 --- a/packages/kit/src/exports/vite/graph_analysis/types.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface ImportGraph { - readonly id: string; - readonly dynamic: boolean; - readonly children: Generator; -} diff --git a/packages/kit/src/exports/vite/graph_analysis/utils.js b/packages/kit/src/exports/vite/graph_analysis/utils.js deleted file mode 100644 index bdfa7a42b058..000000000000 --- a/packages/kit/src/exports/vite/graph_analysis/utils.js +++ /dev/null @@ -1,6 +0,0 @@ -const query_pattern = /\?.*$/s; - -/** @param {string} path */ -export function remove_query_from_id(path) { - return path.replace(query_pattern, ''); -} diff --git a/packages/kit/src/exports/vite/graph_analysis/utils.spec.js b/packages/kit/src/exports/vite/graph_analysis/utils.spec.js deleted file mode 100644 index f4b609c03ab7..000000000000 --- a/packages/kit/src/exports/vite/graph_analysis/utils.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -import { assert, describe } from 'vitest'; -import { remove_query_from_id } from './utils.js'; - -describe('remove_query_string_from_path', (test) => { - const module_ids = [ - '$env/static/private', - 'some-normal-js-module.js', - 'c:\\\\some\\stupid\\windows\\path.js', - '/some/normal/linux/path.js' - ]; - const query_module_ids = module_ids.map((module_id) => `${module_id}?hello=world,something=else`); - - test('does nothing to valid IDs', () => { - module_ids.forEach((id) => { - const query_stringless = remove_query_from_id(id); - assert.equal(query_stringless, id); - }); - }); - - test('removes querystring from paths with querystrings at the end', () => { - query_module_ids.forEach((id, i) => { - const query_stringless = remove_query_from_id(id); - assert.equal(query_stringless, module_ids[i]); - }); - }); -}); diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 31a79718fcb1..352a8705fb62 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -15,9 +15,8 @@ import { build_server_nodes } from './build/build_server.js'; import { build_service_worker } from './build/build_service_worker.js'; import { assets_base, find_deps, resolve_symlinks } from './build/utils.js'; import { dev } from './dev/index.js'; -import { is_illegal, module_guard } from './graph_analysis/index.js'; import { preview } from './preview/index.js'; -import { get_config_aliases, get_env, normalize_id, strip_virtual_prefix } from './utils.js'; +import { get_config_aliases, get_env, normalize_id, stackless } from './utils.js'; import { write_client_manifest } from '../../core/sync/write_client_manifest.js'; import prerender from '../../core/postbuild/prerender.js'; import analyse from '../../core/postbuild/analyse.js'; @@ -219,6 +218,7 @@ async function kit({ svelte_config }) { const normalized_cwd = vite.normalizePath(cwd); const normalized_lib = vite.normalizePath(kit.files.lib); + const normalized_node_modules = vite.normalizePath(path.resolve('node_modules')); /** * A map showing which features (such as `$app/server:read`) are defined @@ -374,9 +374,6 @@ async function kit({ svelte_config }) { } }; - /** @type {Map} */ - const import_map = new Map(); - /** @type {import('vite').Plugin} */ const plugin_virtual_modules = { name: 'vite-plugin-sveltekit-virtual-modules', @@ -389,6 +386,7 @@ async function kit({ svelte_config }) { // If importing from a service-worker, only allow $service-worker & $env/static/public, but none of the other virtual modules. // This check won't catch transitive imports, but it will warn when the import comes from a service-worker directly. // Transitive imports will be caught during the build. + // TODO move this logic to plugin_guard if (importer) { const parsed_importer = path.parse(importer); @@ -405,8 +403,6 @@ async function kit({ svelte_config }) { )} into service-worker code. Only the modules $service-worker and $env/static/public are available in service workers.` ); } - - import_map.set(id, importer); } // treat $env/static/[public|private] as virtual @@ -414,9 +410,11 @@ async function kit({ svelte_config }) { // ids with :$ don't work with reverse proxies like nginx return `\0virtual:${id.substring(1)}`; } + if (id === '__sveltekit/remote') { return `${runtime_directory}/client/remote-functions/index.js`; } + if (id.startsWith('__sveltekit/')) { return `\0virtual:${id}`; } @@ -429,37 +427,6 @@ async function kit({ svelte_config }) { ? `globalThis.__sveltekit_${version_hash}` : 'globalThis.__sveltekit_dev'; - if (options?.ssr === false && process.env.TEST !== 'true') { - if ( - is_illegal(id, { - cwd: normalized_cwd, - node_modules: vite.normalizePath(path.resolve('node_modules')), - server: vite.normalizePath(path.join(normalized_lib, 'server')) - }) - ) { - const relative = normalize_id(id, normalized_lib, normalized_cwd); - - const illegal_module = strip_virtual_prefix(relative); - - const error_prefix = `Cannot import ${illegal_module} into client-side code. This could leak sensitive information.`; - const error_suffix = ` -Tips: - - To resolve this error, ensure that no exports from ${illegal_module} are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`; - - if (import_map.has(illegal_module)) { - const importer = path.relative( - cwd, - /** @type {string} */ (import_map.get(illegal_module)) - ); - throw new Error(`${error_prefix}\nImported by: ${importer}.${error_suffix}`); - } - - throw new Error(`${error_prefix}${error_suffix}`); - } - } - switch (id) { case env_static_private: return create_static_module('$env/static/private', env.private); @@ -566,6 +533,10 @@ Tips: } }; + /** @type {Map>} */ + const import_map = new Map(); + const server_only_pattern = /.*\.server\..+/; + /** * Ensures that client-side code can't accidentally import server-side code, * whether in `*.server.js` files, `$app/server`, `$lib/server`, or `$env/[static|dynamic]/private` @@ -574,23 +545,89 @@ Tips: const plugin_guard = { name: 'vite-plugin-sveltekit-guard', - writeBundle: { - sequential: true, - handler(_options) { - if (vite_config.build.ssr) return; + // Run this plugin before built-in resolution, so that relative imports + // are added to the module graph + enforce: 'pre', - const guard = module_guard(this, { - cwd: vite.normalizePath(process.cwd()), - lib: vite.normalizePath(kit.files.lib) - }); + async resolveId(id, importer) { + if (importer && !importer.endsWith('index.html')) { + const resolved = await this.resolve(id, importer, { skipSelf: true }); - manifest_data.nodes.forEach((_node, i) => { - const id = vite.normalizePath( - path.resolve(kit.outDir, `generated/client-optimized/nodes/${i}.js`) - ); + if (resolved) { + const normalized = normalize_id(resolved.id, normalized_lib, normalized_cwd); - guard.check(id); - }); + let importers = import_map.get(normalized); + + if (!importers) { + importers = new Set(); + import_map.set(normalized, importers); + } + + importers.add(normalize_id(importer, normalized_lib, normalized_cwd)); + } + } + }, + + load(id, options) { + if (options?.ssr === true || process.env.TEST === 'true') { + return; + } + + // skip .server.js files outside the cwd or in node_modules, as the filename might not mean 'server-only module' in this context + const is_internal = id.startsWith(normalized_cwd) && !id.startsWith(normalized_node_modules); + + const normalized = normalize_id(id, normalized_lib, normalized_cwd); + + const is_server_only = + normalized === '$env/static/private' || + normalized === '$env/dynamic/private' || + normalized === '$app/server' || + normalized.startsWith('$lib/server') || + (is_internal && server_only_pattern.test(path.basename(id))); + + if (is_server_only) { + // in dev, this doesn't exist, so we need to create it + manifest_data ??= sync.all(svelte_config, vite_config_env.mode).manifest_data; + + /** @type {Set} */ + const entrypoints = new Set(); + for (const node of manifest_data.nodes) { + if (node.component) entrypoints.add(node.component); + if (node.universal) entrypoints.add(node.universal); + } + + const normalized = normalize_id(id, normalized_lib, normalized_cwd); + const chain = [normalized]; + + let current = normalized; + + while (true) { + const importers = import_map.get(current); + if (!importers) break; + + const candidates = Array.from(importers).filter((importer) => !chain.includes(importer)); + if (candidates.length === 0) break; + + chain.push((current = candidates[0])); + + if (entrypoints.has(current)) { + let message = `Cannot import ${normalized} into code that runs in the browser, as this could leak sensitive information.`; + + const pyramid = chain + .reverse() + .map((id, i) => { + return `${' '.repeat(i + 1)}${id}`; + }) + .join(' imports\n'); + + message += `\n\n${pyramid}`; + message += `\n\nIf you're only using the import as a type, change it to \`import type\`.`; + + throw stackless(message); + } + } + + throw new Error('An impossible situation occurred'); } } }; @@ -956,23 +993,36 @@ Tips: secondary_build_started = true; - const { output: client_chunks } = /** @type {import('vite').Rollup.RollupOutput} */ ( - await vite.build({ - configFile: vite_config.configFile, - // CLI args - mode: vite_config_env.mode, - logLevel: vite_config.logLevel, - clearScreen: vite_config.clearScreen, - build: { - minify: initial_config.build?.minify, - assetsInlineLimit: vite_config.build.assetsInlineLimit, - sourcemap: vite_config.build.sourcemap - }, - optimizeDeps: { - force: vite_config.optimizeDeps.force - } - }) - ); + let client_chunks; + + try { + const bundle = /** @type {import('vite').Rollup.RollupOutput} */ ( + await vite.build({ + configFile: vite_config.configFile, + // CLI args + mode: vite_config_env.mode, + logLevel: vite_config.logLevel, + clearScreen: vite_config.clearScreen, + build: { + minify: initial_config.build?.minify, + assetsInlineLimit: vite_config.build.assetsInlineLimit, + sourcemap: vite_config.build.sourcemap + }, + optimizeDeps: { + force: vite_config.optimizeDeps.force + } + }) + ); + + client_chunks = bundle.output; + } catch (e) { + const error = + e instanceof Error ? e : new Error(/** @type {any} */ (e).message ?? e ?? ''); + + // without this, errors that occur during the secondary build + // will be logged twice + throw stackless(error.stack ?? error.message); + } copy( `${out}/server/${kit.appDir}/immutable/assets`, diff --git a/packages/kit/src/exports/vite/utils.js b/packages/kit/src/exports/vite/utils.js index 02916e4d85c5..cf6223e6c632 100644 --- a/packages/kit/src/exports/vite/utils.js +++ b/packages/kit/src/exports/vite/utils.js @@ -113,6 +113,8 @@ export function not_found(req, res, base) { } } +const query_pattern = /\?.*$/s; + /** * Removes cwd/lib path from the start of the id * @param {string} id @@ -120,6 +122,8 @@ export function not_found(req, res, base) { * @param {string} cwd */ export function normalize_id(id, lib, cwd) { + id = id.replace(query_pattern, ''); + if (id.startsWith(lib)) { id = id.replace(lib, '$lib'); } @@ -155,4 +159,16 @@ export function normalize_id(id, lib, cwd) { return posixify(id); } +/** + * For times when you need to throw an error, but without + * displaying a useless stack trace (since the developer + * can't do anything useful with it) + * @param {string} message + */ +export function stackless(message) { + const error = new Error(message); + error.stack = ''; + return error; +} + export const strip_virtual_prefix = /** @param {string} id */ (id) => id.replace('\0virtual:', ''); diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/dynamic-private-dynamic-import/+page.svelte b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/dynamic-private-dynamic-import/+page.svelte deleted file mode 100644 index d5a365067afe..000000000000 --- a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/dynamic-private-dynamic-import/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{#await p then envModule} -

{envModule.env.SHOULD_EXPLODE}

-{/await} diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/static-private-dynamic-import/+page.svelte b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/static-private-dynamic-import/+page.svelte deleted file mode 100644 index 5b5bcda6a886..000000000000 --- a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/env/static-private-dynamic-import/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{#await p then envModule} -

{envModule.SHOULD_EXPLODE}

-{/await} diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-folder/dynamic-import/+page.svelte b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-folder/dynamic-import/+page.svelte deleted file mode 100644 index 5fa759e0272f..000000000000 --- a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-folder/dynamic-import/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{#await mod then resolved} -

{resolved.should_explode}

-{/await} diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/dynamic-import/+page.svelte b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/dynamic-import/+page.svelte deleted file mode 100644 index 5e763368e26f..000000000000 --- a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/dynamic-import/+page.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{#await mod then resolved} -

{resolved.should_explode}

-{/await} diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/+page.svelte b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/+page.svelte index 43bb66e92f6d..eb91c6f3058f 100644 --- a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/+page.svelte +++ b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/+page.svelte @@ -1,5 +1,5 @@

{should_explode}

diff --git a/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/foo.js b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/foo.js new file mode 100644 index 000000000000..ac86daab308e --- /dev/null +++ b/packages/kit/test/apps/dev-only/src/routes/illegal-imports/server-only-modules/static-import/foo.js @@ -0,0 +1 @@ +export { should_explode } from '../illegal.server.js'; diff --git a/packages/kit/test/apps/dev-only/test/test.js b/packages/kit/test/apps/dev-only/test/test.js index 8d296daf8406..07daa6ac2482 100644 --- a/packages/kit/test/apps/dev-only/test/test.js +++ b/packages/kit/test/apps/dev-only/test/test.js @@ -12,116 +12,70 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); test.describe.serial('Illegal imports', () => { test.skip(({ javaScriptEnabled }) => !process.env.DEV || !javaScriptEnabled); - test('$env/dynamic/private is not statically importable from the client', async ({ page }) => { + test('$env/dynamic/private is not importable from the client', async ({ page }) => { await page.goto('/illegal-imports/env/dynamic-private', { wait_for_started: false }); expect(await page.textContent('.message-body')) - .toBe(`Cannot import $env/dynamic/private into client-side code. This could leak sensitive information. -Imported by: src/routes/illegal-imports/env/dynamic-private/+page.svelte. -Tips: - - To resolve this error, ensure that no exports from $env/dynamic/private are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); - }); + .toBe(`Cannot import $env/dynamic/private into code that runs in the browser, as this could leak sensitive information. - test('$env/dynamic/private is not dynamically importable from the client', async ({ page }) => { - await page.goto('/illegal-imports/env/dynamic-private-dynamic-import', { - wait_for_started: false - }); - expect(await page.textContent('.message-body')) - .toBe(`Cannot import $env/dynamic/private into client-side code. This could leak sensitive information. -Imported by: src/routes/illegal-imports/env/dynamic-private-dynamic-import/+page.svelte. -Tips: - - To resolve this error, ensure that no exports from $env/dynamic/private are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); + src/routes/illegal-imports/env/dynamic-private/+page.svelte imports + $env/dynamic/private + +If you're only using the import as a type, change it to \`import type\`.`); }); - test('$env/static/private is not statically importable from the client', async ({ page }) => { + test('$env/static/private is not importable from the client', async ({ page }) => { await page.goto('/illegal-imports/env/static-private', { wait_for_started: false }); expect(await page.textContent('.message-body')) - .toBe(`Cannot import $env/static/private into client-side code. This could leak sensitive information. -Imported by: src/routes/illegal-imports/env/static-private/+page.svelte. -Tips: - - To resolve this error, ensure that no exports from $env/static/private are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); - }); + .toBe(`Cannot import $env/static/private into code that runs in the browser, as this could leak sensitive information. - test('$env/static/private is not dynamically importable from the client', async ({ page }) => { - await page.goto('/illegal-imports/env/static-private-dynamic-import', { - wait_for_started: false - }); - expect(await page.textContent('.message-body')) - .toBe(`Cannot import $env/static/private into client-side code. This could leak sensitive information. -Imported by: src/routes/illegal-imports/env/static-private-dynamic-import/+page.svelte. -Tips: - - To resolve this error, ensure that no exports from $env/static/private are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); + src/routes/illegal-imports/env/static-private/+page.svelte imports + $env/static/private + +If you're only using the import as a type, change it to \`import type\`.`); }); - test('server-only module is not statically importable from the client', async ({ page }) => { + test('server-only module is not importable from the client', async ({ page }) => { await page.goto('/illegal-imports/server-only-modules/static-import', { wait_for_started: false }); expect(await page.textContent('.message-body')) - .toBe(`Cannot import src/routes/illegal-imports/server-only-modules/illegal.server.js into client-side code. This could leak sensitive information. -Tips: - - To resolve this error, ensure that no exports from src/routes/illegal-imports/server-only-modules/illegal.server.js are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); + .toBe(`Cannot import src/routes/illegal-imports/server-only-modules/illegal.server.js into code that runs in the browser, as this could leak sensitive information. + + src/routes/illegal-imports/server-only-modules/static-import/+page.svelte imports + src/routes/illegal-imports/server-only-modules/static-import/foo.js imports + src/routes/illegal-imports/server-only-modules/illegal.server.js + +If you're only using the import as a type, change it to \`import type\`.`); }); - test('$app/server module is not statically importable from the client', async ({ page }) => { + test('$app/server module is not importable from the client', async ({ page }) => { await page.goto('/illegal-imports/server-only-modules/static-import-2', { wait_for_started: false }); expect(await page.textContent('.message-body')) - .toBe(`Cannot import $app/server into client-side code. This could leak sensitive information. -Tips: - - To resolve this error, ensure that no exports from $app/server are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); - }); + .toBe(`Cannot import $app/server into code that runs in the browser, as this could leak sensitive information. - test('server-only module is not dynamically importable from the client', async ({ page }) => { - await page.goto('/illegal-imports/server-only-modules/dynamic-import', { - wait_for_started: false - }); - expect(await page.textContent('.message-body')) - .toBe(`Cannot import src/routes/illegal-imports/server-only-modules/illegal.server.js into client-side code. This could leak sensitive information. -Tips: - - To resolve this error, ensure that no exports from src/routes/illegal-imports/server-only-modules/illegal.server.js are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); + src/routes/illegal-imports/server-only-modules/static-import-2/+page.svelte imports + $app/server + +If you're only using the import as a type, change it to \`import type\`.`); }); - test('server-only folder is not statically importable from the client', async ({ page }) => { + test('server-only folder is not importable from the client', async ({ page }) => { await page.goto('/illegal-imports/server-only-folder/static-import', { wait_for_started: false }); expect(await page.textContent('.message-body')) - .toBe(`Cannot import $lib/server/blah/private.js into client-side code. This could leak sensitive information. -Tips: - - To resolve this error, ensure that no exports from $lib/server/blah/private.js are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); - }); + .toBe(`Cannot import $lib/server/blah/private.js into code that runs in the browser, as this could leak sensitive information. - test('server-only folder is not dynamically importable from the client', async ({ page }) => { - await page.goto('/illegal-imports/server-only-folder/dynamic-import', { - wait_for_started: false - }); - expect(await page.textContent('.message-body')) - .toBe(`Cannot import $lib/server/blah/private.js into client-side code. This could leak sensitive information. -Tips: - - To resolve this error, ensure that no exports from $lib/server/blah/private.js are used, even transitively, in client-side code. - - If you're only using the import as a type, change it to \`import type\`. - - If you're not sure which module is causing this, try building your app -- it will create a more helpful error.`); + src/routes/illegal-imports/server-only-folder/static-import/+page.svelte imports + $lib/server/blah/private.js + +If you're only using the import as a type, change it to \`import type\`.`); }); }); diff --git a/packages/kit/test/build-errors/env.spec.js b/packages/kit/test/build-errors/env.spec.js index 64d21150f85a..aa41d4db6545 100644 --- a/packages/kit/test/build-errors/env.spec.js +++ b/packages/kit/test/build-errors/env.spec.js @@ -5,15 +5,20 @@ import process from 'node:process'; const timeout = 60_000; +// ordinarily server-only modules are allowed during testing, since Vitest can't differentiate +/** @type {Record} */ +const env = { ...process.env, TEST: false }; + test('$env/dynamic/private is not statically importable from the client', { timeout }, () => { assert.throws( () => execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/private-dynamic-env'), stdio: 'pipe', - timeout + timeout, + env }), - /.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs + /.*Cannot import \$env\/dynamic\/private into code that runs in the browser.*/gs ); }); @@ -23,9 +28,10 @@ test('$env/dynamic/private is not dynamically importable from the client', { tim execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/private-dynamic-env-dynamic-import'), stdio: 'pipe', - timeout + timeout, + env }), - /.*Cannot import \$env\/dynamic\/private into client-side code:.*/gs + /.*Cannot import \$env\/dynamic\/private into code that runs in the browser.*/gs ); }); @@ -35,9 +41,10 @@ test('$env/static/private is not statically importable from the client', { timeo execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/private-static-env'), stdio: 'pipe', - timeout + timeout, + env }), - /.*Cannot import \$env\/static\/private into client-side code:.*/gs + /.*Cannot import \$env\/static\/private into code that runs in the browser.*/gs ); }); @@ -47,9 +54,10 @@ test('$env/static/private is not dynamically importable from the client', { time execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/private-static-env-dynamic-import'), stdio: 'pipe', - timeout + timeout, + env }), - /.*Cannot import \$env\/static\/private into client-side code:.*/gs + /.*Cannot import \$env\/static\/private into code that runs in the browser.*/gs ); }); @@ -59,7 +67,8 @@ test('$env/dynamic/private is not importable from the service worker', { timeout execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/service-worker-private-env'), stdio: 'pipe', - timeout: 60000 + timeout, + env }), /.*Cannot import \$env\/dynamic\/private into service-worker code.*/gs ); @@ -71,7 +80,8 @@ test('$env/dynamic/public is not importable from the service worker', { timeout execSync('pnpm build', { cwd: path.join(process.cwd(), 'apps/service-worker-dynamic-public-env'), stdio: 'pipe', - timeout + timeout, + env }), /.*Cannot import \$env\/dynamic\/public into service-worker code.*/gs ); diff --git a/packages/kit/test/build-errors/server-only.spec.js b/packages/kit/test/build-errors/server-only.spec.js index 3275bb50e46c..67bccd96afd3 100644 --- a/packages/kit/test/build-errors/server-only.spec.js +++ b/packages/kit/test/build-errors/server-only.spec.js @@ -5,74 +5,58 @@ import process from 'node:process'; const timeout = 60_000; +// ordinarily server-only modules are allowed during testing, since Vitest can't differentiate +/** @type {Record} */ +const env = { ...process.env, TEST: false }; + test('$lib/*.server.* is not statically importable from the client', { timeout }, () => { - try { - execSync('pnpm build', { - cwd: path.join(process.cwd(), 'apps/server-only-module'), - stdio: 'pipe', - timeout - }); - } catch (err) { - const message = /** @type {Error} */ (err).message; - assert.ok( - message.includes('Cannot import $lib/test.server.js into client-side code'), - `received unexpected exception message ${message}` - ); - return; - } - throw new Error(); + assert.throws( + () => + execSync('pnpm build', { + cwd: path.join(process.cwd(), 'apps/server-only-module'), + stdio: 'pipe', + timeout, + env + }), + /.*Cannot import \$lib\/test.server.js into code that runs in the browser.*/gs + ); }); test('$lib/*.server.* is not dynamically importable from the client', { timeout }, () => { - try { - execSync('pnpm build', { - cwd: path.join(process.cwd(), 'apps/server-only-module-dynamic-import'), - stdio: 'pipe', - timeout - }); - } catch (err) { - const message = /** @type {Error} */ (err).message; - assert.ok( - message.includes('Cannot import $lib/test.server.js into client-side code'), - `received unexpected exception message ${message}` - ); - return; - } - throw new Error(); + assert.throws( + () => + execSync('pnpm build', { + cwd: path.join(process.cwd(), 'apps/server-only-module-dynamic-import'), + stdio: 'pipe', + timeout, + env + }), + /.*Cannot import \$lib\/test.server.js into code that runs in the browser.*/gs + ); }); test('$lib/server/* is not statically importable from the client', { timeout }, () => { - try { - execSync('pnpm build', { - cwd: path.join(process.cwd(), 'apps/server-only-folder'), - stdio: 'pipe', - timeout - }); - } catch (err) { - const message = /** @type {Error} */ (err).message; - assert.ok( - message.includes('Cannot import $lib/server/something/private.js into client-side code'), - `received unexpected exception message ${message}` - ); - return; - } - throw new Error(); + assert.throws( + () => + execSync('pnpm build', { + cwd: path.join(process.cwd(), 'apps/server-only-folder'), + stdio: 'pipe', + timeout, + env + }), + /.*Cannot import \$lib\/server\/something\/private.js into code that runs in the browser.*/gs + ); }); test('$lib/server/* is not dynamically importable from the client', { timeout }, () => { - try { - execSync('pnpm build', { - cwd: path.join(process.cwd(), 'apps/server-only-folder-dynamic-import'), - stdio: 'pipe', - timeout - }); - } catch (err) { - const message = /** @type {Error} */ (err).message; - assert.ok( - message.includes('Cannot import $lib/server/something/private.js into client-side code'), - `received unexpected exception message ${message}` - ); - return; - } - throw new Error(); + assert.throws( + () => + execSync('pnpm build', { + cwd: path.join(process.cwd(), 'apps/server-only-folder-dynamic-import'), + stdio: 'pipe', + timeout, + env + }), + /.*Cannot import \$lib\/server\/something\/private.js into code that runs in the browser.*/gs + ); });