Skip to content

Commit

Permalink
feat(tools-node): Add a destructureModuleRef function that does all t…
Browse files Browse the repository at this point in the history
…he module reference parsing at the same time (#3505)

* add destructuring function for parsing module refs

* update docs

* docs(changeset): add a full module destructuring function, route other routines to use this new function.

* change away from regex based module destructuring

* update tests to expect keys with undefined values
  • Loading branch information
JasonVMo authored Feb 5, 2025
1 parent 87e909b commit 8b3d041
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 54 deletions.
6 changes: 6 additions & 0 deletions .changeset/tough-buckets-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rnx-kit/tools-node": patch
---

add a full module destructuring function, route other routines to use this new
function.
32 changes: 17 additions & 15 deletions packages/tools-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,27 @@ import * as pathTools from "@rnx-kit/tools-node/path";
| -------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| module | FileModuleRef | Module reference rooted to a file system location, either relative to a directory, or as an absolute path. For example, `./index` or `/repos/rnx-kit/packages/tools/src/index`. |
| module | PackageModuleRef | Module reference relative to a package, such as `react-native` or `@rnx-kit/tools/node/index`. |
| package | DestructuredModuleRef | Module reference with the package name and optional sub-module path included as path |
| package | FindPackageDependencyOptions | Options which control how package dependecies are located. |
| package | PackageManifest | Schema for the contents of a `package.json` manifest file. |
| package | PackagePerson | Schema for a reference to a person in `package.json`. |
| package | PackageRef | Components of a package reference. |

| Category | Function | Description |
| -------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| module | `getPackageModuleRefFromModulePath(modulePath)` | Convert a module path to a package module reference. |
| module | `isFileModuleRef(r)` | Is the module reference relative to a file location? |
| module | `isPackageModuleRef(r)` | Is the module reference a package module reference? |
| module | `parseModuleRef(r)` | Parse a module reference into either a package module reference or a file module reference. |
| package | `findPackage(startDir)` | Find the nearest `package.json` manifest file. Search upward through all parent directories. |
| package | `findPackageDependencyDir(ref, options)` | Find the package dependency's directory, starting from the given directory and moving outward, through all parent directories. |
| package | `findPackageDir(startDir)` | Find the parent directory of the nearest `package.json` manifest file. Search upward through all parent directories. |
| package | `parsePackageRef(r)` | Parse a package reference string. An example reference is the `name` property found in `package.json`. |
| package | `readPackage(pkgPath)` | Read a `package.json` manifest from a file. |
| package | `resolveDependencyChain(chain, startDir)` | Resolve the path to a dependency given a chain of dependencies leading up to it. |
| package | `writePackage(pkgPath, manifest, space)` | Write a `package.json` manifest to a file. |
| path | `findUp(names, options)` | Finds the specified file(s) or directory(s) by walking up parent directories. |
| path | `normalizePath(p)` | Normalize the separators in a path, converting each backslash ('\\') to a foreward slash ('/'). |
| Category | Function | Description |
| -------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| module | `getPackageModuleRefFromModulePath(modulePath)` | Convert a module path to a package module reference. |
| module | `isFileModuleRef(r)` | Is the module reference relative to a file location? |
| module | `isPackageModuleRef(r)` | Is the module reference a package module reference? |
| module | `parseModuleRef(r)` | Parse a module reference into either a package module reference or a file module reference. If there are any sub-paths, they are returned in paths. |
| package | `destructureModuleRef(r)` | Destructure a module reference into its component par |
| package | `findPackage(startDir)` | Find the nearest `package.json` manifest file. Search upward through all parent directories. |
| package | `findPackageDependencyDir(ref, options)` | Find the package dependency's directory, starting from the given directory and moving outward, through all parent directories. |
| package | `findPackageDir(startDir)` | Find the parent directory of the nearest `package.json` manifest file. Search upward through all parent directories. |
| package | `parsePackageRef(r)` | Parse a package reference string. An example reference is the `name` property found in `package.json`. |
| package | `readPackage(pkgPath)` | Read a `package.json` manifest from a file. |
| package | `resolveDependencyChain(chain, startDir)` | Resolve the path to a dependency given a chain of dependencies leading up to it. |
| package | `writePackage(pkgPath, manifest, space)` | Write a `package.json` manifest to a file. |
| path | `findUp(names, options)` | Finds the specified file(s) or directory(s) by walking up parent directories. |
| path | `normalizePath(p)` | Normalize the separators in a path, converting each backslash ('\\') to a foreward slash ('/'). |

<!-- @rnx-kit/api end -->
2 changes: 2 additions & 0 deletions packages/tools-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export {
export type { FileModuleRef, PackageModuleRef } from "./module";

export {
destructureModuleRef,
findPackage,
findPackageDependencyDir,
findPackageDir,
Expand All @@ -16,6 +17,7 @@ export {
writePackage,
} from "./package";
export type {
DestructuredModuleRef,
FindPackageDependencyOptions,
PackageManifest,
PackagePerson,
Expand Down
24 changes: 9 additions & 15 deletions packages/tools-node/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import path from "path";
import type { PackageRef } from "./package";
import { findPackageDir, parsePackageRef, readPackage } from "./package";
import {
destructureModuleRef,
findPackageDir,
parsePackageRef,
readPackage,
} from "./package";

/**
* Module reference relative to a package, such as `react-native` or
Expand All @@ -21,9 +26,9 @@ export type FileModuleRef = {

/**
* Parse a module reference into either a package module reference or a file
* module reference.
* module reference. If there are any sub-paths, they are returned in paths.
*
* @param moduleRef Module reference
* @param r Module reference
* @return Module components
*/
export function parseModuleRef(r: string): PackageModuleRef | FileModuleRef {
Expand All @@ -33,18 +38,7 @@ export function parseModuleRef(r: string): PackageModuleRef | FileModuleRef {
};
}

const ref: PackageModuleRef = parsePackageRef(r);

const indexPath = ref.name.indexOf("/");
if (indexPath >= 0) {
const p = ref.name.substring(indexPath + 1);
if (p) {
ref.path = p;
}
ref.name = ref.name.substring(0, indexPath);
}

return ref;
return destructureModuleRef(r);
}

/**
Expand Down
77 changes: 55 additions & 22 deletions packages/tools-node/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,58 @@ export type PackageRef = {
name: string;
};

/**
* Module reference with the package name and optional sub-module path included as path
*/
export type DestructuredModuleRef = PackageRef & {
path?: string;
};

function getModuleRefParts(
ref: string,
found: string[] = [],
requested = 2,
startPos = 0
): string[] {
if (startPos < ref.length) {
if (requested > 1) {
const splitAt = ref.indexOf("/", startPos);
if (splitAt >= 0) {
requested += startPos === 0 && ref[0] === "@" ? 1 : 0;
found.push(ref.slice(startPos, splitAt));
return getModuleRefParts(ref, found, requested - 1, splitAt + 1);
}
}
found.push(ref.slice(startPos));
}
return found;
}

/**
* Destructure a module reference into its component par
* @param r module reference to destructure
* @returns either a destructured module reference or undefined if it is a file reference
*/
export function destructureModuleRef(r: string): DestructuredModuleRef {
if (r && !r.endsWith("/")) {
const parts = getModuleRefParts(r);
if (parts.length > 0) {
// if we only have one term the first term will be treated as the name, even if it is a scope
if (parts.length === 1) {
return { name: parts[0] };
}
// otherwise split the parts and return them
const scope = parts[0].startsWith("@") ? parts.shift() : undefined;
const name = parts.shift();
const path = parts.shift();
if (name) {
return { name, scope, path };
}
}
}
throw new Error(`Invalid package reference: "${r}"`);
}

/**
* Parse a package reference string. An example reference is the `name`
* property found in `package.json`.
Expand All @@ -18,28 +70,9 @@ export type PackageRef = {
* @returns Parsed package reference object
*/
export function parsePackageRef(r: string): PackageRef {
if (r.startsWith("@")) {
// If `/` is not found, the reference could be a path alias.
const indexSeparator = r.indexOf("/");
if (indexSeparator >= 0) {
// The separator must have at least 1 character following it, before the
// end of the string. Note that the scope may be an empty string.
// TypeScript does not place any restrictions on import re-mappings.
if (indexSeparator + 1 >= r.length) {
throw new Error(`Invalid package reference: ${r}`);
}

return {
scope: r.substring(0, indexSeparator),
name: r.substring(indexSeparator + 1),
};
}
}

if (!r) {
throw new Error(`Invalid package reference: "${r}"`);
}
return { name: r };
const { scope, name, path } = destructureModuleRef(r);
const fullName = path ? `${name}/${path}` : name;
return { name: fullName, scope };
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/tools-node/test/module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ describe("Node > Module", () => {
deepEqual(parseModuleRef("react-native/Libraries/Promise"), {
name: "react-native",
path: "Libraries/Promise",
scope: undefined,
});
});

it("parseModuleRef('@babel/core')", () => {
deepEqual(parseModuleRef("@babel/core"), {
scope: "@babel",
name: "core",
path: undefined,
});
});

Expand All @@ -50,6 +52,7 @@ describe("Node > Module", () => {
deepEqual(parseModuleRef("@types/babel__core"), {
scope: "@types",
name: "babel__core",
path: undefined,
});
});

Expand Down
7 changes: 5 additions & 2 deletions packages/tools-node/test/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ describe("Node > Package", () => {
});

it("parsePackageRef(react-native) returns an unscoped reference", () => {
deepEqual(parsePackageRef("react-native"), { name: "react-native" });
deepEqual(parsePackageRef("react-native"), {
name: "react-native",
scope: undefined,
});
});

it("parsePackageRef(@babel/core) returns a scoped reference", () => {
Expand All @@ -47,7 +50,7 @@ describe("Node > Package", () => {
});

it("parsePackageRef(@alias) is allowed", () => {
deepEqual(parsePackageRef("@alias"), { name: "@alias" });
deepEqual(parsePackageRef("@alias"), { name: "@alias", scope: undefined });
});

it("parsePackageRef(@/core) is allowed", () => {
Expand Down

0 comments on commit 8b3d041

Please sign in to comment.