Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/icy-clubs-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'svelte-language-server': patch
'svelte-check': patch
'svelte2tsx': patch
---

fix: handle relative imports reaching outside working directory when using `--incremental/--tsgo` flags
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export interface SvelteSnapshotOptions {
transformOnTemplateError: boolean;
typingsNamespace: string;
emitJsDoc?: boolean;
rewriteExternalImports?: {
workspacePath: string;
generatedPath: string;
};
}

const ambientPathPattern = /node_modules[\/\\]svelte[\/\\]types[\/\\]ambient\.d\.ts$/;
Expand Down Expand Up @@ -213,7 +217,8 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
accessors:
document.config?.compilerOptions?.accessors ??
document.config?.compilerOptions?.customElement,
emitJsDoc: options.emitJsDoc
emitJsDoc: options.emitJsDoc,
rewriteExternalImports: options.rewriteExternalImports
});
text = tsx.code;
tsxMap = tsx.map as EncodedSourceMap;
Expand Down
11 changes: 9 additions & 2 deletions packages/language-server/src/svelte-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ import { groupBy } from 'lodash';
export function mapSvelteCheckDiagnostics(
sourcePath: string,
sourceText: string,
tsDiagnostics: ts.Diagnostic[]
tsDiagnostics: ts.Diagnostic[],
options?: {
rewriteExternalImports?: {
workspacePath: string;
generatedPath: string;
};
}
): Diagnostic[] {
const document = new Document(pathToUrl(sourcePath), sourceText);
const snapshot = DocumentSnapshot.fromDocument(document, {
parse: document.compiler?.parse,
version: document.compiler?.VERSION,
transformOnTemplateError: false,
typingsNamespace: 'svelteHTML',
emitJsDoc: true
emitJsDoc: true,
rewriteExternalImports: options?.rewriteExternalImports
} satisfies SvelteSnapshotOptions) as SvelteDocumentSnapshot;

return mapAndFilterDiagnostics(tsDiagnostics, document, snapshot);
Expand Down
41 changes: 30 additions & 11 deletions packages/svelte-check/src/incremental.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,11 @@ export async function emitSvelteFiles(
isTsFile,
mode: 'ts',
emitOnTemplateError: false,
emitJsDoc: true // without this, tsc/tsgo will choke on the syntactic errors and not emit semantic errors
emitJsDoc: true, // without this, tsc/tsgo will choke on the syntactic errors and not emit semantic errors
rewriteExternalImports: {
workspacePath,
generatedPath: outPath
}
});

