Skip to content

Commit

Permalink
feat: add support for pkg links
Browse files Browse the repository at this point in the history
  • Loading branch information
wkillerud committed Apr 24, 2024
1 parent aa7e976 commit a6d35c6
Show file tree
Hide file tree
Showing 39 changed files with 727 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ npm-debug.log*
node_modules/
!vscode-extension/test/fixtures/node_modules
!vscode-extension/test/fixtures/completion/node_modules
!vscode-extension/test/fixtures/pkg-import/node_modules

# Compiled and temporary files
dist/
Expand Down
3 changes: 3 additions & 0 deletions packages/language-services/src/utils/fs-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export function mapFsProviders(
]);
return result;
},
getContent(uri, encoding) {
return ours.readFile(URI.parse(uri), encoding);
},
};
return theirs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ export interface FileStat {
export interface FileSystemProvider {
stat(uri: DocumentUri): Promise<FileStat>;
readDirectory?(uri: DocumentUri): Promise<[string, FileType][]>;
getContent?(uri: DocumentUri, encoding?: BufferEncoding): Promise<string>;
}

export interface CSSFormatConfiguration {
Expand Down
111 changes: 111 additions & 0 deletions packages/vscode-css-languageservice/src/services/cssNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,13 @@ export class CSSNavigation {
return this.mapReference(await this.resolveModuleReference(target, documentUri, documentContext), isRawLink);
}

// Following the [sass package importer](https://github.com/sass/sass/blob/f6832f974c61e35c42ff08b3640ff155071a02dd/js-api-doc/importer.d.ts#L349),
// look for the `exports` field of the module and any `sass`, `style` or `default` that matches the import.
// If it's only `pkg:module`, also look for `sass` and `style` on the root of package.json.
if (target.startsWith("pkg:")) {
return this.resolvePkgModulePath(target, documentUri, documentContext);
}

const ref = await this.mapReference(documentContext.resolveReference(target, documentUri), isRawLink);

// Following [less-loader](https://github.com/webpack-contrib/less-loader#imports)
Expand Down Expand Up @@ -612,6 +619,99 @@ export class CSSNavigation {
return undefined;
}

private async resolvePkgModulePath(
target: string,
documentUri: string,
documentContext: DocumentContext,
): Promise<string | undefined> {
const bareTarget = target.replace("pkg:", "");
const moduleName = bareTarget.includes("/") ? getModuleNameFromPath(bareTarget) : bareTarget;
const rootFolderUri = documentContext.resolveReference("/", documentUri);
const documentFolderUri = dirname(documentUri);
const modulePath = await this.resolvePathToModule(moduleName, documentFolderUri, rootFolderUri);
if (modulePath) {
const packageJsonPath = `${modulePath}/package.json`;
if (packageJsonPath) {
// Since submodule exports import strings don't match the file system,
// we need the contents of `package.json` to look up the correct path.
let packageJsonContent = await this.getContent(packageJsonPath);
if (packageJsonContent) {
const packageJson: {
style?: string;
sass?: string;
exports: Record<string, string | Record<string, string>>;
} = JSON.parse(packageJsonContent);

const subpath = bareTarget.substring(moduleName.length + 1);
if (packageJson.exports) {
if (!subpath) {
// look for the default/index export
const entry =
// @ts-expect-error If ['.'] is a string this just produces undefined
packageJson.exports["."]["sass"] ||
// @ts-expect-error If ['.'] is a string this just produces undefined
packageJson.exports["."]["style"] ||
// @ts-expect-error If ['.'] is a string this just produces undefined
packageJson.exports["."]["default"];
// the 'default' entry can be whatever, typically .js – confirm it looks like `scss`
if (entry && entry.endsWith(".scss")) {
const entryPath = joinPath(modulePath, entry);
return entryPath;
}
} else {
// The import string may be with or without .scss.
// Likewise the exports entry. Look up both paths.
// However, they need to be relative (start with ./).
const lookupSubpath = subpath.endsWith(".scss") ? `./${subpath.replace(".scss", "")}` : `./${subpath}`;
const lookupSubpathScss = subpath.endsWith(".scss") ? `./${subpath}` : `./${subpath}.scss`;
const subpathObject = packageJson.exports[lookupSubpathScss] || packageJson.exports[lookupSubpath];
if (subpathObject) {
// @ts-expect-error If subpathObject is a string this just produces undefined
const entry = subpathObject["sass"] || subpathObject["styles"] || subpathObject["default"];
// the 'default' entry can be whatever, typically .js – confirm it looks like `scss`
if (entry && entry.endsWith(".scss")) {
const entryPath = joinPath(modulePath, entry);
return entryPath;
}
} else {
// We have a subpath, but found no matches on direct lookup.
// It may be a [subpath pattern](https://nodejs.org/api/packages.html#subpath-patterns).
for (const [maybePattern, subpathObject] of Object.entries(packageJson.exports)) {
if (!maybePattern.includes("*")) {
continue;
}
// Patterns may also be without `.scss` on the left side, so compare without on both sides
const re = new RegExp(maybePattern.replace("./", "\\./").replace(".scss", "").replace("*", "(.+)"));
const match = re.exec(lookupSubpath);
if (match) {
// @ts-expect-error If subpathObject is a string this just produces undefined
const entry = subpathObject["sass"] || subpathObject["styles"] || subpathObject["default"];
// the 'default' entry can be whatever, typically .js – confirm it looks like `scss`
if (entry && entry.endsWith(".scss")) {
// The right-hand side of a subpath pattern is also a pattern.
// Replace the pattern with the match from our regexp capture group above.
const expandedPattern = entry.replace("*", match[1]);
const entryPath = joinPath(modulePath, expandedPattern);
return entryPath;
}
}
}
}
}
} else if (!subpath && (packageJson.sass || packageJson.style)) {
// Fall back to a direct lookup on `sass` and `style` on package root
const entry = packageJson.sass || packageJson.style;
if (entry) {
const entryPath = joinPath(modulePath, entry);
return entryPath;
}
}
}
}
}
return undefined;
}

protected async fileExists(uri: string): Promise<boolean> {
if (!this.fileSystemProvider) {
return false;
Expand All @@ -627,6 +727,17 @@ export class CSSNavigation {
return false;
}
}

protected async getContent(uri: string): Promise<string | null> {
if (!this.fileSystemProvider || !this.fileSystemProvider.getContent) {
return null;
}
try {
return await this.fileSystemProvider.getContent(uri);
} catch (err) {
return null;
}
}
}

function getColorInformation(node: nodes.Node, document: TextDocument): ColorInformation | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,207 @@ suite("SCSS - Navigation", () => {
workspaceFolder,
);
});

test("SCSS node package resolving", async () => {
let ls = getSCSSLS();
let testUri = getTestResource("about.scss");
let workspaceFolder = getTestResource("");
await assertLinks(
ls,
`@use "pkg:bar"`,
[
{
namespace: "bar",
range: newRange(5, 14),
target: getTestResource("node_modules/bar/styles/index.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:bar/colors"`,
[
{
namespace: "colors",
range: newRange(5, 21),
target: getTestResource("node_modules/bar/styles/colors.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:bar/colors.scss"`,
[
{
namespace: "colors",
range: newRange(5, 26),
target: getTestResource("node_modules/bar/styles/colors.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:@foo/baz"`,
[
{
namespace: "baz",
range: newRange(5, 19),
target: getTestResource("node_modules/@foo/baz/styles/index.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:@foo/baz/colors"`,
[
{
namespace: "colors",
range: newRange(5, 26),
target: getTestResource("node_modules/@foo/baz/styles/colors.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:@foo/baz/colors.scss"`,
[
{
namespace: "colors",
range: newRange(5, 31),
target: getTestResource("node_modules/@foo/baz/styles/colors.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:@foo/baz/button"`,
[
{
namespace: "button",
range: newRange(5, 26),
target: getTestResource("node_modules/@foo/baz/styles/button.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:@foo/baz/button.scss"`,
[
{
namespace: "button",
range: newRange(5, 31),
target: getTestResource("node_modules/@foo/baz/styles/button.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:root-sass"`,
[
{
namespace: "root-sass",
range: newRange(5, 20),
target: getTestResource("node_modules/root-sass/styles/index.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:root-style"`,
[
{
namespace: "root-style",
range: newRange(5, 21),
target: getTestResource("node_modules/root-style/styles/index.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:bar-pattern/anything"`,
[
{
namespace: "anything",
range: newRange(5, 31),
target: getTestResource("node_modules/bar-pattern/styles/anything.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:bar-pattern/anything.scss"`,
[
{
namespace: "anything",
range: newRange(5, 36),
target: getTestResource("node_modules/bar-pattern/styles/anything.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
await assertLinks(
ls,
`@use "pkg:bar-pattern/theme/dark.scss"`,
[
{
namespace: "dark",
range: newRange(5, 38),
target: getTestResource("node_modules/bar-pattern/styles/theme/dark.scss"),
type: nodes.NodeType.Use,
},
],
"scss",
testUri,
workspaceFolder,
);
});
});

suite("Symbols", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { FileSystemProvider, FileType } from "../../cssLanguageTypes";
import { URI } from "vscode-uri";
import { stat as fsStat, readdir } from "fs";
import { stat as fsStat, readFile, readdir } from "fs";

export function getFsProvider(): FileSystemProvider {
return {
Expand Down Expand Up @@ -75,5 +75,20 @@ export function getFsProvider(): FileSystemProvider {
});
});
},
getContent(locationString, encoding = "utf-8") {
return new Promise((c, e) => {
const location = URI.parse(locationString);
if (location.scheme !== "file") {
e(new Error("Protocol not supported: " + location.scheme));
return;
}
readFile(location.fsPath, encoding, (err, data) => {
if (err) {
return e(err);
}
c(data);
});
});
},
};
}
Loading

0 comments on commit a6d35c6

Please sign in to comment.