Skip to content

Commit 0032048

Browse files
feat: Disallow import of user-designated server-only modules from the client (sveltejs#6422)
* feat: Failing tests * feat: Dev-only tests * feat: It works...? * changeset * fix: Don't re-resolve $lib * fix: Dumb test * fix: More dumb tests * fix: Even more dumb tests * fix: wut * fix: vscode autoimport is a miserable bully * fix: Workspace? * fix: format * fix: jsconfig => tsconfig * fix: Some svelte-check misery * fix: noEmit * fix: Windows paths are miserable funsuckers * feat: Better documentation of server-only stuff * Revert "feat: Better documentation of server-only stuff" This reverts commit 6b584b8. * feat: Switch to "not in node_modules" logic * broken lockfile, apparently * fix * Update packages/kit/src/exports/vite/utils.js Co-authored-by: Rich Harris <[email protected]> * simplify * simplify * lockfile * DRY out, ignore files outside project root * bruh * bruh 2.0 * bruh 3.0 * Update packages/kit/src/exports/vite/utils.js Co-authored-by: Rich Harris <[email protected]> Co-authored-by: Rich Harris <[email protected]>
1 parent 55e9151 commit 0032048

File tree

43 files changed

+456
-72
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+456
-72
lines changed

.changeset/perfect-rules-burn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
[breaking] Allow users to designate modules as server-only

packages/kit/src/exports/vite/dev/index.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as sync from '../../../core/sync/sync.js';
1313
import { get_mime_lookup, runtime_base, runtime_prefix } from '../../../core/utils.js';
1414
import { prevent_illegal_vite_imports, resolve_entry } from '../utils.js';
1515
import { compact } from '../../../utils/array.js';
16+
import { normalizePath } from 'vite';
1617

1718
// Vite doesn't expose this so we just copy the list for now
1819
// https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96
@@ -24,10 +25,9 @@ const cwd = process.cwd();
2425
* @param {import('vite').ViteDevServer} vite
2526
* @param {import('vite').ResolvedConfig} vite_config
2627
* @param {import('types').ValidatedConfig} svelte_config
27-
* @param {Set<string>} illegal_imports
2828
* @return {Promise<Promise<() => void>>}
2929
*/
30-
export async function dev(vite, vite_config, svelte_config, illegal_imports) {
30+
export async function dev(vite, vite_config, svelte_config) {
3131
installPolyfills();
3232

3333
sync.init(svelte_config, vite_config.mode);
@@ -90,7 +90,11 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
9090
module_nodes.push(module_node);
9191
result.file = url.endsWith('.svelte') ? url : url + '?import'; // TODO what is this for?
9292

93-
prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
93+
prevent_illegal_vite_imports(
94+
module_node,
95+
normalizePath(svelte_config.kit.files.lib),
96+
extensions
97+
);
9498

9599
return module.default;
96100
};
@@ -103,7 +107,11 @@ export async function dev(vite, vite_config, svelte_config, illegal_imports) {
103107

104108
result.shared = module;
105109

106-
prevent_illegal_vite_imports(module_node, illegal_imports, extensions);
110+
prevent_illegal_vite_imports(
111+
module_node,
112+
normalizePath(svelte_config.kit.files.lib),
113+
extensions
114+
);
107115
}
108116

109117
if (node.server) {

packages/kit/src/exports/vite/index.js

+2-12
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,6 @@ function kit() {
103103
/** @type {import('types').BuildData} */
104104
let build_data;
105105

106-
/** @type {Set<string>} */
107-
let illegal_imports;
108-
109106
/** @type {string | undefined} */
110107
let deferred_warning;
111108

@@ -212,13 +209,6 @@ function kit() {
212209
client_out_dir: `${svelte_config.kit.outDir}/output/client`
213210
};
214211

215-
illegal_imports = new Set([
216-
'/@id/__x00__$env/dynamic/private', //dev
217-
'\0$env/dynamic/private', // prod
218-
'/@id/__x00__$env/static/private', // dev
219-
'\0$env/static/private' // prod
220-
]);
221-
222212
if (is_build) {
223213
manifest_data = (await sync.all(svelte_config, config_env.mode)).manifest_data;
224214

@@ -361,7 +351,7 @@ function kit() {
361351
prevent_illegal_rollup_imports(
362352
this.getModuleInfo.bind(this),
363353
module_node,
364-
illegal_imports
354+
vite.normalizePath(svelte_config.kit.files.lib)
365355
);
366356
}
367357
});
@@ -517,7 +507,7 @@ function kit() {
517507
if (deferred_warning) console.error('\n' + deferred_warning);
518508
};
519509

520-
return await dev(vite, vite_config, svelte_config, illegal_imports);
510+
return await dev(vite, vite_config, svelte_config);
521511
},
522512

523513
/**

packages/kit/src/exports/vite/utils.js

+42-26
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ import { loadConfigFromFile, loadEnv, normalizePath } from 'vite';
44
import { runtime_directory } from '../../core/utils.js';
55
import { posixify } from '../../utils/filesystem.js';
66

7+
const illegal_imports = new Set([
8+
'/@id/__x00__$env/dynamic/private', //dev
9+
'\0$env/dynamic/private', // prod
10+
'/@id/__x00__$env/static/private', // dev
11+
'\0$env/static/private' // prod
12+
]);
13+
14+
/** @param {string} id */
15+
function is_illegal(id) {
16+
if (illegal_imports.has(id)) return true;
17+
18+
// files outside the project root are ignored
19+
if (!id.startsWith(normalizePath(process.cwd()))) return false;
20+
21+
// so are files inside node_modules
22+
if (id.startsWith(normalizePath(node_modules_dir))) return false;
23+
24+
return /.*\.server\..+/.test(path.basename(id));
25+
}
26+
727
/**
828
* @param {import('vite').ResolvedConfig} config
929
* @param {import('vite').ConfigEnv} config_env
@@ -183,8 +203,9 @@ function repeat(str, times) {
183203
/**
184204
* Create a formatted error for an illegal import.
185205
* @param {Array<{name: string, dynamic: boolean}>} stack
206+
* @param {string} lib_dir
186207
*/
187-
function format_illegal_import_chain(stack) {
208+
function format_illegal_import_chain(stack, lib_dir) {
188209
const dev_virtual_prefix = '/@id/__x00__';
189210
const prod_virtual_prefix = '\0';
190211

@@ -195,6 +216,9 @@ function format_illegal_import_chain(stack) {
195216
if (file.name.startsWith(prod_virtual_prefix)) {
196217
return { ...file, name: file.name.replace(prod_virtual_prefix, '') };
197218
}
219+
if (file.name.startsWith(lib_dir)) {
220+
return { ...file, name: file.name.replace(lib_dir, '$lib') };
221+
}
198222

199223
return { ...file, name: path.relative(process.cwd(), file.name) };
200224
});
@@ -211,6 +235,8 @@ function format_illegal_import_chain(stack) {
211235
return `Cannot import ${stack.at(-1)?.name} into client-side code:\n${pyramid}`;
212236
}
213237

238+
const node_modules_dir = path.resolve(process.cwd(), 'node_modules');
239+
214240
/**
215241
* Load environment variables from process.env and .env files
216242
* @param {import('types').ValidatedKitConfig['env']} env_config
@@ -228,11 +254,11 @@ export function get_env(env_config, mode) {
228254
/**
229255
* @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
230256
* @param {import('rollup').ModuleInfo} node
231-
* @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
257+
* @param {string} lib_dir
232258
*/
233-
export function prevent_illegal_rollup_imports(node_getter, node, illegal_imports) {
234-
const chain = find_illegal_rollup_imports(node_getter, node, false, illegal_imports);
235-
if (chain) throw new Error(format_illegal_import_chain(chain));
259+
export function prevent_illegal_rollup_imports(node_getter, node, lib_dir) {
260+
const chain = find_illegal_rollup_imports(node_getter, node, false);
261+
if (chain) throw new Error(format_illegal_import_chain(chain, lib_dir));
236262
}
237263

238264
const query_pattern = /\?.*$/s;
@@ -246,36 +272,27 @@ function remove_query_from_path(path) {
246272
* @param {(id: string) => import('rollup').ModuleInfo | null} node_getter
247273
* @param {import('rollup').ModuleInfo} node
248274
* @param {boolean} dynamic
249-
* @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
250275
* @param {Set<string>} seen
251276
* @returns {Array<import('types').ImportNode> | null}
252277
*/
253-
const find_illegal_rollup_imports = (
254-
node_getter,
255-
node,
256-
dynamic,
257-
illegal_imports,
258-
seen = new Set()
259-
) => {
278+
const find_illegal_rollup_imports = (node_getter, node, dynamic, seen = new Set()) => {
260279
const name = remove_query_from_path(normalizePath(node.id));
261280
if (seen.has(name)) return null;
262281
seen.add(name);
263282

264-
if (illegal_imports.has(name)) {
283+
if (is_illegal(name)) {
265284
return [{ name, dynamic }];
266285
}
267286

268287
for (const id of node.importedIds) {
269288
const child = node_getter(id);
270-
const chain =
271-
child && find_illegal_rollup_imports(node_getter, child, false, illegal_imports, seen);
289+
const chain = child && find_illegal_rollup_imports(node_getter, child, false, seen);
272290
if (chain) return [{ name, dynamic }, ...chain];
273291
}
274292

275293
for (const id of node.dynamicallyImportedIds) {
276294
const child = node_getter(id);
277-
const chain =
278-
child && find_illegal_rollup_imports(node_getter, child, true, illegal_imports, seen);
295+
const chain = child && find_illegal_rollup_imports(node_getter, child, true, seen);
279296
if (chain) return [{ name, dynamic }, ...chain];
280297
}
281298

@@ -308,22 +325,21 @@ const get_module_types = (config_module_types) => {
308325
/**
309326
* Throw an error if a private module is imported from a client-side node.
310327
* @param {import('vite').ModuleNode} node
311-
* @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
328+
* @param {string} lib_dir
312329
* @param {Iterable<string>} module_types File extensions to analyze in addition to the defaults: `.ts`, `.js`, etc.
313330
*/
314-
export function prevent_illegal_vite_imports(node, illegal_imports, module_types) {
315-
const chain = find_illegal_vite_imports(node, illegal_imports, get_module_types(module_types));
316-
if (chain) throw new Error(format_illegal_import_chain(chain));
331+
export function prevent_illegal_vite_imports(node, lib_dir, module_types) {
332+
const chain = find_illegal_vite_imports(node, get_module_types(module_types));
333+
if (chain) throw new Error(format_illegal_import_chain(chain, lib_dir));
317334
}
318335

319336
/**
320337
* @param {import('vite').ModuleNode} node
321-
* @param {Set<string>} illegal_imports Illegal module IDs -- be sure to call vite.normalizePath!
322338
* @param {Set<string>} module_types File extensions to analyze: `.ts`, `.js`, etc.
323339
* @param {Set<string>} seen
324340
* @returns {Array<import('types').ImportNode> | null}
325341
*/
326-
function find_illegal_vite_imports(node, illegal_imports, module_types, seen = new Set()) {
342+
function find_illegal_vite_imports(node, module_types, seen = new Set()) {
327343
if (!node.id) return null; // TODO when does this happen?
328344
const name = remove_query_from_path(normalizePath(node.id));
329345

@@ -332,12 +348,12 @@ function find_illegal_vite_imports(node, illegal_imports, module_types, seen = n
332348
}
333349
seen.add(name);
334350

335-
if (name && illegal_imports.has(name)) {
351+
if (is_illegal(name)) {
336352
return [{ name, dynamic: false }];
337353
}
338354

339355
for (const child of node.importedModules) {
340-
const chain = child && find_illegal_vite_imports(child, illegal_imports, module_types, seen);
356+
const chain = child && find_illegal_vite_imports(child, module_types, seen);
341357
if (chain) return [{ name, dynamic: false }, ...chain];
342358
}
343359

packages/kit/src/exports/vite/utils.spec.js

+33-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from 'path';
22
import { test } from 'uvu';
33
import * as assert from 'uvu/assert';
4-
import { normalizePath } from 'vite';
54
import { validate_config } from '../../core/config/index.js';
65
import { posixify } from '../../utils/filesystem.js';
76
import {
@@ -12,6 +11,8 @@ import {
1211
prevent_illegal_vite_imports
1312
} from './utils.js';
1413

14+
const illegal_id = '/@id/__x00__$env/dynamic/private';
15+
1516
test('basic test no conflicts', async () => {
1617
const merged = deep_merge(
1718
{
@@ -277,7 +278,7 @@ const rollup_node_getter = (id) => {
277278
},
278279
'/statically-imports/bad/module.js': {
279280
id: '/statically-imports/bad/module.js',
280-
importedIds: ['/illegal/boom.js'],
281+
importedIds: [illegal_id],
281282
dynamicallyImportedIds: ['/test/path2.js']
282283
},
283284
'/bad/dynamic.js': {
@@ -288,43 +289,50 @@ const rollup_node_getter = (id) => {
288289
'/dynamically-imports/bad/module.js': {
289290
id: '/dynamically-imports/bad/module.js',
290291
importedIds: ['/test/path5.js'],
291-
dynamicallyImportedIds: ['/test/path2.js', '/illegal/boom.js']
292+
dynamicallyImportedIds: ['/test/path2.js', illegal_id]
292293
},
293-
'/illegal/boom.js': {
294-
id: '/illegal/boom.js',
294+
[illegal_id]: {
295+
id: illegal_id,
295296
importedIds: [],
296297
dynamicallyImportedIds: []
297298
}
298299
};
299300
return nodes[id] ?? null;
300301
};
301302

302-
const illegal_imports = new Set([normalizePath('/illegal/boom.js')]);
303303
const ok_rollup_node = rollup_node_getter('/test/path1.js');
304304
const bad_rollup_node_static = rollup_node_getter('/bad/static.js');
305305
const bad_rollup_node_dynamic = rollup_node_getter('/bad/dynamic.js');
306306

307307
test('allows ok rollup imports', () => {
308308
assert.not.throws(() => {
309-
// @ts-ignore
310-
prevent_illegal_rollup_imports(rollup_node_getter, ok_rollup_node, illegal_imports, '');
309+
prevent_illegal_rollup_imports(
310+
// @ts-expect-error
311+
rollup_node_getter,
312+
ok_rollup_node,
313+
'should_not_match_anything'
314+
);
311315
});
312316
});
313317

314318
test('does not allow bad static rollup imports', () => {
315319
assert.throws(() => {
316-
// @ts-ignore
317-
prevent_illegal_rollup_imports(rollup_node_getter, bad_rollup_node_static, illegal_imports, '');
320+
prevent_illegal_rollup_imports(
321+
// @ts-expect-error
322+
rollup_node_getter,
323+
bad_rollup_node_static,
324+
'should_not_match_anything'
325+
);
318326
});
319327
});
320328

321329
test('does not allow bad dynamic rollup imports', () => {
322330
assert.throws(() => {
323331
prevent_illegal_rollup_imports(
324-
// @ts-ignore
332+
// @ts-expect-error
325333
rollup_node_getter,
326334
bad_rollup_node_dynamic,
327-
illegal_imports
335+
'should_not_match_anything'
328336
);
329337
});
330338
});
@@ -359,7 +367,7 @@ const bad_vite_node = {
359367
id: '/test/path2.js',
360368
importedModules: new Set([
361369
{
362-
id: '/illegal/boom.js',
370+
id: illegal_id,
363371
importedModules: new Set()
364372
}
365373
])
@@ -372,15 +380,23 @@ const bad_vite_node = {
372380

373381
test('allows ok vite imports', () => {
374382
assert.not.throws(() => {
375-
// @ts-ignore
376-
prevent_illegal_vite_imports(ok_vite_node, illegal_imports, '');
383+
prevent_illegal_vite_imports(
384+
// @ts-expect-error
385+
ok_vite_node,
386+
'should_not_match_anything',
387+
[]
388+
);
377389
});
378390
});
379391

380392
test('does not allow bad static rollup imports', () => {
381393
assert.throws(() => {
382-
// @ts-ignore
383-
prevent_illegal_vite_imports(bad_vite_node, illegal_imports, '');
394+
prevent_illegal_vite_imports(
395+
// @ts-expect-error
396+
bad_vite_node,
397+
'should_not_match_anything',
398+
[]
399+
);
384400
});
385401
});
386402

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const should_explode = 'boom';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
const mod = import('$lib/test.server.js');
3+
</script>
4+
5+
{#await mod then resolved}
6+
<p>{resolved.should_explode}</p>
7+
{/await}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import { should_explode } from '$lib/test.server.js';
3+
</script>
4+
5+
<p>{should_explode}</p>

0 commit comments

Comments
 (0)