From 9a3d8ec36f4e5f82f4fd8104ebba39c21d3fa863 Mon Sep 17 00:00:00 2001 From: Kaladin13 <335095@niuitmo.ru> Date: Tue, 6 May 2025 16:58:40 +0300 Subject: [PATCH] add scoped modules resolution --- src/ast/ast-constants.ts | 1 + src/ast/ast.ts | 2 +- src/grammar/index.ts | 15 +++++++++ src/imports/resolveLibrary.spec.ts | 21 +++++++++++++ src/imports/resolveLibrary.ts | 50 ++++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/ast/ast-constants.ts b/src/ast/ast-constants.ts index 4d492a32c0..ecb2a8f275 100644 --- a/src/ast/ast-constants.ts +++ b/src/ast/ast-constants.ts @@ -70,6 +70,7 @@ export const astNumberBases = Object.freeze( const importTypesRecord: Record = { stdlib: true, relative: true, + package: true, }; export const importTypes = Object.freeze(keys(importTypesRecord)); diff --git a/src/ast/ast.ts b/src/ast/ast.ts index 6d006a651f..fdf09a0008 100644 --- a/src/ast/ast.ts +++ b/src/ast/ast.ts @@ -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 = { diff --git a/src/grammar/index.ts b/src/grammar/index.ts index b66a81bb15..320b956d19 100644 --- a/src/grammar/index.ts +++ b/src/grammar/index.ts @@ -1450,6 +1450,7 @@ const guessExtension = ( }; const stdlibPrefix = "@stdlib/"; +const packagePrefix = "@"; const parseImportString = (importText: string, loc: $.Loc): Handler => @@ -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("../") diff --git a/src/imports/resolveLibrary.spec.ts b/src/imports/resolveLibrary.spec.ts index 94094c82c6..670fa58707 100644 --- a/src/imports/resolveLibrary.spec.ts +++ b/src/imports/resolveLibrary.spec.ts @@ -7,6 +7,7 @@ const project = createVirtualFileSystem("/project", { ["main.tact"]: "", ["import.tact"]: "", ["main.fc"]: "", + ["node_modules/@name/j.tact"]: "", }); const mainSource: Source = { @@ -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, diff --git a/src/imports/resolveLibrary.ts b/src/imports/resolveLibrary.ts index be794e95ba..416d040557 100644 --- a/src/imports/resolveLibrary.ts +++ b/src/imports/resolveLibrary.ts @@ -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; @@ -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, @@ -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(