Skip to content

Commit

Permalink
feat(aa): ship generated types (#638)
Browse files Browse the repository at this point in the history
This ships declaration files for `@lavamoat/aa`.

- added [@types/resolve](https://npm.im/@types/resolve) for types from [resolve](https://npm.im/resolve)
- remove some dead code; `canonicalNameMap` didn't need to be passed to `walkDependencyTreeForBestLogicalPaths`
- maybe fixed a bug in signature for `walkDependencyTreeForBestLogicalPaths` where `performantResolve` was `undefined` (or pulled out of global scope??)
  • Loading branch information
boneskull committed Oct 5, 2023
1 parent 10c667b commit d8d5996
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ temp/
.idea

packages/allow-scripts/types
packages/aa/types

# VScode
.vscode
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/aa/.depcheckrc.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ignores:
# monorepo deps
- 'ava'
# types
- "@types/resolve"
6 changes: 5 additions & 1 deletion packages/aa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"test:run": "ava",
"lint:deps": "depcheck"
},
"devDependencies": {
"@types/resolve": "^1.20.2"
},
"dependencies": {
"resolve": "^1.22.3"
},
Expand All @@ -30,5 +33,6 @@
"test/*.spec.js"
],
"timeout": "30s"
}
},
"types": "./types/index.d.ts"
}
109 changes: 94 additions & 15 deletions packages/aa/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use strict'

const { readFileSync } = require('fs')
const path = require('path')
const nodeResolve = require('resolve')
Expand All @@ -10,7 +12,14 @@ module.exports = {
createPerformantResolve,
}

