diff --git a/server/src/buildSchema.ts b/server/src/buildSchema.ts new file mode 100644 index 000000000..99c7cfe4f --- /dev/null +++ b/server/src/buildSchema.ts @@ -0,0 +1,33 @@ +// This file has been generated from https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json +// with https://app.quicktype.io/ and stripped down to what we actually need for the extension. + +export interface BuildSchema { + name: string; + namespace?: boolean | string; + "package-specs"?: + | Array + | ModuleFormat + | ModuleFormatObject; + suffix?: SuffixSpec; +} + +export enum ModuleFormat { + Commonjs = "commonjs", + Es6 = "es6", + Es6Global = "es6-global", +} + +export interface ModuleFormatObject { + "in-source"?: boolean; + module: ModuleFormat; + suffix?: SuffixSpec; +} + +export enum SuffixSpec { + BsCjs = ".bs.cjs", + BsJS = ".bs.js", + BsMjs = ".bs.mjs", + Cjs = ".cjs", + JS = ".js", + Mjs = ".mjs", +} diff --git a/server/src/constants.ts b/server/src/constants.ts index 1bc0bd55b..0709089ab 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import { ModuleFormat } from "./buildSchema"; export let platformDir = process.arch == "arm64" ? process.platform + process.arch : process.platform; @@ -49,7 +50,7 @@ export let cmiExt = ".cmi"; export let startBuildAction = "Start Build"; // bsconfig defaults according configuration schema (https://rescript-lang.org/docs/manual/latest/build-configuration-schema) -export let bsconfigModuleDefault = "commonjs"; +export let bsconfigModuleDefault = ModuleFormat.Commonjs; export let bsconfigSuffixDefault = ".js"; export let configurationRequestId = "rescript_configuration_request"; diff --git a/server/src/lookup.ts b/server/src/lookup.ts new file mode 100644 index 000000000..62c0535ec --- /dev/null +++ b/server/src/lookup.ts @@ -0,0 +1,150 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as p from "vscode-languageserver-protocol"; + +import { BuildSchema, ModuleFormat, ModuleFormatObject } from "./buildSchema"; +import * as c from "./constants"; + +const getCompiledFolderName = (moduleFormat: ModuleFormat): string => { + switch (moduleFormat) { + case "es6": + return "es6"; + case "es6-global": + return "es6_global"; + case "commonjs": + default: + return "js"; + } +}; + +export const replaceFileExtension = (filePath: string, ext: string): string => { + let name = path.basename(filePath, path.extname(filePath)); + return path.format({ dir: path.dirname(filePath), name, ext }); +}; + +// Check if filePartialPath exists at directory and return the joined path, +// otherwise recursively check parent directories for it. +export const findFilePathFromProjectRoot = ( + directory: p.DocumentUri | null, // This must be a directory and not a file! + filePartialPath: string +): null | p.DocumentUri => { + if (directory == null) { + return null; + } + + let filePath: p.DocumentUri = path.join(directory, filePartialPath); + if (fs.existsSync(filePath)) { + return filePath; + } + + let parentDir: p.DocumentUri = path.dirname(directory); + if (parentDir === directory) { + // reached the top + return null; + } + + return findFilePathFromProjectRoot(parentDir, filePartialPath); +}; + +export const readBsConfig = (projDir: p.DocumentUri): BuildSchema | null => { + try { + let bsconfigFile = fs.readFileSync( + path.join(projDir, c.bsconfigPartialPath), + { encoding: "utf-8" } + ); + + let result: BuildSchema = JSON.parse(bsconfigFile); + return result; + } catch (e) { + return null; + } +}; + +// Collect data from bsconfig to be able to find out the correct path of +// the compiled JS artifacts. +export const getSuffixAndPathFragmentFromBsconfig = (bsconfig: BuildSchema) => { + let pkgSpecs = bsconfig["package-specs"]; + let pathFragment = ""; + let module = c.bsconfigModuleDefault; + let moduleFormatObj: ModuleFormatObject = { module: module }; + let suffix = c.bsconfigSuffixDefault; + + if (pkgSpecs) { + if ( + !Array.isArray(pkgSpecs) && + typeof pkgSpecs !== "string" && + pkgSpecs.module + ) { + moduleFormatObj = pkgSpecs; + } else if (typeof pkgSpecs === "string") { + module = pkgSpecs; + } else if (Array.isArray(pkgSpecs) && pkgSpecs[0]) { + if (typeof pkgSpecs[0] === "string") { + module = pkgSpecs[0]; + } else { + moduleFormatObj = pkgSpecs[0]; + } + } + } + + if (moduleFormatObj["module"]) { + module = moduleFormatObj["module"]; + } + + if (!moduleFormatObj["in-source"]) { + pathFragment = "lib/" + getCompiledFolderName(module); + } + + if (moduleFormatObj.suffix) { + suffix = moduleFormatObj.suffix; + } else if (bsconfig.suffix) { + suffix = bsconfig.suffix; + } + + return [suffix, pathFragment]; +}; + +export const getFilenameFromBsconfig = ( + projDir: string, + partialFilePath: string +): string | null => { + let bsconfig = readBsConfig(projDir); + + if (!bsconfig) { + return null; + } + + let [suffix, pathFragment] = getSuffixAndPathFragmentFromBsconfig(bsconfig); + + let compiledPartialPath = replaceFileExtension(partialFilePath, suffix); + + return path.join(projDir, pathFragment, compiledPartialPath); +}; + +// Monorepo helpers +export const getFilenameFromRootBsconfig = ( + projDir: string, + partialFilePath: string +): string | null => { + let rootBsConfigPath = findFilePathFromProjectRoot( + path.join("..", projDir), + c.bsconfigPartialPath + ); + + if (!rootBsConfigPath) { + return null; + } + + let rootBsconfig = readBsConfig(path.dirname(rootBsConfigPath)); + + if (!rootBsconfig) { + return null; + } + + let [suffix, pathFragment] = + getSuffixAndPathFragmentFromBsconfig(rootBsconfig); + + let compiledPartialPath = replaceFileExtension(partialFilePath, suffix); + + return path.join(projDir, pathFragment, compiledPartialPath); +}; diff --git a/server/src/server.ts b/server/src/server.ts index 7a1e374ea..2fb305f59 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -15,6 +15,7 @@ import { CodeLensParams, SignatureHelpParams, } from "vscode-languageserver-protocol"; +import * as lookup from "./lookup"; import * as utils from "./utils"; import * as codeActions from "./codeActions"; import * as c from "./constants"; @@ -102,7 +103,10 @@ let send: (msg: p.Message) => void = (_) => {}; let findRescriptBinary = (projectRootPath: p.DocumentUri | null) => extensionConfiguration.binaryPath == null - ? utils.findFilePathFromProjectRoot(projectRootPath, path.join(c.nodeModulesBinDir, c.rescriptBinName)) + ? lookup.findFilePathFromProjectRoot( + projectRootPath, + path.join(c.nodeModulesBinDir, c.rescriptBinName) + ) : utils.findBinary(extensionConfiguration.binaryPath, c.rescriptBinName); let findPlatformPath = (projectRootPath: p.DocumentUri | null) => { @@ -110,12 +114,15 @@ let findPlatformPath = (projectRootPath: p.DocumentUri | null) => { return extensionConfiguration.platformPath; } - let rescriptDir = utils.findFilePathFromProjectRoot(projectRootPath, path.join("node_modules", "rescript")); + let rescriptDir = lookup.findFilePathFromProjectRoot( + projectRootPath, + path.join("node_modules", "rescript") + ); if (rescriptDir == null) { return null; } - let platformPath = path.join(rescriptDir, c.platformDir) + let platformPath = path.join(rescriptDir, c.platformDir); // Workaround for darwinarm64 which has no folder yet in ReScript <= 9.1.4 if ( @@ -127,10 +134,10 @@ let findPlatformPath = (projectRootPath: p.DocumentUri | null) => { } return platformPath; -} +}; let findBscExeBinary = (projectRootPath: p.DocumentUri | null) => - utils.findBinary(findPlatformPath(projectRootPath), c.bscExeName) + utils.findBinary(findPlatformPath(projectRootPath), c.bscExeName); interface CreateInterfaceRequestParams { uri: string; @@ -941,7 +948,7 @@ function createInterface(msg: p.RequestMessage): p.Message { let result = typeof response.result === "string" ? response.result : ""; try { - let resiPath = utils.replaceFileExtension(filePath, c.resiExt); + let resiPath = lookup.replaceFileExtension(filePath, c.resiExt); fs.writeFileSync(resiPath, result, { encoding: "utf-8" }); let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion, diff --git a/server/src/utils.ts b/server/src/utils.ts index 5360e10ec..1535c7050 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -1,5 +1,3 @@ -import * as c from "./constants"; -import * as codeActions from "./codeActions"; import * as childProcess from "child_process"; import * as p from "vscode-languageserver-protocol"; import * as path from "path"; @@ -11,6 +9,10 @@ import { import fs from "fs"; import * as os from "os"; +import * as codeActions from "./codeActions"; +import * as c from "./constants"; +import * as lookup from "./lookup"; + let tempFilePrefix = "rescript_format_file_" + process.pid + "_"; let tempFileId = 0; @@ -38,30 +40,6 @@ export let findProjectRootOfFile = ( } }; -// Check if filePartialPath exists at directory and return the joined path, -// otherwise recursively check parent directories for it. -export let findFilePathFromProjectRoot = ( - directory: p.DocumentUri | null, // This must be a directory and not a file! - filePartialPath: string -): null | p.DocumentUri => { - if (directory == null) { - return null; - } - - let filePath: p.DocumentUri = path.join(directory, filePartialPath); - if (fs.existsSync(filePath)) { - return filePath; - } - - let parentDir: p.DocumentUri = path.dirname(directory); - if (parentDir === directory) { - // reached the top - return null; - } - - return findFilePathFromProjectRoot(parentDir, filePartialPath); -}; - // Check if binaryName exists inside binaryDirPath and return the joined path. export let findBinary = ( binaryDirPath: p.DocumentUri | null, @@ -208,35 +186,16 @@ export let getReferencesForPosition = ( position.character, ]); -export let replaceFileExtension = (filePath: string, ext: string): string => { - let name = path.basename(filePath, path.extname(filePath)); - return path.format({ dir: path.dirname(filePath), name, ext }); -}; - export const toCamelCase = (text: string): string => { return text .replace(/(?:^\w|[A-Z]|\b\w)/g, (s: string) => s.toUpperCase()) .replace(/(\s|-)+/g, ""); }; -const readBsConfig = (projDir: p.DocumentUri) => { - try { - let bsconfigFile = fs.readFileSync( - path.join(projDir, c.bsconfigPartialPath), - { encoding: "utf-8" } - ); - - let result = JSON.parse(bsconfigFile); - return result; - } catch (e) { - return null; - } -}; - export const getNamespaceNameFromBsConfig = ( projDir: p.DocumentUri ): execResult => { - let bsconfig = readBsConfig(projDir); + let bsconfig = lookup.readBsConfig(projDir); let result = ""; if (!bsconfig) { @@ -258,70 +217,38 @@ export const getNamespaceNameFromBsConfig = ( }; }; -let getCompiledFolderName = (moduleFormat: string): string => { - switch (moduleFormat) { - case "es6": - return "es6"; - case "es6-global": - return "es6_global"; - case "commonjs": - default: - return "js"; - } -}; - export let getCompiledFilePath = ( filePath: string, projDir: string ): execResult => { - let bsconfig = readBsConfig(projDir); + let error: execResult = { + kind: "error", + error: "Could not read bsconfig", + }; + let partialFilePath = filePath.split(projDir)[1]; + let compiledPath = lookup.getFilenameFromBsconfig(projDir, partialFilePath); - if (!bsconfig) { - return { - kind: "error", - error: "Could not read bsconfig", - }; + if (!compiledPath) { + return error; } - let pkgSpecs = bsconfig["package-specs"]; - let pathFragment = ""; - let moduleFormatObj: any = {}; - - let module = c.bsconfigModuleDefault; - let suffix = c.bsconfigSuffixDefault; - - if (pkgSpecs) { - if (pkgSpecs.module) { - moduleFormatObj = pkgSpecs; - } else if (typeof pkgSpecs === "string") { - module = pkgSpecs; - } else if (pkgSpecs[0]) { - if (typeof pkgSpecs[0] === "string") { - module = pkgSpecs[0]; - } else { - moduleFormatObj = pkgSpecs[0]; - } - } - } + let result = compiledPath; - if (moduleFormatObj["module"]) { - module = moduleFormatObj["module"]; - } + // If the file is not found, lookup a possible root bsconfig that may contain + // info about the possible location of the file. + if (!fs.existsSync(result)) { + let compiledPath = lookup.getFilenameFromRootBsconfig( + projDir, + partialFilePath + ); - if (!moduleFormatObj["in-source"]) { - pathFragment = "lib/" + getCompiledFolderName(module); - } + if (!compiledPath) { + return error; + } - if (moduleFormatObj.suffix) { - suffix = moduleFormatObj.suffix; - } else if (bsconfig.suffix) { - suffix = bsconfig.suffix; + result = compiledPath; } - let partialFilePath = filePath.split(projDir)[1]; - let compiledPartialPath = replaceFileExtension(partialFilePath, suffix); - let result = path.join(projDir, pathFragment, compiledPartialPath); - return { kind: "success", result,