Repository: sveltejs/kit (js)

Issue:
🐛 Symlink routes no longer work (1.0.0-next.432 — PR #6174)
### Describe the bug

Symlink routes / Symbolic link routes are no longer working since Sveltekit version `1.0.0-next.432` [(PR #6174)](https://github.com/sveltejs/kit/pull/6174)

### Reproduction

**1.0.0-next.431** (working)
✅ https://github.com/oneezy/sveltekit-symlink-repro-next-431

**1.0.0-next.432** (broke)
❌ https://github.com/oneezy/sveltekit-symlink-repro-next-432

![image](https://user-images.githubusercontent.com/7369575/187002410-3f58ccb9-8b14-419e-b5e8-15b0a606c708.png)

### Logs

```shell
Error: Not found: /symlink
    at resolve (file:///C:/Users/oneezy/Desktop/www/10.Repros/sveltekit-symlink-repro-next-442/node_modules/.pnpm/@sveltejs+kit@1.0.0-next.442_svelte@3.49.0+vite@3.0.9/node_modules/@sveltejs/kit/src/runtime/server/index.js:430:14)
    at Object.handle (file:///C:/Users/oneezy/Desktop/www/10.Repros/sveltekit-symlink-repro-next-442/node_modules/.pnpm/@sveltejs+kit@1.0.0-next.442_svelte@3.49.0+vite@3.0.9/node_modules/@sveltejs/kit/src/vite/dev/index.js:318:66)
    at respond (file:///C:/Users/oneezy/Desktop/www/10.Repros/sveltekit-symlink-repro-next-442/node_modules/.pnpm/@sveltejs+kit@1.0.0-next.442_svelte@3.49.0+vite@3.0.9/node_modules/@sveltejs/kit/src/runtime/server/index.js:213:40)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async file:///C:/Users/oneezy/Desktop/www/10.Repros/sveltekit-symlink-repro-next-442/node_modules/.pnpm/@sveltejs+kit@1.0.0-next.442_svelte@3.49.0+vite@3.0.9/node_modules/@sveltejs/kit/src/vite/dev/index.js:375:22
```

### System Info

```shell
System:
    OS: Windows 11
    CPU: (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
    Memory: 15.50 GB / 31.75 GB
  Binaries:
    Node: 16.13.2 - C:\Program Files\nodejs\node.EXE
    npm: 8.1.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Chrome: 104.0.5112.102
    Edge: Spartan (44.22000.120.0), Chromium (104.0.1293.63)
    Internet Explorer: 11.0.22000.120
  npmPackages:
    @sveltejs/adapter-auto: next => 1.0.0-next.66 
    @sveltejs/kit: 1.0.0-next.406 => 1.0.0-next.406 
    svelte: ^3.44.0 => 3.49.0 
    vite: ^3.0.4 => 3.0.9
```


### Severity

blocking all usage of SvelteKit

### Additional Information

We use a [monorepo](https://github.com/oneezy/monorepo) with multiple separate apps that we link together via symlink routes so this one's a showstopper for us.

Pull request:
[fix] symlink routes
Fixes #6303

No test because I don't know how to write one for this. @oneezy could you test out if this fixes the issue for you?

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/pre.json b/.changeset/pre.json
index ff8dd1048..08cce8d6b 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -179,6 +179,7 @@
     "cold-shirts-lick",
     "cold-tables-shake",
     "cool-bananas-share",
+    "cool-beans-only",
     "cool-birds-attack",
     "cool-crabs-listen",
     "cool-drinks-press",
@@ -215,6 +216,7 @@
     "cyan-roses-act",
     "cyan-schools-kiss",
     "cyan-seals-trade",
+    "cyan-years-live",
     "dirty-bikes-join",
     "dirty-ligers-protect",
     "dry-baboons-turn",
@@ -1364,6 +1366,7 @@
     "young-pumpkins-approve",
     "young-scissors-collect",
     "young-students-chew",
+    "young-students-relate",
     "young-swans-burn",
     "young-ways-fail"
   ]
diff --git a/.changeset/silent-jeans-vanish.md b/.changeset/silent-jeans-vanish.md
new file mode 100644
index 000000000..44b09d9c1
--- /dev/null
+++ b/.changeset/silent-jeans-vanish.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] symlink routes
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 6724737a4..a1848b42a 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,15 @@
 # @sveltejs/kit
 
+## 1.0.0-next.490
+
+### Patch Changes
+
+- fix `write_types` on windows using posixify() ([#6913](https://github.com/sveltejs/kit/pull/6913))
+
+* Prevent caching of `__data.js` files ([#6904](https://github.com/sveltejs/kit/pull/6904))
+
+- Make url property getters non-enumerable ([#6909](https://github.com/sveltejs/kit/pull/6909))
+
 ## 1.0.0-next.489
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 6e8a24bb0..4d18742c0 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.489",
+	"version": "1.0.0-next.490",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js
index d0e35d59f..91ec76997 100644
--- a/packages/kit/src/core/generate_manifest/index.js
+++ b/packages/kit/src/core/generate_manifest/index.js
@@ -1,5 +1,6 @@
 import { s } from '../../utils/misc.js';
 import { get_mime_lookup } from '../utils.js';
+import { resolve_symlinks } from '../../exports/vite/build/utils.js';
 
 /**
  * Generates the data used to write the server-side manifest.js file. This data is used in the Vite
@@ -65,7 +66,7 @@ export function generate_manifest({ build_data, relative_path, routes, format =
 					names: ${s(route.names)},
 					types: ${s(route.types)},
 					page: ${route.page ? `{ layouts: ${get_nodes(route.page.layouts)}, errors: ${get_nodes(route.page.errors)}, leaf: ${route.page.leaf} }` : 'null'},
-					endpoint: ${route.endpoint ? loader(`${relative_path}/${build_data.server.vite_manifest[route.endpoint.file].file}`) : 'null'}
+					endpoint: ${route.endpoint ? loader(`${relative_path}/${resolve_symlinks(build_data.server.vite_manifest, route.endpoint.file).chunk.file}`) : 'null'}
 				}`;
 				}).filter(Boolean).join(',\n\t\t\t\t')}
 			],
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 563c391b0..e9cd7205e 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -164,13 +164,16 @@ function create_routes_and_nodes(cwd, config, fallback) {
 
 			const dir = path.join(cwd, routes_base, id);
 
-			const files = fs.readdirSync(dir, {
-				withFileTypes: true
-			});
+			// We can't use withFileTypes because of a NodeJs bug which returns wrong results
+			// with isDirectory() in case of symlinks: https://github.com/nodejs/node/issues/30646
+			const files = fs.readdirSync(dir).map((name) => ({
+				is_dir: fs.statSync(path.join(dir, name)).isDirectory(),
+				name
+			}));
 
 			// process files first
 			for (const file of files) {
-				if (file.isDirectory()) continue;
+				if (file.is_dir) continue;
 				if (!file.name.startsWith('+')) continue;
 				if (!valid_extensions.find((ext) => file.name.endsWith(ext))) continue;
 
@@ -213,7 +216,7 @@ function create_routes_and_nodes(cwd, config, fallback) {
 
 			// then handle children
 			for (const file of files) {
-				if (file.isDirectory()) {
+				if (file.is_dir) {
 					walk(depth + 1, path.posix.join(id, file.name), file.name, route);
 				}
 			}
diff --git a/packages/kit/src/exports/vite/build/build_server.js b/packages/kit/src/exports/vite/build/build_server.js
index 1b53cd9fb..9b116d423 100644
--- a/packages/kit/src/exports/vite/build/build_server.js
+++ b/packages/kit/src/exports/vite/build/build_server.js
@@ -4,7 +4,13 @@ import { mkdirp, posixify, resolve_entry } from '../../../utils/filesystem.js';
 import { get_vite_config, merge_vite_configs } from '../utils.js';
 import { load_error_page, load_template } from '../../../core/config/index.js';
 import { runtime_directory } from '../../../core/utils.js';
-import { create_build, find_deps, get_default_build_config, is_http_method } from './utils.js';
+import {
+	create_build,
+	find_deps,
+	get_default_build_config,
+	is_http_method,
+	resolve_symlinks
+} from './utils.js';
 import { s } from '../../../utils/misc.js';
 
 /**
@@ -285,7 +291,7 @@ export async function build_server(options, client) {
 
 			exports.push(
 				`export const component = async () => (await import('../${
-					vite_manifest[node.component].file
+					resolve_symlinks(vite_manifest, node.component).chunk.file
 				}')).default;`,
 				`export const file = '${entry.file}';` // TODO what is this?
 			);
diff --git a/packages/kit/src/exports/vite/build/utils.js b/packages/kit/src/exports/vite/build/utils.js
index 4a0e19e4a..da3134c31 100644
--- a/packages/kit/src/exports/vite/build/utils.js
+++ b/packages/kit/src/exports/vite/build/utils.js
@@ -1,3 +1,5 @@
+import fs from 'fs';
+import path from 'path';
 import * as vite from 'vite';
 import { get_aliases } from '../utils.js';
 
@@ -44,14 +46,14 @@ export function find_deps(manifest, entry, add_dynamic_css) {
 	const stylesheets = new Set();
 
 	/**
-	 * @param {string} file
+	 * @param {string} current
 	 * @param {boolean} add_js
 	 */
-	function traverse(file, add_js) {
-		if (seen.has(file)) return;
-		seen.add(file);
+	function traverse(current, add_js) {
+		if (seen.has(current)) return;
+		seen.add(current);
 
-		const chunk = manifest[file];
+		const { chunk } = resolve_symlinks(manifest, current);
 
 		if (add_js) imports.add(chunk.file);
 
@@ -68,15 +70,31 @@ export function find_deps(manifest, entry, add_dynamic_css) {
 		}
 	}
 
-	traverse(entry, true);
+	const { chunk, file } = resolve_symlinks(manifest, entry);
+
+	traverse(file, true);
 
 	return {
-		file: manifest[entry].file,
+		file: chunk.file,
 		imports: Array.from(imports),
 		stylesheets: Array.from(stylesheets)
 	};
 }
 
+/**
+ * @param {import('vite').Manifest} manifest
+ * @param {string} file
+ */
+export function resolve_symlinks(manifest, file) {
+	while (!manifest[file]) {
+		file = path.relative('.', fs.realpathSync(file));
+	}
+
+	const chunk = manifest[file];
+
+	return { chunk, file };
+}
+
 /**
  * The Vite configuration that we use by default.
  * @param {{


Test patch:
diff --git a/packages/kit/test/apps/basics/.gitignore b/packages/kit/test/apps/basics/.gitignore
index fa2f5a579..fad4d3e15 100644
--- a/packages/kit/test/apps/basics/.gitignore
+++ b/packages/kit/test/apps/basics/.gitignore
@@ -1,2 +1,3 @@
 /test/errors.json
 !/.env
+/src/routes/routing/symlink-from
\ No newline at end of file
diff --git a/packages/kit/test/apps/basics/package.json b/packages/kit/test/apps/basics/package.json
index c3da79b25..76e9cf5b7 100644
--- a/packages/kit/test/apps/basics/package.json
+++ b/packages/kit/test/apps/basics/package.json
@@ -7,7 +7,7 @@
 		"build": "vite build",
 		"preview": "vite preview",
 		"check": "svelte-kit sync && tsc && svelte-check",
-		"test": "npm run test:dev && npm run test:build",
+		"test": "node test/setup.js && npm run test:dev && npm run test:build",
 		"test:dev": "rimraf test/errors.json && cross-env DEV=true playwright test",
 		"test:build": "rimraf test/errors.json && playwright test"
 	},
diff --git a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
index 61f62ec6f..1d300bf63 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
@@ -6,6 +6,7 @@
 
 <a href="/routing/a">a</a>
 <a href="/routing/ambiguous/ok.json" rel="external">ok</a>
+<a href="/routing/symlink-from">symlinked</a>
 <a href="http://localhost:{$page.url.searchParams.get('port')}">elsewhere</a>
 <a href="/static.json">static.json</a>
 
diff --git a/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte
new file mode 100644
index 000000000..d80b82ddf
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/symlink-to/+page.svelte
@@ -0,0 +1,1 @@
+<h1>symlinked</h1>
diff --git a/packages/kit/test/apps/basics/test/setup.js b/packages/kit/test/apps/basics/test/setup.js
new file mode 100644
index 000000000..9be580053
--- /dev/null
+++ b/packages/kit/test/apps/basics/test/setup.js
@@ -0,0 +1,7 @@
+import fs from 'fs';
+
+if (process.platform !== 'win32') {
+	process.chdir('src/routes/routing');
+	fs.rmSync('symlink-from', { recursive: true, force: true });
+	fs.symlinkSync('symlink-to', 'symlink-from', 'dir');
+}
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 1ce41289c..e63236b8b 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1639,6 +1639,15 @@ test.describe('Routing', () => {
 		expect(await page.textContent('h1')).toBe('404');
 		expect(await page.textContent('p')).toBe('This is your custom error page saying: "Not Found"');
 	});
+
+	if (process.platform !== 'win32') {
+		test('Respects symlinks', async ({ page, clicknav }) => {
+			await page.goto('/routing');
+			await clicknav('[href="/routing/symlink-from"]');
+
+			expect(await page.textContent('h1')).toBe('symlinked');
+		});
+	}
 });
 
 test.describe('Matchers', () => {