/**
* @returns {Resolver}
*/
function createPerformantResolve() {
/**
* @param {string} self
* @returns {(readFileSync: (file: string) => (string|{toString(): string}), pkgfile: string) => Record<string,unknown>}
*/
const readPackageWithout = (self) => (readFileSync, pkgfile) => {
// avoid loading the package.json we're just trying to resolve
if (pkgfile.endsWith(self)) {
Expand All @@ -19,6 +28,7 @@ function createPerformantResolve() {
// original readPackageSync implementation from resolve internals:
var body = readFileSync(pkgfile)
try {
// @ts-expect-error - JSON.parse calls toString() on its parameter if not given a string
var pkg = JSON.parse(body)
return pkg
} catch (jsonErr) {}
Expand All @@ -34,11 +44,11 @@ function createPerformantResolve() {
}

/**
* @param {object} options
* @returns {Promise<Map<string, string>>}
* @param {LoadCanonicalNameMapOpts} options
* @returns {Promise<CanonicalNameMap>}
*/
async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve } = {}) {
const canonicalNameMap = new Map()
async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve }) {
const canonicalNameMap = /** @type {CanonicalNameMap} */ (new Map())
// performant resolve avoids loading package.jsons if their path is what's being resolved,
// offering 2x performance improvement compared to using original resolve
resolve = resolve || createPerformantResolve()
Expand All @@ -48,7 +58,6 @@ async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve } = {}) {
packageDir: rootDir,
includeDevDeps,
resolve,
canonicalNameMap,
})
//convert dependency paths to canonical package names
for (const [packageDir, logicalPathParts] of logicalPathMap.entries()) {
Expand All @@ -61,18 +70,30 @@ async function loadCanonicalNameMap({ rootDir, includeDevDeps, resolve } = {}) {
return canonicalNameMap
}

/**
* @param {Resolver} resolve
* @param {string} depName
* @param {string} packageDir
* @returns {string|undefined}
*/
function wrappedResolveSync(resolve, depName, packageDir) {
const depRelativePackageJsonPath = path.join(depName, 'package.json')
try {
return resolve.sync(depRelativePackageJsonPath, { basedir: packageDir })
} catch (err) {
if (!err.message.includes('Cannot find module')) {
if (!(/** @type {Error} */ (err).message.includes('Cannot find module'))) {
throw err
}
// debug: log resolution failures
// console.log('resolve failed', depName, packageDir)
}
}

/**
* @param {string} packageDir
* @param {boolean} includeDevDeps
* @returns {string[]}
*/
function getDependencies(packageDir, includeDevDeps) {
const packageJsonPath = path.join(packageDir, 'package.json')
const rawPackageJson = readFileSync(packageJsonPath, 'utf8')
Expand All @@ -86,11 +107,15 @@ function getDependencies(packageDir, includeDevDeps) {
return depsToWalk
}

/** @type {WalkDepTreeOpts[]} */
let currentLevelTodos

/** @type {WalkDepTreeOpts[]} */
let nextLevelTodos

/**
* @param {object} options
* @returns {Map<{packageDir: string, logicalPathParts: string[]}>}
* @param {WalkDepTreeOpts} options
* @returns {Map<string, string[]>}
*/
function walkDependencyTreeForBestLogicalPaths({
packageDir,
Expand All @@ -100,6 +125,7 @@ function walkDependencyTreeForBestLogicalPaths({
resolve,
}) {
resolve = resolve ?? createPerformantResolve()
/** @type {Map<string, string[]>} */
const preferredPackageLogicalPathMap = new Map()
// add the entry package as the first work unit
currentLevelTodos = [
Expand All @@ -118,6 +144,10 @@ function walkDependencyTreeForBestLogicalPaths({
return preferredPackageLogicalPathMap
}

/**
* @param {Map<string, string[]>} preferredPackageLogicalPathMap
* @param {Resolver} resolve
*/
function processOnePackageInLogicalTree(
preferredPackageLogicalPathMap,
resolve
Expand All @@ -127,9 +157,8 @@ function processOnePackageInLogicalTree(
logicalPath = [],
includeDevDeps = false,
visited = new Set(),
} = currentLevelTodos.pop()
} = /** @type {WalkDepTreeOpts} */ (currentLevelTodos.pop())
const depsToWalk = getDependencies(packageDir, includeDevDeps)
const results = []

// deps are already sorted by preference for paths
for (const depName of depsToWalk) {
Expand Down Expand Up @@ -170,16 +199,20 @@ function processOnePackageInLogicalTree(
continue
}
}
return results
}

/**
* @param {CanonicalNameMap} canonicalNameMap
* @param {string} modulePath
* @returns {string}
*/
function getPackageNameForModulePath(canonicalNameMap, modulePath) {
const packageDir = getPackageDirForModulePath(canonicalNameMap, modulePath)
if (packageDir === undefined) {
const relativeToRoot = path.relative(canonicalNameMap.rootDir, modulePath)
return `external:${relativeToRoot}`
}
const packageName = canonicalNameMap.get(packageDir)
const packageName = /** @type {string} */ (canonicalNameMap.get(packageDir))
const relativeToPackageDir = path.relative(packageDir, modulePath)
// files should never be associated with a package directory across a package boundary (as tested via the presense of "node_modules" in the path)
if (relativeToPackageDir.includes('node_modules')) {
Expand All @@ -190,6 +223,11 @@ function getPackageNameForModulePath(canonicalNameMap, modulePath) {
return packageName
}

/**
* @param {CanonicalNameMap} canonicalNameMap
* @param {string} modulePath
* @returns {string|undefined}
*/
function getPackageDirForModulePath(canonicalNameMap, modulePath) {
// find which of these directories the module is in
const matchingPackageDirs = Array.from(canonicalNameMap.keys()).filter(
Expand All @@ -203,12 +241,22 @@ function getPackageDirForModulePath(canonicalNameMap, modulePath) {
return longestMatch
}

// for comparing string lengths
/**
* for comparing string lengths
* @param {string} a
* @param {string} b
* @returns {string}
*/
function takeLongest(a, b) {
return a.length > b.length ? a : b
}

// for package logical path names
/**
* for package logical path names
* @param {string} a
* @param {string} b
* @returns {0|1|-1}
*/
function comparePreferredPackageName(a, b) {
// prefer shorter package names
if (a.length > b.length) {
Expand All @@ -226,7 +274,12 @@ function comparePreferredPackageName(a, b) {
}
}

// for comparing package logical path arrays (shorter is better)
/**
* for comparing package logical path arrays (shorter is better)
* @param {string[]} [aPath]
* @param {string[]} [bPath]
* @returns {0|1|-1}
*/
function comparePackageLogicalPaths(aPath, bPath) {
// undefined is not preferred
if (aPath === undefined && bPath === undefined) {
Expand Down Expand Up @@ -261,3 +314,29 @@ function comparePackageLogicalPaths(aPath, bPath) {
}
return 0
}

/**
* @typedef Resolver
* @property {(path: string, opts: {basedir: string}) => string} sync
*/

/**
* @typedef LoadCanonicalNameMapOpts
* @property {string} rootDir
* @property {boolean} [includeDevDeps]
* @property {Resolver} [resolve]
*/

/**
* @internal
* @typedef WalkDepTreeOpts
* @property {string} packageDir
* @property {string[]} [logicalPath]
* @property {boolean} [includeDevDeps]
* @property {Set<string>} [visited]
* @property {Resolver} [resolve]
*/

/**
* @typedef {Map<string, string> & {rootDir: string}} CanonicalNameMap
*/
8 changes: 8 additions & 0 deletions packages/aa/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../config/tsconfig.base.json",
"include": ["src"],
"compilerOptions": {
"rootDir": "src",
"outDir": "types"
}
}
15 changes: 0 additions & 15 deletions packages/allow-scripts/src/types/lavamoat__aa.d.ts

This file was deleted.

8 changes: 6 additions & 2 deletions packages/allow-scripts/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
"compilerOptions": {
"outDir": "types",
"rootDir": "src",
"skipLibCheck": false
}
"skipLibCheck": false,
"paths": {
"@lavamoat/aa": ["../aa"]
}
},
"references": [{ "path": "../aa" }]
}
3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"references": [
{
"path": "packages/allow-scripts"
},
{
"path": "packages/aa"
}
]
}

0 comments on commit d8d5996

Please sign in to comment.