Skip to content

feat: add npm scoped-modules import resolution #3014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions src/ast/ast-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const astNumberBases = Object.freeze(
const importTypesRecord: Record<Ast.ImportType, true> = {
stdlib: true,
relative: true,
package: true,
};

export const importTypes = Object.freeze(keys(importTypesRecord));
Expand Down
2 changes: 1 addition & 1 deletion src/ast/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ export type ImportPath = {

// This is different from ItemOrigin, because relative import
// from standard library is still import with origin: "stdlib"
export type ImportType = "stdlib" | "relative";
export type ImportType = "stdlib" | "relative" | "package";

// A String is a string in which escaping characters, like '\\' has been simplified, e.g., '\\' simplified to '\'.
export type String = {
Expand Down
15 changes: 15 additions & 0 deletions src/grammar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1450,6 +1450,7 @@ const guessExtension = (
};

const stdlibPrefix = "@stdlib/";
const packagePrefix = "@";

const parseImportString =
(importText: string, loc: $.Loc): Handler<Ast.ImportPath> =>
Expand Down Expand Up @@ -1478,6 +1479,20 @@ const parseImportString =
type: "stdlib",
language,
};
} else if (guessedPath.startsWith(packagePrefix)) {
// TODO: support non-scoped npm packages
// (@package-name instead of @scope/package-name)
const path = fromString(guessedPath);

if (path.stepsUp !== 0) {
ctx.err.importWithBackslash()(loc);
}

return {
path,
type: "package",
language,
};
} else if (
guessedPath.startsWith("./") ||
guessedPath.startsWith("../")
Expand Down
21 changes: 21 additions & 0 deletions src/imports/resolveLibrary.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const project = createVirtualFileSystem("/project", {
["main.tact"]: "",
["import.tact"]: "",
["main.fc"]: "",
["node_modules/@name/j.tact"]: "",
});

const mainSource: Source = {
Expand All @@ -26,6 +27,26 @@ const stdlibSource: Source = {
code: "",
};

it("project file, node_modules package import", () => {
const resolved = resolveLibrary({
sourceFrom: mainSource,
importPath: {
path: fromString("@name/j.tact"),
language: "tact",
type: "package",
},
project,
stdlib,
});

expect(resolved).toMatchObject({
ok: true,
path: "/project/node_modules/@name/j.tact",
origin: "user",
language: "tact",
});
});

it("project file, stdlib import", () => {
const resolved = resolveLibrary({
sourceFrom: mainSource,
Expand Down
50 changes: 50 additions & 0 deletions src/imports/resolveLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type * as Ast from "@/ast/ast";
import type { VirtualFileSystem } from "@/vfs/VirtualFileSystem";
import { asString } from "@/imports/path";
import type { ItemOrigin, Language, Source } from "@/imports/source";
import { repeat } from "@/utils/array";

type ResolveLibraryArgs = {
readonly importPath: Ast.ImportPath;
Expand All @@ -23,6 +24,38 @@ type ResolveLibraryFailure = {

type ResolveLibraryResult = ResolveLibrarySuccess | ResolveLibraryFailure;

const nodeModulesPath = "node_modules";

const findIndentNodeModules = (
project: VirtualFileSystem,
root: string,
path: string,
) => {
// TODO: unwind folders until we either
// find our import or reach project root
for (let index = 0; index < 20; index++) {
const tryRes = project.resolve(
root,
...repeat("..", index),
nodeModulesPath,
path,
);

if (!project.exists(tryRes)) {
continue;
}

return {
ok: true,
path: tryRes,
};
}

return {
ok: false,
};
};

export function resolveLibrary({
importPath,
sourceFrom,
Expand All @@ -42,6 +75,23 @@ export function resolveLibrary({
} else {
return { ok: false };
}
} else if (importPath.type === "package") {
const res = findIndentNodeModules(
project,
sourceFrom.path.slice(project.root.length),
asString(importPath.path),
);

if (res.ok) {
return {
ok: true,
path: res.path!,
origin: sourceFrom.origin,
language: importPath.language,
};
} else {
return { ok: false };
}
} else {
const vfs = sourceFrom.origin === "stdlib" ? stdlib : project;
const resolvedPath = vfs.resolve(
Expand Down
Loading