fs.writeFileSync(outPath, tsx.code, 'utf-8');
Expand Down Expand Up @@ -306,14 +310,23 @@ export async function emitSvelteFiles(
const text = fs.readFileSync(sourcePath, 'utf-8');
const isTsFile = sourcePath.endsWith('.ts');

const result = internalHelpers.upsertKitFile(ts, sourcePath, kitFilesSettings, () =>
ts.createSourceFile(
sourcePath,
text,
ts.ScriptTarget.Latest,
true,
isTsFile ? ts.ScriptKind.TS : ts.ScriptKind.JS
)
const result = internalHelpers.upsertKitFile(
ts,
sourcePath,
kitFilesSettings,
() =>
ts.createSourceFile(
sourcePath,
text,
ts.ScriptTarget.Latest,
true,
isTsFile ? ts.ScriptKind.TS : ts.ScriptKind.JS
),
undefined,
{
workspacePath,
generatedPath: outPath
}
);

if (!result) {
Expand Down Expand Up @@ -532,7 +545,7 @@ export function mapCliDiagnosticsToLsp(
);
const excludedSourcePaths = new Set(
emitResult.entries
.map((e) => e.addedCode?.length && path.normalize(e.sourcePath))
.map((e) => e.isKitFile && e.addedCode?.length && path.normalize(e.sourcePath))
.filter((p): p is string => !!p)
);

Expand Down Expand Up @@ -613,7 +626,13 @@ export function mapCliDiagnosticsToLsp(
const mappedDiagnostics = mapSvelteCheckDiagnostics(
entry.sourcePath,
sourceText,
tsDiagnostics
tsDiagnostics,
{
rewriteExternalImports: {
workspacePath: emitResult.workspacePath,
generatedPath: entry.outPath
}
}
);

results.set(entry.sourcePath, {
Expand Down
1 change: 1 addition & 0 deletions packages/svelte-check/test-error/Index.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import Jsdoc from './Jsdoc.svelte';
import { foo } from './relative';
import nope from '../../outside';

let count: number = 'oops';
let x = 0;
Expand Down
14 changes: 10 additions & 4 deletions packages/svelte-check/test-sanity.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,25 +139,31 @@ test('clean project (incremental, warm cache)', {
const errors = [
{
file: 'Index.svelte',
line: 4,
line: 3,
column: 21,
code: 2307
},
{
file: 'Index.svelte',
line: 5,
column: 8,
code: 2322
},
{
file: 'Index.svelte',
line: 7,
line: 8,
column: 4,
code: 2367
},
{
file: 'Index.svelte',
line: 10,
line: 11,
column: 4,
code: 2367
},
{
file: 'Index.svelte',
line: 14,
line: 15,
column: 1,
code: 2741
},
Expand Down
13 changes: 12 additions & 1 deletion packages/svelte2tsx/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export function svelte2tsx(
* valid JS that tsc can process without errors.
*/
emitJsDoc?: boolean;
/**
* Rewrites relative imports that resolve outside the workspace so they stay valid
* from the generated file location.
*/
rewriteExternalImports?: InternalHelpers.RewriteExternalImportsConfig;
}
): SvelteCompiledToTsx

Expand Down Expand Up @@ -163,7 +168,8 @@ export const internalHelpers: {
fileName: string,
kitFilesSettings: InternalHelpers.KitFilesSettings,
getSource: () => ts.SourceFile | undefined,
surround?: (code: string) => string
surround?: (code: string) => string,
rewriteExternalImports?: InternalHelpers.RewriteExternalImportsConfig
) => { text: string; addedCode: InternalHelpers.AddedCode[] } | undefined,
toVirtualPos: (pos: number, addedCode: InternalHelpers.AddedCode[]) => number,
toOriginalPos: (pos: number, addedCode: InternalHelpers.AddedCode[]) => {pos: number; inGenerated: boolean},
Expand Down Expand Up @@ -201,4 +207,9 @@ export namespace InternalHelpers {
universalHooksPath: string;
paramsPath: string;
}

export interface RewriteExternalImportsConfig {
workspacePath: string;
generatedPath: string;
}
}
153 changes: 153 additions & 0 deletions packages/svelte2tsx/src/helpers/rewriteExternalImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import path from 'path';
import type ts from 'typescript';

export type RewriteExternalImportsOptions = {
sourcePath: string;
generatedPath: string;
workspacePath: string;
};

export type ExternalImportRewrite = {
rewritten: string;
insertedPrefix: string;
};

function toPosixPath(value: string): string {
return value.replace(/\\/g, '/');
}

function isWithinDirectory(filePath: string, directoryPath: string): boolean {
const relative = path.relative(path.resolve(directoryPath), path.resolve(filePath));
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
}

function splitImportSpecifier(specifier: string): { pathPart: string; suffix: string } {
const queryIndex = specifier.indexOf('?');
const hashIndex = specifier.indexOf('#');
const cutIndex =
queryIndex === -1
? hashIndex
: hashIndex === -1
? queryIndex
: Math.min(queryIndex, hashIndex);

if (cutIndex === -1) {
return { pathPart: specifier, suffix: '' };
}

return {
pathPart: specifier.slice(0, cutIndex),
suffix: specifier.slice(cutIndex)
};
}

export function getExternalImportRewrite(
specifier: string,
options: RewriteExternalImportsOptions
): ExternalImportRewrite | null {
const sourceDir = path.dirname(options.sourcePath);
const generatedDir = path.dirname(options.generatedPath);
const { pathPart, suffix } = splitImportSpecifier(specifier);
if (!pathPart.startsWith('../')) {
return null;
}

const targetPath = path.resolve(sourceDir, pathPart);
if (isWithinDirectory(targetPath, options.workspacePath)) {
return null;
}

const rewrittenRelative = toPosixPath(path.relative(generatedDir, targetPath));
const rewritten = `${rewrittenRelative}${suffix}`;
if (rewritten === specifier) {
return null;
}

return {
rewritten,
insertedPrefix: rewrittenRelative.slice(0, rewrittenRelative.length - pathPart.length)
};
}

export function getImportTypeSpecifierLiteral(
ts_impl: typeof ts,
node: ts.ImportTypeNode
): ts.StringLiteralLike | undefined {
const argument = node.argument;
if (ts_impl.isLiteralTypeNode(argument) && ts_impl.isStringLiteralLike(argument.literal)) {
return argument.literal;
}
return undefined;
}

function rewriteImportTypesInNode(
ts_impl: typeof ts,
node: ts.Node,
applyImportRewrite: (module_specifier: ts.StringLiteralLike) => void
) {
if (ts_impl.isImportTypeNode(node)) {
const specifier = getImportTypeSpecifierLiteral(ts_impl, node);
if (specifier) {
applyImportRewrite(specifier);
}
}
ts_impl.forEachChild(node, (child) =>
rewriteImportTypesInNode(ts_impl, child, applyImportRewrite)
);
}

export function rewriteExternalImportsInNode(
ts_impl: typeof ts,
node: ts.Node,
options: RewriteExternalImportsOptions,
on_rewrite: (module_specifier: ts.StringLiteralLike, rewrite: ExternalImportRewrite) => void
) {
const applyImportRewrite = (module_specifier: ts.StringLiteralLike) => {
const rewrite = getExternalImportRewrite(module_specifier.text, options);
if (rewrite) {
on_rewrite(module_specifier, rewrite);
}
};

if (ts_impl.isImportDeclaration(node) || ts_impl.isExportDeclaration(node)) {
if (node.moduleSpecifier && ts_impl.isStringLiteralLike(node.moduleSpecifier)) {
applyImportRewrite(node.moduleSpecifier);
}
} else if (ts_impl.isCallExpression(node)) {
const firstArg = node.arguments[0];
if (firstArg && ts_impl.isStringLiteralLike(firstArg)) {
const isDynamicImport = node.expression.kind === ts_impl.SyntaxKind.ImportKeyword;
const isRequireCall =
ts_impl.isIdentifier(node.expression) && node.expression.text === 'require';
if (isDynamicImport || isRequireCall) {
applyImportRewrite(firstArg);
}
}
} else if (ts_impl.isImportTypeNode(node)) {
const specifier = getImportTypeSpecifierLiteral(ts_impl, node);
if (specifier) {
applyImportRewrite(specifier);
}
}

const jsDoc = (node as ts.Node & { jsDoc?: ts.NodeArray<ts.JSDoc> }).jsDoc;
if (jsDoc) {
for (const doc of jsDoc) {
rewriteImportTypesInNode(ts_impl, doc, applyImportRewrite);
}
}
}

export function forEachExternalImportRewrite(
ts_impl: typeof ts,
source: ts.SourceFile,
options: RewriteExternalImportsOptions,
on_rewrite: (module_specifier: ts.StringLiteralLike, rewrite: ExternalImportRewrite) => void
) {
const visit = (node: ts.Node) => {
rewriteExternalImportsInNode(ts_impl, node, options, on_rewrite);
ts_impl.forEachChild(node, visit);
};

ts_impl.forEachChild(source, visit);
}
Loading