Skip to content

Commit 8aea83d

Browse files
JasonVMotido64
andauthored
feat(tools-packages): Add a tools package for loading and caching information about packages (#3520)
* update tools-workspaces to share more data and consume it in tools-package * add consumption of tools-package in config * rename tools-package to tools-packages, remove unneeded dependency * update readme files * docs(changeset): Create tools-packages which add some caching to loading package information * change accessor call signatures * update readme * Apply suggestions from code review Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com> * update readme and address PR feedback * fix build break * remove unused dependency * small change to readme file * fix bad lockfile merge by regenerating from main --------- Co-authored-by: Tommy Nguyen <4123478+tido64@users.noreply.github.com>
1 parent 64fb7ef commit 8aea83d

File tree

24 files changed

+597
-10
lines changed

24 files changed

+597
-10
lines changed

.changeset/poor-ducks-occur.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@rnx-kit/tools-packages": minor
3+
"@rnx-kit/tools-workspaces": patch
4+
"@rnx-kit/config": patch
5+
---
6+
7+
Create tools-packages which add some caching to loading package information

packages/config/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"dependencies": {
3838
"@rnx-kit/console": "^2.0.0",
3939
"@rnx-kit/tools-node": "^3.0.0",
40+
"@rnx-kit/tools-packages": "^0.0.1",
4041
"lodash.merge": "^4.6.2",
4142
"semver": "^7.0.0"
4243
},

packages/config/src/getKitConfig.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import {
33
findPackageDependencyDir,
44
readPackage,
55
} from "@rnx-kit/tools-node/package";
6+
import {
7+
type PackageInfo,
8+
createPackageValueLoader,
9+
getPackageInfoFromPath,
10+
} from "@rnx-kit/tools-packages";
611
import merge from "lodash.merge";
712
import * as fs from "node:fs";
813
import * as path from "node:path";
@@ -24,6 +29,17 @@ export type GetKitConfigOptions = {
2429
cwd?: string;
2530
};
2631

32+
// loader function for the package info accessor, used when it isn't already cached
33+
function loadConfigFromPackageInfo(pkgInfo: PackageInfo): KitConfig {
34+
const packageJson = pkgInfo.manifest;
35+
return loadBaseConfig(packageJson["rnx-kit"], pkgInfo.root) || {};
36+
}
37+
38+
export const getKitConfigFromPackageInfo = createPackageValueLoader(
39+
"kitConfig",
40+
loadConfigFromPackageInfo
41+
);
42+
2743
function findPackageDir({
2844
module,
2945
cwd = process.cwd(),
@@ -48,7 +64,18 @@ function loadBaseConfig(
4864
const spec = fs.existsSync(baseConfigPath)
4965
? baseConfigPath
5066
: require.resolve(base, { paths: [packageDir] });
51-
const mergedConfig = merge(require(spec), config);
67+
68+
let baseConfig: KitConfig;
69+
if (path.basename(spec).toLowerCase() === "package.json") {
70+
// if the reference is to a package.json file, load the config from the package info
71+
const pkgInfo = getPackageInfoFromPath(spec);
72+
baseConfig = getKitConfigFromPackageInfo(pkgInfo);
73+
} else {
74+
// otherwise require the file directly
75+
baseConfig = require(spec);
76+
}
77+
78+
const mergedConfig = merge(baseConfig, config);
5279
delete mergedConfig["extends"];
5380
return mergedConfig;
5481
}

packages/config/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ export { getBundleConfig, getPlatformBundleConfig } from "./getBundleConfig";
1111
export { getKitCapabilities } from "./getKitCapabilities";
1212
export type { KitCapabilities } from "./getKitCapabilities";
1313

14-
export { getKitConfig, getKitConfigFromPackageManifest } from "./getKitConfig";
14+
export {
15+
getKitConfig,
16+
getKitConfigFromPackageInfo,
17+
getKitConfigFromPackageManifest,
18+
} from "./getKitConfig";
1519
export type { GetKitConfigOptions } from "./getKitConfig";
1620

1721
export type {

packages/tools-packages/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# @rnx-kit/tools-packages
2+
3+
[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
4+
[![npm version](https://img.shields.io/npm/v/@rnx-kit/tools-packages)](https://www.npmjs.com/package/@rnx-kit/tools-packages)
5+
6+
This package has utilities for loading base information about packages,
7+
retrieved in a `PackageInfo` type, with a layer of caching that happens
8+
automatically, as well as the ability to store additional custom values in the
9+
retrieved `PackageInfo`
10+
11+
## Motivation
12+
13+
While loading package.json is pretty quick, this can quickly end up being a
14+
redundant operation as there different packages in rnx-kit all need different
15+
information from the file. This adds a simple caching layer for retrieving
16+
packages so work is not done multiple times.
17+
18+
The packages can also have custom accessors defined that allow storing of
19+
additional data in the `PackageInfo` and because of that, associated with that
20+
package in the cache. This might be loading the `KitConfig` parsing and
21+
validating a tsconfig.json file. This package doesn't need to care what is being
22+
stored, other packages can add their custom accessors as needed.
23+
24+
## Installation
25+
26+
```sh
27+
yarn add @rnx-kit/tools-packages --dev
28+
```
29+
30+
or if you're using npm
31+
32+
```sh
33+
npm add --save-dev @rnx-kit/tools-packages
34+
```
35+
36+
## Usage
37+
38+
There are two main parts of this package, helpers for retrieving package info
39+
and helpers for accessors.
40+
41+
### Types
42+
43+
| Type Name | Description |
44+
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45+
| `PlatformInfo` | Main returned type for the module. This contains information about the package name, root package path, the loaded package.json in `Manifest` form, whether or not the package is a workspace, as well as a `symbol` based index signature for attaching additional information to the type. |
46+
| `GetPackageValue<T>` | Format for a value accessor, used when creating accessors that only need to be loaded once. |
47+
| `PackageValueAccessors<T>` | Typed has/get/set methods to access values attached to the `PackageInfo` when they may be updated. |
48+
49+
### Functions
50+
51+
| Function | Description |
52+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
53+
| `getPackageInfoFromPath` | Given a path to either the root folder of a package, or the package.json for that package, return a loaded `PackageInfo` for that package. This will attempt to look up the package in the cache, loading it if not found. It will throw an exception on an invalid path. |
54+
| `getPackageInfoFromWorkspaces` | Try to retrieve a `PackageInfo` by name. This only works for in-workspace packages as module resolution outside of that scope is more complicated. Note that by default this only finds packages previously cached. If the optional boolean parameter is set to true, in the case that the package is not found, all workspaces will be loaded into the cache. This can be expensive though it is a one time cost. |
55+
| `getRootPackageInfo` | Get the package info for the root of the workspaces |
56+
| `createPackageValueLoader<T>` | Create a function which retrieves a cached value from `PackageInfo` calling the initializer function if it hasn't been loaded yet. This creates an internal symbol for to make the access unique with the supplied friendly name to make debugging easier. |
57+
| `createPackageValueAccessors` | Create three typed functions matching the has/get/set signature associated with a new and contained symbol. This is for accessors that may need to change over time. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require("@rnx-kit/eslint-config");
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@rnx-kit/tools-packages",
3+
"version": "0.0.1",
4+
"description": "tools-packages",
5+
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/packages/tools-packages#readme",
6+
"license": "MIT",
7+
"author": {
8+
"name": "Microsoft Open Source",
9+
"email": "microsoftopensource@users.noreply.github.com"
10+
},
11+
"files": [
12+
"lib/**/*.d.ts",
13+
"lib/**/*.js"
14+
],
15+
"main": "lib/index.js",
16+
"types": "lib/index.d.ts",
17+
"exports": {
18+
".": {
19+
"types": "./lib/index.d.ts",
20+
"typescript": "./src/index.ts",
21+
"default": "./lib/index.js"
22+
},
23+
"./package.json": "./package.json"
24+
},
25+
"repository": {
26+
"type": "git",
27+
"url": "https://github.com/microsoft/rnx-kit",
28+
"directory": "packages/tools-packages"
29+
},
30+
"engines": {
31+
"node": ">=16.17"
32+
},
33+
"scripts": {
34+
"build": "rnx-kit-scripts build",
35+
"format": "rnx-kit-scripts format",
36+
"lint": "rnx-kit-scripts lint",
37+
"test": "rnx-kit-scripts test"
38+
},
39+
"dependencies": {
40+
"@rnx-kit/tools-node": "^3.0.0",
41+
"@rnx-kit/tools-workspaces": "^0.2.0"
42+
},
43+
"devDependencies": {
44+
"@rnx-kit/eslint-config": "*",
45+
"@rnx-kit/scripts": "*",
46+
"@rnx-kit/tsconfig": "*",
47+
"@types/node": "^20.0.0",
48+
"eslint": "^9.0.0",
49+
"prettier": "^3.0.0",
50+
"typescript": "^5.0.0"
51+
}
52+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { GetPackageValue, PackageInfo } from "./types";
2+
3+
/**
4+
* Helper function to create a typed accessor function for getting and storing information
5+
* in PackageInfo. This can be whatever you want, the key is only created and stored in
6+
* the generated function so there are no collisions.
7+
*
8+
* @param friendlyName name used to create a symbol key for the package info
9+
* @param initialize function used to initialize the value stored in the key
10+
* @returns a function to retrieve the value from the package info, if unset the initialize function is called
11+
*/
12+
export function createPackageValueLoader<T>(
13+
friendlyName: string,
14+
initialize: (pkgInfo: PackageInfo) => T
15+
): GetPackageValue<T> {
16+
const symbolKey = Symbol(friendlyName);
17+
return (pkgInfo: PackageInfo) => {
18+
if (!(symbolKey in pkgInfo)) {
19+
pkgInfo[symbolKey] = initialize(pkgInfo);
20+
}
21+
return pkgInfo[symbolKey] as T;
22+
};
23+
}
24+
25+
/**
26+
* Create has/get/set accessors for a newly created symbol key that can look up values in PackageInfo
27+
*
28+
* @param friendlyName name used to create a symbol key for the package info
29+
* @returns a set of accessors for the symbol key
30+
*/
31+
export function createPackageValueAccessors<T>(friendlyName: string) {
32+
const symbolKey = Symbol(friendlyName);
33+
return {
34+
has(pkgInfo: PackageInfo) {
35+
return symbolKey in pkgInfo;
36+
},
37+
get(pkgInfo: PackageInfo) {
38+
return pkgInfo[symbolKey] as T;
39+
},
40+
set(pkgInfo: PackageInfo, value: T) {
41+
pkgInfo[symbolKey] = value;
42+
},
43+
};
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export {
2+
createPackageValueAccessors,
3+
createPackageValueLoader,
4+
} from "./accessors";
5+
export {
6+
getPackageInfoFromPath,
7+
getPackageInfoFromWorkspaces,
8+
} from "./package";
9+
export type {
10+
GetPackageValue,
11+
PackageInfo,
12+
PackageValueAccessors,
13+
} from "./types";
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { readPackage } from "@rnx-kit/tools-node";
2+
import { getWorkspacesInfoSync } from "@rnx-kit/tools-workspaces";
3+
import path from "node:path";
4+
import type { PackageInfo } from "./types";
5+
6+
class PackageInfoCache {
7+
private workspaceInfo;
8+
private byName = new Map<string, PackageInfo>();
9+
private byRoot = new Map<string, PackageInfo>();
10+
private loadedWorkspaces = false;
11+
private rootPath;
12+
13+
constructor() {
14+
this.workspaceInfo = getWorkspacesInfoSync();
15+
this.rootPath = this.workspaceInfo.getRoot();
16+
}
17+
18+
/**
19+
* @returns the root package info
20+
*/
21+
getRootInfo(): PackageInfo {
22+
return this.getByPath(this.rootPath);
23+
}
24+
25+
/**
26+
* @param pkgPath path to the package.json or the root of the package
27+
* @returns loaded PackageInfo for the package, or throws an exception if not found
28+
*/
29+
getByPath(pkgPath: string): PackageInfo {
30+
const [root, manifestPath] = this.getPackagePaths(pkgPath);
31+
32+
if (!this.byRoot.has(root)) {
33+
// it's not in the cache so load it
34+
const manifest = readPackage(manifestPath);
35+
const workspace = this.isWorkspace(root);
36+
37+
// it's not a valid package if the manifest can't be laoded
38+
if (manifest) {
39+
// create a new entry and cache it
40+
this.store({ name: manifest.name, root, manifest, workspace });
41+
}
42+
}
43+
44+
const result = this.byRoot.get(root);
45+
if (!result) {
46+
throw new Error(`No package.json found at ${pkgPath}`);
47+
}
48+
return result;
49+
}
50+
51+
/**
52+
* @param name the package to load, only valid for workspaces
53+
* @param loadWorkspacesIfNotFound do the expensive workspace load if not found
54+
* @returns a package info object for the workspace (if it exists)
55+
*/
56+
getWorkspace(name: string, loadWorkspacesIfNotFound?: boolean) {
57+
const cachedResult = this.byName.get(name);
58+
if (cachedResult || !loadWorkspacesIfNotFound) {
59+
return cachedResult;
60+
}
61+
62+
// load the workspaces if we haven't already which will populate the name cache
63+
this.loadWorkspaces();
64+
return this.byName.get(name);
65+
}
66+
67+
/** ensure the workspaces are fully loaded */
68+
private loadWorkspaces() {
69+
if (!this.loadedWorkspaces) {
70+
const packagePaths = this.workspaceInfo?.findPackagesSync() ?? [];
71+
for (const packagePath of packagePaths) {
72+
getPackageInfoFromPath(packagePath);
73+
}
74+
this.loadedWorkspaces = true;
75+
}
76+
}
77+
78+
/** return the root package path and the package.json path for a path that could be either */
79+
private getPackagePaths(pkgPath: string): [string, string] {
80+
const isPkgPath = path.basename(pkgPath).toLowerCase() === "package.json";
81+
return isPkgPath
82+
? [path.dirname(pkgPath), pkgPath]
83+
: [pkgPath, path.join(pkgPath, "package.json")];
84+
}
85+
86+
/** set the package info into the caches as appropriate */
87+
private store(pkgInfo: PackageInfo) {
88+
if (pkgInfo.workspace) {
89+
this.byName.set(pkgInfo.name, pkgInfo);
90+
}
91+
this.byRoot.set(pkgInfo.root, pkgInfo);
92+
}
93+
94+
/** check if this package is a workspace */
95+
private isWorkspace(root: string) {
96+
return Boolean(this.workspaceInfo?.isWorkspace(root));
97+
}
98+
}
99+
100+
let cache: PackageInfoCache | undefined = undefined;
101+
function ensureCache() {
102+
if (!cache) {
103+
cache = new PackageInfoCache();
104+
}
105+
return cache;
106+
}
107+
108+
/**
109+
* Looks up a package info by path, loading it if necessary
110+
*
111+
* @param pkgPath path to the package.json or the root of the package
112+
* @returns (potentially cached) package info for this package
113+
*/
114+
export function getPackageInfoFromPath(pkgPath: string): PackageInfo {
115+
return ensureCache().getByPath(pkgPath);
116+
}
117+
118+
/**
119+
* Load the package info by name, loading the project workspaces if necessary
120+
*
121+
* @param name name of the package to load
122+
* @param loadWorkspacesIfNotFound if the package is not in the cache load the workspaces (potentially expensive)
123+
* @returns the package info for the package
124+
*/
125+
export function getPackageInfoFromWorkspaces(
126+
name: string,
127+
loadWorkspacesIfNotFound?: boolean
128+
) {
129+
return ensureCache().getWorkspace(name, loadWorkspacesIfNotFound);
130+
}
131+
132+
/**
133+
* @returns the root package info
134+
*/
135+
export function getRootPackageInfo(): PackageInfo {
136+
return ensureCache().getRootInfo();
137+
}

0 commit comments

Comments
 (0)