Skip to content
Closed
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/tsgo-symlinked-dirs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-check': patch
---

fix: check components in symlinked directories with `--tsgo` / `--incremental`
2 changes: 1 addition & 1 deletion packages/svelte-check/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@jridgewell/trace-mapping": "^0.3.25",
"@sveltejs/load-config": "workspace:^",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"fdir": "^6.4.6",
"picocolors": "^1.0.0",
"sade": "^1.7.4"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte-check/src/incremental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export async function emitSvelteFiles(
filePathsToIgnore,
(filePath) =>
filePath.endsWith('.svelte') ||
(isJsOrTsFile(filePath) && internalHelpers.isKitFile(filePath, kitFilesSettings))
(isJsOrTsFile(filePath) && internalHelpers.isKitFile(filePath, kitFilesSettings)),
true
);
const svelteFiles: string[] = [];
const kitFiles: string[] = [];
Expand Down
51 changes: 44 additions & 7 deletions packages/svelte-check/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import { fdir } from 'fdir';

/**
Expand Down Expand Up @@ -41,12 +43,14 @@ export function createIgnored(filePathsToIgnore: string[]): Array<(path: string)
* @param workspacePath - Root directory to search from
* @param filePathsToIgnore - Patterns for files/directories to exclude
* @param filter - Predicate that decides whether a file should be included
* @param followSymlinks - Also search symlinked directories that lead out of the workspace
* @returns Array of absolute paths to all discovered matching files
*/
export async function findFiles(
workspacePath: string,
filePathsToIgnore: string[],
filter: (filePath: string) => boolean
filter: (filePath: string) => boolean,
followSymlinks = false
): Promise<string[]> {
const offset = workspacePath.length + 1;
const ignored = createIgnored(filePathsToIgnore);
Expand All @@ -60,17 +64,50 @@ export async function findFiles(
return false;
};

return new fdir()
const isRedundantLink = followSymlinks ? createRedundantLinkCheck(workspacePath) : () => false;
const crawler = new fdir()
.filter((filePath) => filter(filePath) && !isIgnored(filePath))
.exclude((dirName) => {
.exclude((dirName, dirPath) => {
// Match by directory name rather than full path so that ancestor
// directories of the workspace (e.g. a `.local/`, `.config/`,
// worktree directory) do not cause every descendant to be skipped.
// See https://github.com/sveltejs/language-tools/issues/3034.
return dirName === 'node_modules' || dirName.startsWith('.');
return (
dirName === 'node_modules' || dirName.startsWith('.') || isRedundantLink(dirPath)
);
})
.withPathSeparator('/')
.withFullPaths()
.crawl(workspacePath)
.withPromise();
.withFullPaths();
if (followSymlinks) {
// Like TypeScript, keep the paths through the link
crawler.withSymlinks({ resolvePaths: false });
}
return crawler.crawl(workspacePath).withPromise();
}

/**
* A link into the workspace, or to a folder that contains it, leads to files that are found anyway
* or to a loop, and following it would emit the same components twice.
*/
function createRedundantLinkCheck(workspacePath: string): (dirPath: string) => boolean {
const realWorkspacePath = fs.realpathSync(workspacePath);
return (dirPath) => {
try {
// Without fdir's trailing slash, which would make lstat follow the link
const linkPath = path.resolve(dirPath);
if (!fs.lstatSync(linkPath).isSymbolicLink()) {
return false;
}
const target = fs.realpathSync(linkPath);
return isInside(realWorkspacePath, target) || isInside(target, realWorkspacePath);
} catch {
// Removed since its parent was read
return true;
}
};
}

function isInside(directory: string, filePath: string): boolean {
const relative = path.relative(directory, filePath);
return !path.isAbsolute(relative) && relative.split(path.sep)[0] !== '..';
}
25 changes: 24 additions & 1 deletion packages/svelte-check/test-sanity.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

const { execFileSync } = require('child_process');
const { mkdirSync, mkdtempSync, rmSync, writeFileSync } = require('fs');
const { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } = require('fs');
const os = require('os');
const path = require('path');

Expand Down Expand Up @@ -335,6 +335,29 @@ test('project with errors --tsgo', {
errors
});

// Components in a symlinked directory outside the workspace are checked like the default mode does
const symlinkRoot = mkdtempSync(path.join(realpathSync(os.tmpdir()), 'svelte-check-symlink-'));
const symlinkWorkspace = path.join(symlinkRoot, 'app');
mkdirSync(symlinkWorkspace);
mkdirSync(path.join(symlinkRoot, 'shared'));
writeFileSync(
path.join(symlinkWorkspace, 'tsconfig.json'),
JSON.stringify({ compilerOptions: { strict: true }, include: ['src/**/*'] })
);
writeFileSync(
path.join(symlinkRoot, 'shared', 'Test.svelte'),
'<script lang="ts">\n\tconst n: number = "";\n</script>\n'
);
// A junction on Windows, which needs no extra rights
symlinkSync(path.join(symlinkRoot, 'shared'), path.join(symlinkWorkspace, 'src'), 'junction');
test('symlinked directory --tsgo', {
workspace: symlinkWorkspace,
tsconfig: path.join(symlinkWorkspace, 'tsconfig.json'),
tsgo: true,
errors: [{ file: 'src/Test.svelte', line: 1, column: 7, code: 2322 }]
});
rmSync(symlinkRoot, { recursive: true, force: true });

// A compiler that dies must not look like a clean run, and one that dies after reporting
// some diagnostics must not look like a completed one.
testCrashedCompiler(
Expand Down
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

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