@travetto/manifest
Advanced tools
Comparing version 4.0.0-rc.0 to 4.0.0-rc.1
@@ -11,2 +11,5 @@ /// <reference path="./src/global.d.ts" /> | ||
export * from './src/file'; | ||
export * from './src/types'; | ||
export * from './src/types/context'; | ||
export * from './src/types/package'; | ||
export * from './src/types/manifest'; | ||
export * from './src/types/common'; |
@@ -7,5 +7,5 @@ import type { ManifestContext } from '../src/types'; | ||
*/ | ||
function getManifestContext(folder?: string): Promise<ManifestContext>; | ||
function getManifestContext(folder?: string): ManifestContext; | ||
} | ||
export = ManifestBootstrap; |
// @ts-check | ||
/** | ||
* @typedef {import('../src/types').Package & { path:string }} Pkg | ||
* @typedef {import('../src/types/package').Package & { path:string }} Pkg | ||
* @typedef {Pkg & { mono: boolean, manager: 'yarn'|'npm', resolve: (file:string) => string}} Workspace | ||
* @typedef {import('../src/types').ManifestContext} ManifestContext | ||
* @typedef {import('../src/types/context').ManifestContext} ManifestContext | ||
*/ | ||
import fs from 'node:fs/promises'; | ||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; | ||
import path from 'node:path'; | ||
@@ -20,8 +20,10 @@ import { createRequire } from 'node:module'; | ||
* @param {string} dir | ||
* @returns {Promise<Pkg|undefined>} | ||
* @returns {Pkg|undefined} | ||
*/ | ||
async function $readPackage(dir) { | ||
function $readPackage(dir) { | ||
dir = dir.endsWith('.json') ? path.dirname(dir) : dir; | ||
return await fs.readFile(path.resolve(dir, 'package.json'), 'utf8') | ||
.then(v => ({ ...JSON.parse(v), path: path.resolve(dir) }), () => undefined); | ||
try { | ||
const v = readFileSync(path.resolve(dir, 'package.json'), 'utf8'); | ||
return ({ ...JSON.parse(v), path: path.resolve(dir) }); | ||
} catch { } | ||
} | ||
@@ -32,9 +34,9 @@ | ||
* @param {string} dir | ||
* @return {Promise<Pkg>} | ||
* @return {Pkg} | ||
*/ | ||
async function $findPackage(dir) { | ||
function $findPackage(dir) { | ||
let prev; | ||
let pkg, curr = path.resolve(dir); | ||
while (!pkg && curr !== prev) { | ||
pkg = await $readPackage(curr); | ||
pkg = $readPackage(curr); | ||
[prev, curr] = [curr, path.dirname(curr)]; | ||
@@ -51,5 +53,5 @@ } | ||
* Get workspace root | ||
* @return {Promise<Workspace>} | ||
* @return {Workspace} | ||
*/ | ||
async function $resolveWorkspace(base = process.cwd()) { | ||
function $resolveWorkspace(base = process.cwd()) { | ||
if (base in WS_ROOT) { return WS_ROOT[base]; } | ||
@@ -63,6 +65,6 @@ let folder = base; | ||
[prev, prevPkg] = [folder, pkg]; | ||
pkg = await $readPackage(folder) ?? pkg; | ||
pkg = $readPackage(folder) ?? pkg; | ||
if ( | ||
(pkg && (!!pkg.workspaces || !!pkg.travetto?.build?.isolated)) || // if we have a monorepo root, or we are isolated | ||
await fs.stat(path.resolve(folder, '.git')).catch(() => { }) // we made it to the source repo root | ||
existsSync(path.resolve(folder, '.git')) // we made it to the source repo root | ||
) { | ||
@@ -82,3 +84,3 @@ break; | ||
type: pkg.type, | ||
manager: await fs.stat(path.resolve(pkg.path, 'yarn.lock')).catch(() => { }) ? 'yarn' : 'npm', | ||
manager: existsSync(path.resolve(pkg.path, 'yarn.lock')) ? 'yarn' : 'npm', | ||
resolve: createRequire(`${pkg.path}/node_modules`).resolve.bind(null), | ||
@@ -92,11 +94,12 @@ mono: !!pkg.workspaces || (!pkg.travetto?.build?.isolated && !!prevPkg) // Workspaces or nested projects | ||
* @param {Workspace} ws | ||
* @param {string} toolFolder | ||
*/ | ||
async function $getCompilerUrl(ws) { | ||
const file = path.resolve(ws.path, TOOL_FOLDER, 'build.compilerUrl'); | ||
function $getCompilerUrl(ws, toolFolder) { | ||
const file = path.resolve(ws.path, toolFolder, 'build.compilerUrl'); | ||
// eslint-disable-next-line no-bitwise | ||
const port = (Math.abs([...file].reduce((a, b) => (a * 33) ^ b.charCodeAt(0), 5381)) % 29000) + 20000; | ||
const out = `http://localhost:${port}`; | ||
try { await fs.stat(file); } catch { | ||
await fs.mkdir(path.dirname(file), { recursive: true }); | ||
await fs.writeFile(file, out, 'utf8'); | ||
if (!existsSync(file)) { | ||
mkdirSync(path.dirname(file), { recursive: true }); | ||
writeFileSync(file, out, 'utf8'); | ||
} | ||
@@ -111,3 +114,3 @@ return out; | ||
*/ | ||
async function $resolveModule(workspace, folder) { | ||
function $resolveModule(workspace, folder) { | ||
let mod; | ||
@@ -117,4 +120,7 @@ if (!folder && process.env.TRV_MODULE) { | ||
if (/[.](t|j)sx?$/.test(mod)) { // Rewrite from file to module | ||
process.env.TRV_MODULE = mod = await $findPackage(path.dirname(mod)) | ||
.then(v => v.name, () => ''); | ||
try { | ||
process.env.TRV_MODULE = mod = $findPackage(path.dirname(mod)).name; | ||
} catch { | ||
process.env.TRV_MODULE = mod = ''; | ||
} | ||
} | ||
@@ -127,3 +133,3 @@ } | ||
} catch { | ||
const workspacePkg = await $readPackage(workspace.path); | ||
const workspacePkg = $readPackage(workspace.path); | ||
if (workspacePkg?.name === mod) { | ||
@@ -143,8 +149,9 @@ folder = workspace.path; | ||
* @param {string} [folder] | ||
* @return {Promise<ManifestContext>} | ||
* @return {ManifestContext} | ||
*/ | ||
export async function getManifestContext(folder) { | ||
const workspace = await $resolveWorkspace(folder); | ||
const mod = await $resolveModule(workspace, folder); | ||
export function getManifestContext(folder) { | ||
const workspace = $resolveWorkspace(folder); | ||
const mod = $resolveModule(workspace, folder); | ||
const build = workspace.travetto?.build ?? {}; | ||
const toolFolder = build.toolFolder ?? TOOL_FOLDER; | ||
@@ -157,9 +164,11 @@ return { | ||
manager: workspace.manager, | ||
type: workspace.type ?? 'commonjs' | ||
type: workspace.type ?? 'commonjs', | ||
defaultEnv: workspace.travetto?.defaultEnv ?? 'local' | ||
}, | ||
build: { | ||
compilerFolder: build.compilerFolder ?? COMPILER_FOLDER, | ||
compilerUrl: build.compilerUrl ?? await $getCompilerUrl(workspace), | ||
compilerUrl: build.compilerUrl ?? $getCompilerUrl(workspace, toolFolder), | ||
compilerModuleFolder: path.dirname(workspace.resolve('@travetto/compiler/package.json')).replace(`${workspace.path}/`, ''), | ||
outputFolder: build.outputFolder ?? OUTPUT_FOLDER, | ||
toolFolder | ||
}, | ||
@@ -166,0 +175,0 @@ main: { |
{ | ||
"name": "@travetto/manifest", | ||
"version": "4.0.0-rc.0", | ||
"version": "4.0.0-rc.1", | ||
"description": "Support for project indexing, manifesting, along with file watching", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -33,3 +33,3 @@ <!-- This file was generated by @travetto/doc and should not be modified directly --> | ||
## Manifest Delta | ||
During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L20) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled. | ||
During the compilation process, it is helpful to know how the output content differs from the manifest, which is produced from the source input. The [ManifestDeltaUtil](https://github.com/travetto/travetto/tree/main/module/manifest/src/delta.ts#L21) provides the functionality for a given manifest, and will produce a stream of changes grouped by module. This is the primary input into the [Compiler](https://github.com/travetto/travetto/tree/main/module/compiler#readme "The compiler infrastructure for the Travetto framework")'s incremental behavior to know when a file has changed and needs to be recompiled. | ||
@@ -39,3 +39,3 @@ ## Class and Function Metadata | ||
`Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RuntimeIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/runtime.ts#L11) using the `getFunctionMetadata` method. | ||
`Ⲑid` is used heavily throughout the framework for determining which classes are owned by the framework, and being able to lookup the needed data from the [RuntimeIndex](https://github.com/travetto/travetto/tree/main/module/manifest/src/runtime.ts#L14) using the `getFunctionMetadata` method. | ||
@@ -102,3 +102,4 @@ **Code: Test Class** | ||
"manager": "npm", | ||
"type": "commonjs" | ||
"type": "commonjs", | ||
"defaultEnv": "local" | ||
}, | ||
@@ -109,3 +110,4 @@ "build": { | ||
"compilerModuleFolder": "module/compiler", | ||
"outputFolder": ".trv/output" | ||
"outputFolder": ".trv/output", | ||
"toolFolder": ".trv/tool" | ||
}, | ||
@@ -121,5 +123,6 @@ "main": { | ||
"main": true, | ||
"prod": true, | ||
"name": "@travetto/manifest", | ||
"version": "x.x.x", | ||
"local": true, | ||
"workspace": true, | ||
"internal": false, | ||
@@ -130,3 +133,2 @@ "sourceFolder": "module/manifest", | ||
"parents": [], | ||
"prod": true, | ||
"files": { | ||
@@ -169,4 +171,7 @@ "$root": [ | ||
[ "src/runtime.ts", "ts", 1868155200000 ], | ||
[ "src/types.ts", "ts", 1868155200000 ], | ||
[ "src/util.ts", "ts", 1868155200000 ] | ||
[ "src/util.ts", "ts", 1868155200000 ], | ||
[ "src/types/common.ts", "ts", 1868155200000 ], | ||
[ "src/types/context.ts", "ts", 1868155200000 ], | ||
[ "src/types/manifest.ts", "ts", 1868155200000 ], | ||
[ "src/types/package.ts", "ts", 1868155200000 ] | ||
], | ||
@@ -173,0 +178,0 @@ "bin": [ |
@@ -1,10 +0,10 @@ | ||
import { | ||
ManifestContext, ManifestModule, ManifestModuleCore, ManifestModuleFile, | ||
ManifestModuleFileType, ManifestModuleFolderType, ManifestRoot | ||
} from './types'; | ||
import fs from 'node:fs/promises'; | ||
import { ManifestModuleUtil } from './module'; | ||
import { ManifestFileUtil } from './file'; | ||
import { path } from './path'; | ||
import type { ManifestModule, ManifestModuleCore, ManifestModuleFile, ManifestRoot } from './types/manifest'; | ||
import type { ManifestModuleFileType, ManifestModuleFolderType } from './types/common'; | ||
import type { ManifestContext } from './types/context'; | ||
type DeltaEventType = 'added' | 'changed' | 'removed' | 'missing' | 'dirty'; | ||
@@ -15,3 +15,4 @@ type DeltaModule = ManifestModuleCore & { files: Record<string, ManifestModuleFile> }; | ||
const VALID_SOURCE_FOLDERS = new Set<ManifestModuleFolderType>(['bin', 'src', 'test', 'support', '$index', '$package', 'doc']); | ||
const VALID_SOURCE_TYPE = new Set<ManifestModuleFileType>(['js', 'ts', 'package-json']); | ||
const VALID_OUTPUT_TYPE = new Set<ManifestModuleFileType>(['js', 'ts', 'package-json']); | ||
const VALID_SOURCE_TYPE = new Set<ManifestModuleFileType>([...VALID_OUTPUT_TYPE, 'typings']); | ||
@@ -41,3 +42,3 @@ /** | ||
const type = ManifestModuleUtil.getFileType(x); | ||
return type === 'ts' || type === 'typings' || type === 'js' || type === 'package-json'; | ||
return VALID_SOURCE_TYPE.has(type); | ||
}) | ||
@@ -50,3 +51,3 @@ .map(x => ManifestModuleUtil.sourceToBlankExt(x.replace(`${root}/`, ''))) | ||
const [, , leftTs] = left.files[el]; | ||
const stat = await ManifestFileUtil.statFile(output); | ||
const stat = await fs.stat(output).catch(() => undefined); | ||
right.delete(ManifestModuleUtil.sourceToBlankExt(el)); | ||
@@ -83,3 +84,3 @@ | ||
for (const [name, type, date] of m.files?.[key] ?? []) { | ||
if (VALID_SOURCE_TYPE.has(type)) { | ||
if (VALID_OUTPUT_TYPE.has(type)) { | ||
out[name] = [name, type, date]; | ||
@@ -86,0 +87,0 @@ } |
import { PackageUtil } from './package'; | ||
import { path } from './path'; | ||
import { ManifestContext, ManifestModuleRole, PackageVisitor, PackageVisitReq, Package, ManifestDepCore } from './types'; | ||
export type ModuleDep = ManifestDepCore & { | ||
pkg: Package; | ||
mainLike?: boolean; | ||
sourcePath: string; | ||
childSet: Set<string>; | ||
parentSet: Set<string>; | ||
roleSet: Set<ManifestModuleRole>; | ||
topLevel?: boolean; | ||
}; | ||
import type { Package, PackageDepType, PackageVisitReq, PackageVisitor } from './types/package'; | ||
import type { ManifestContext } from './types/context'; | ||
import type { PackageModule } from './types/manifest'; | ||
type CreateOpts = Partial<Pick<PackageModule, 'main' | 'workspace' | 'prod'>> & { roleRoot?: boolean }; | ||
/** | ||
* Used for walking dependencies for collecting modules for the manifest | ||
*/ | ||
export class ModuleDependencyVisitor implements PackageVisitor<ModuleDep> { | ||
export class PackageModuleVisitor implements PackageVisitor<PackageModule> { | ||
@@ -24,34 +19,33 @@ constructor(public ctx: ManifestContext) { | ||
#mainLikeModules = new Set<string>(); | ||
#mainSourcePath: string; | ||
#cache: Record<string, PackageModule> = {}; | ||
#workspaceModules: Map<string, string>; | ||
/** | ||
* Main source path for searching | ||
*/ | ||
get rootPath(): string { | ||
return this.#mainSourcePath; | ||
} | ||
/** | ||
* Initialize visitor, and provide global dependencies | ||
*/ | ||
async init(req: PackageVisitReq<ModuleDep>): Promise<PackageVisitReq<ModuleDep>[]> { | ||
const pkg = PackageUtil.readPackage(req.sourcePath); | ||
const workspacePkg = PackageUtil.readPackage(this.ctx.workspace.path); | ||
const workspaceModules = pkg.workspaces?.length ? (await PackageUtil.resolveWorkspaces(this.ctx, req.sourcePath)) : []; | ||
async init(): Promise<Iterable<PackageVisitReq<PackageModule>>> { | ||
const mainPkg = PackageUtil.readPackage(this.#mainSourcePath); | ||
const mainReq = this.create(mainPkg, { main: true, workspace: true, roleRoot: true, prod: true }); | ||
const globals = [mainReq]; | ||
this.#workspaceModules = new Map( | ||
(await PackageUtil.resolveWorkspaces(this.ctx)).map(x => [x.name, x.path]) | ||
); | ||
this.#mainLikeModules = new Set([ | ||
pkg.name, | ||
...Object.entries(pkg.travetto?.build?.withModules ?? []).filter(x => x[1] === 'main').map(x => x[0]), | ||
// Add workspace folders, for tests and docs | ||
...workspaceModules.map(x => x.name) | ||
]); | ||
// Treat all workspace modules as main modules | ||
if (this.ctx.workspace.mono && !this.ctx.main.folder) { | ||
for (const [, loc] of this.#workspaceModules) { | ||
const depPkg = PackageUtil.readPackage(loc); | ||
globals.push(this.create(depPkg, { main: true, workspace: true, roleRoot: true })); | ||
} | ||
} else { | ||
// If we have 'withModules' at workspace root | ||
const root = PackageUtil.readPackage(this.ctx.workspace.path); | ||
for (const [name, type] of Object.entries(root.travetto?.build?.withModules ?? {})) { | ||
const depPkg = PackageUtil.readPackage(PackageUtil.resolvePackagePath(name)); | ||
globals.push(this.create(depPkg, { main: type === 'main', workspace: true })); | ||
} | ||
} | ||
const globals = Object.keys(workspacePkg.travetto?.build?.withModules ?? []) | ||
.map(name => PackageUtil.packageReq<ModuleDep>(PackageUtil.resolvePackagePath(name), name in (workspacePkg.dependencies ?? {}), true)); | ||
const workspaceModuleDeps = workspaceModules | ||
.map(entry => PackageUtil.packageReq<ModuleDep>(path.resolve(req.sourcePath, entry.sourcePath), false, true)); | ||
return [...globals, ...workspaceModuleDeps]; | ||
return globals.map((x, i) => i === 0 ? x : { ...x, parent: mainReq.value }); | ||
} | ||
@@ -62,25 +56,29 @@ | ||
*/ | ||
valid(req: PackageVisitReq<ModuleDep>): boolean { | ||
return req.sourcePath === this.#mainSourcePath || ( | ||
(!!req.pkg.travetto || req.pkg.private === true || !req.sourcePath.includes('node_modules')) | ||
); | ||
valid({ value: node }: PackageVisitReq<PackageModule>): boolean { | ||
return node.workspace || !!node.state.travetto; // Workspace or travetto module | ||
} | ||
/** | ||
* Create dependency from request | ||
* Build a package module | ||
*/ | ||
create(req: PackageVisitReq<ModuleDep>): ModuleDep { | ||
const { pkg, sourcePath } = req; | ||
const { name, version } = pkg; | ||
const main = name === this.ctx.main.name; | ||
const mainLike = main || this.#mainLikeModules.has(name); | ||
const internal = pkg.private === true; | ||
const local = internal || mainLike || !sourcePath.includes('node_modules'); | ||
const dep: ModuleDep = { | ||
name, version, sourcePath, main, mainLike, local, internal, pkg: req.pkg, | ||
parentSet: new Set([]), childSet: new Set([]), roleSet: new Set([]), prod: req.prod, topLevel: req.topLevel | ||
create(pkg: Package, { main, workspace, prod = false, roleRoot = false }: CreateOpts = {}): PackageVisitReq<PackageModule> { | ||
const sourcePath = PackageUtil.getPackagePath(pkg); | ||
const value = this.#cache[sourcePath] ??= { | ||
main, | ||
prod, | ||
name: pkg.name, | ||
version: pkg.version, | ||
workspace: workspace ?? this.#workspaceModules.has(pkg.name), | ||
internal: pkg.private === true, | ||
sourceFolder: sourcePath === this.ctx.workspace.path ? '' : sourcePath.replace(`${this.ctx.workspace.path}/`, ''), | ||
outputFolder: `node_modules/${pkg.name}`, | ||
state: { | ||
childSet: new Set(), parentSet: new Set(), roleSet: new Set(), roleRoot, | ||
travetto: pkg.travetto, prodDeps: new Set(Object.keys(pkg.dependencies ?? {})) | ||
} | ||
}; | ||
return dep; | ||
const deps: PackageDepType[] = ['dependencies', ...(value.main ? ['devDependencies'] as const : [])]; | ||
const children = Object.fromEntries(deps.flatMap(x => Object.entries(pkg[x] ?? {}))); | ||
return { pkg, value, children }; | ||
} | ||
@@ -91,7 +89,7 @@ | ||
*/ | ||
visit(req: PackageVisitReq<ModuleDep>, dep: ModuleDep): void { | ||
const { parent } = req; | ||
if (parent && dep.name !== this.ctx.main.name) { | ||
dep.parentSet.add(parent.name); | ||
parent.childSet.add(dep.name); | ||
visit({ value: mod, parent }: PackageVisitReq<PackageModule>): void { | ||
if (mod.name === this.ctx.main.name) { return; } // Skip root | ||
if (parent) { | ||
mod.state.parentSet.add(parent.name); | ||
parent.state.childSet.add(mod.name); | ||
} | ||
@@ -103,23 +101,17 @@ } | ||
*/ | ||
complete(deps: Set<ModuleDep>): Set<ModuleDep> { | ||
const mapping = new Map<string, { parent: Set<string>, child: Set<string>, el: ModuleDep }>(); | ||
for (const el of deps) { | ||
mapping.set(el.name, { parent: new Set(el.parentSet), child: new Set(el.childSet), el }); | ||
} | ||
async complete(mods: Iterable<PackageModule>): Promise<PackageModule[]> { | ||
const mapping = new Map([...mods].map(el => [el.name, { parent: new Set(el.state.parentSet), el }])); | ||
const main = mapping.get(this.ctx.main.name)!; | ||
// Visit all direct dependencies and mark | ||
for (const { el } of mapping.values()) { | ||
if (!main.child.has(el.name)) { // Not a direct descendent | ||
el.prod = false; | ||
// All first-level dependencies should have role filled in (for propagation) | ||
for (const dep of [...mods].filter(x => x.state.roleRoot)) { | ||
dep.state.roleSet.clear(); // Ensure the roleRoot is empty | ||
for (const c of dep.state.childSet) { // Visit children | ||
const cDep = mapping.get(c)!.el; | ||
if (cDep.state.roleRoot) { continue; } | ||
// Set roles for all top level modules | ||
cDep.state.roleSet = new Set(cDep.state.travetto?.roles ?? ['std']); | ||
} | ||
if (main.child.has(el.name) || (el.topLevel && el !== main.el)) { // Direct descendant | ||
el.roleSet = new Set(el.pkg.travetto?.roles ?? []); | ||
if (!el.roleSet.size) { | ||
el.roleSet.add('std'); | ||
} | ||
} | ||
} | ||
// Visit all nodes | ||
while (mapping.size > 0) { | ||
@@ -130,11 +122,16 @@ const toProcess = [...mapping.values()].filter(x => x.parent.size === 0); | ||
} | ||
// Propagate | ||
for (const { el, child } of toProcess) { | ||
for (const c of child.keys()) { | ||
const { el: cDep, parent } = mapping.get(c)!; | ||
parent.delete(el.name); // Remove from child | ||
for (const role of el.roleSet) { | ||
cDep.roleSet.add(role); | ||
// Propagate to children | ||
for (const { el } of toProcess) { | ||
for (const c of el.state.childSet) { | ||
const child = mapping.get(c); | ||
if (!child) { continue; } | ||
child.parent.delete(el.name); | ||
// Propagate roles from parent to child | ||
if (!child.el.state.roleRoot) { | ||
for (const role of el.state.roleSet) { | ||
child.el.state.roleSet.add(role); | ||
} | ||
} | ||
cDep.prod ||= el.prod; // Allow prod to trickle down as needed | ||
// Allow prod to trickle down as needed | ||
child.el.prod ||= (el.prod && el.state.prodDeps.has(c)); | ||
} | ||
@@ -148,8 +145,9 @@ } | ||
// Color parent as final step | ||
main.el.prod = true; | ||
main.el.roleSet.add('std'); | ||
// Mark as standard at the end | ||
for (const dep of [...mods].filter(x => x.state.roleRoot)) { | ||
dep.state.roleSet = new Set(['std']); | ||
} | ||
return deps; | ||
return [...mods].sort((a, b) => a.name.localeCompare(b.name)); | ||
} | ||
} |
@@ -6,3 +6,2 @@ import os from 'node:os'; | ||
import { path } from './path'; | ||
import type { ManifestContext } from './types'; | ||
@@ -20,3 +19,3 @@ export class ManifestFileUtil { | ||
await fs.copyFile(temp, file); | ||
fs.unlink(temp); | ||
fs.unlink(temp); // Don't wait for completion | ||
return file; | ||
@@ -38,21 +37,2 @@ } | ||
} | ||
/** | ||
* Stat file | ||
*/ | ||
static statFile(file: string): Promise<{ mtimeMs: number, ctimeMs: number } | undefined> { | ||
return fs.stat(file).catch(() => undefined); | ||
} | ||
/** | ||
* Resolve tool path for usage | ||
*/ | ||
static toolPath(ctx: ManifestContext | { manifest: ManifestContext }, rel: string, moduleSpecific = false): string { | ||
ctx = 'manifest' in ctx ? ctx.manifest : ctx; | ||
const parts = [rel]; | ||
if (moduleSpecific) { | ||
parts.unshift('node_modules', ctx.main.name); | ||
} | ||
return path.resolve(ctx.workspace.path, '.trv/tool', ...parts); | ||
} | ||
} |
@@ -0,35 +1,10 @@ | ||
import { existsSync } from 'node:fs'; | ||
import { ManifestModuleUtil } from './module'; | ||
import { path } from './path'; | ||
import { | ||
ManifestModule, ManifestModuleCore, ManifestModuleFile, | ||
ManifestModuleFileType, ManifestModuleFolderType, ManifestModuleRole, ManifestRoot | ||
} from './types'; | ||
import { ManifestUtil } from './util'; | ||
export type FindConfig = { | ||
folder?: (folder: ManifestModuleFolderType) => boolean; | ||
module?: (module: IndexedModule) => boolean; | ||
file?: (file: IndexedFile) => boolean; | ||
sourceOnly?: boolean; | ||
}; | ||
import type { ManifestModuleFolderType } from './types/common'; | ||
import type { ManifestModule, ManifestRoot, ManifestModuleFile, IndexedModule, IndexedFile, FindConfig } from './types/manifest'; | ||
export type IndexedFile = { | ||
id: string; | ||
import: string; | ||
module: string; | ||
sourceFile: string; | ||
outputFile: string; | ||
relativeFile: string; | ||
role: ManifestModuleRole; | ||
type: ManifestModuleFileType; | ||
}; | ||
export type IndexedModule = ManifestModuleCore & { | ||
sourcePath: string; | ||
outputPath: string; | ||
files: Record<ManifestModuleFolderType, IndexedFile[]>; | ||
}; | ||
const TypedObject: { | ||
@@ -46,3 +21,3 @@ keys<T = unknown, K extends keyof T = keyof T>(o: T): K[]; | ||
#manifestFile: string; | ||
#arbitraryLookup?: (parts: string[]) => ManifestModule | undefined; | ||
#manifest: ManifestRoot; | ||
@@ -73,10 +48,4 @@ #modules: IndexedModule[]; | ||
get manifestFile(): string { | ||
return this.#manifestFile; | ||
} | ||
init(manifestInput: string): void { | ||
const { manifest, file } = ManifestUtil.readManifestSync(manifestInput); | ||
this.#manifest = manifest; | ||
this.#manifestFile = file; | ||
this.#manifest = ManifestUtil.readManifestSync(manifestInput); | ||
this.#outputRoot = path.resolve(this.#manifest.workspace.path, this.#manifest.build.outputFolder); | ||
@@ -109,2 +78,3 @@ this.#index(); | ||
this.#sourceToEntry.clear(); | ||
this.#arbitraryLookup = undefined; | ||
@@ -116,2 +86,3 @@ this.#modules = Object.values(this.#manifest.modules) | ||
sourcePath: path.resolve(this.#manifest.workspace.path, m.sourceFolder), | ||
children: new Set(), | ||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
@@ -135,2 +106,9 @@ files: Object.fromEntries( | ||
this.#modulesByFolder = Object.fromEntries(this.#modules.map(x => [x.sourceFolder, x])); | ||
// Store child information | ||
for (const mod of this.#modules) { | ||
for (const p of mod.parents) { | ||
this.#modulesByName[p]?.children.add(mod.name); | ||
} | ||
} | ||
} | ||
@@ -146,7 +124,7 @@ | ||
/** | ||
* Get all local modules | ||
* Get all workspace modules | ||
* @returns | ||
*/ | ||
getLocalModules(): IndexedModule[] { | ||
return this.#modules.filter(x => x.local); | ||
getWorkspaceModules(): IndexedModule[] { | ||
return this.#modules.filter(x => x.workspace); | ||
} | ||
@@ -246,8 +224,5 @@ | ||
*/ | ||
getModuleList(mode: 'local' | 'all', exprList: string = ''): Set<string> { | ||
getModuleList(mode: 'workspace' | 'all', exprList: string = ''): Set<string> { | ||
const allMods = Object.keys(this.#manifest.modules); | ||
const active = new Set<string>( | ||
mode === 'local' ? this.getLocalModules().map(x => x.name) : | ||
(mode === 'all' ? allMods : []) | ||
); | ||
const active = new Set<string>(mode === 'workspace' ? this.getWorkspaceModules().map(x => x.name) : allMods); | ||
@@ -267,19 +242,36 @@ for (const expr of exprList.split(/\s*,\s*/g)) { | ||
/** | ||
* Get all modules (transitively) that depend on this module | ||
* Get all modules, parents or children, (transitively) of the provided root, in a DFS fashion | ||
*/ | ||
getDependentModules(root: IndexedModule): Set<IndexedModule> { | ||
getDependentModules(root: IndexedModule | string, field: 'parents' | 'children'): IndexedModule[] { | ||
const seen = new Set<string>(); | ||
const out = new Set<IndexedModule>(); | ||
const toProcess = [root.name]; | ||
const out: IndexedModule[] = []; | ||
const toProcess = [typeof root === 'string' ? root : root.name]; | ||
while (toProcess.length) { | ||
const next = toProcess.shift()!; | ||
if (seen.has(next)) { | ||
continue; | ||
if (!seen.has(next)) { | ||
seen.add(next); | ||
const mod = this.getModule(next)!; | ||
toProcess.push(...mod[field]); | ||
if (next !== this.#manifest.main.name) { // Do not include self | ||
out.push(mod); | ||
} | ||
} | ||
const mod = this.getModule(next)!; | ||
toProcess.push(...mod.parents); | ||
out.add(mod); | ||
} | ||
return out; | ||
} | ||
/** | ||
* Find the module for an arbitrary source file, if it falls under a given workspace module | ||
*/ | ||
findModuleForArbitraryFile(file: string): ManifestModule | undefined { | ||
const base = this.#manifest.workspace.path; | ||
const lookup = this.#arbitraryLookup ??= ManifestUtil.lookupTrie( | ||
Object.values(this.#manifest.modules), | ||
x => x.sourceFolder.split('/'), | ||
sub => | ||
!existsSync(path.resolve(base, ...sub, 'package.json')) && | ||
!existsSync(path.resolve(base, ...sub, '.git')) | ||
); | ||
return lookup(file.replace(`${base}/`, '').split('/')); | ||
} | ||
} |
import fs from 'node:fs/promises'; | ||
import { path } from './path'; | ||
import { | ||
ManifestContext, | ||
ManifestModule, ManifestModuleFile, ManifestModuleFileType, | ||
ManifestModuleFolderType, | ||
ManifestModuleRole | ||
} from './types'; | ||
import { ModuleDep, ModuleDependencyVisitor } from './dependencies'; | ||
import { PackageUtil } from './package'; | ||
import { PackageModuleVisitor } from './dependencies'; | ||
import type { ManifestModuleFileType, ManifestModuleRole, ManifestModuleFolderType } from './types/common'; | ||
import type { ManifestModuleFile, ManifestModule, PackageModule } from './types/manifest'; | ||
import type { ManifestContext } from './types/context'; | ||
const EXT_MAPPING: Record<string, ManifestModuleFileType> = { | ||
@@ -50,5 +48,6 @@ '.js': 'js', | ||
*/ | ||
static async scanFolder(folder: string, mainLike = false): Promise<string[]> { | ||
if (!mainLike && folder in this.#scanCache) { | ||
return this.#scanCache[folder]; | ||
static async scanFolder(folder: string, full = false): Promise<string[]> { | ||
const key = `${folder}|${full}`; | ||
if (key in this.#scanCache) { | ||
return this.#scanCache[key]; | ||
} | ||
@@ -77,3 +76,3 @@ | ||
for (const sub of await fs.readdir(top)) { | ||
const valid = !sub.startsWith('.') && (depth > 0 || mainLike); | ||
const valid = !sub.startsWith('.') && (depth > 0 || full); | ||
const stat = await fs.stat(`${top}/${sub}`); | ||
@@ -92,7 +91,3 @@ if (stat.isFile()) { | ||
if (!mainLike) { | ||
this.#scanCache[folder] = out; | ||
} | ||
return out; | ||
return this.#scanCache[key] = out; | ||
} | ||
@@ -183,8 +178,9 @@ | ||
*/ | ||
static async describeModule(ctx: ManifestContext, dep: ModuleDep): Promise<ManifestModule> { | ||
const { main, mainLike, local, name, version, sourcePath, roleSet, prod, parentSet, internal } = dep; | ||
static async describeModule(ctx: ManifestContext, mod: PackageModule): Promise<ManifestModule> { | ||
const { state, ...rest } = mod; | ||
const sourcePath = path.resolve(ctx.workspace.path, rest.sourceFolder); | ||
const files: ManifestModule['files'] = {}; | ||
for (const file of await this.scanFolder(sourcePath, mainLike)) { | ||
for (const file of await this.scanFolder(sourcePath, rest.main)) { | ||
// Group by top folder | ||
@@ -197,16 +193,8 @@ const moduleFile = file.replace(`${sourcePath}/`, ''); | ||
// Refine non-main source, remove anything in root that is source (doesn't include $index) | ||
if (!mainLike) { | ||
files.$root = files.$root?.filter(([file, type]) => type !== 'ts'); | ||
} | ||
const roles = [...roleSet ?? []].sort(); | ||
const parents = [...parentSet].sort(); | ||
const outputFolder = `node_modules/${name}`; | ||
const sourceFolder = sourcePath === ctx.workspace.path ? '' : sourcePath.replace(`${ctx.workspace.path}/`, ''); | ||
const res: ManifestModule = { | ||
main, name, version, local, internal, sourceFolder, outputFolder, roles, parents, prod, files | ||
return { | ||
...rest, | ||
roles: [...state.roleSet].sort(), | ||
parents: [...state.parentSet].sort(), | ||
files | ||
}; | ||
return res; | ||
} | ||
@@ -218,6 +206,4 @@ | ||
static async produceModules(ctx: ManifestContext): Promise<Record<string, ManifestModule>> { | ||
const visitor = new ModuleDependencyVisitor(ctx); | ||
const declared = await PackageUtil.visitPackages(visitor); | ||
const sorted = [...declared].sort((a, b) => a.name.localeCompare(b.name)); | ||
const modules = await Promise.all(sorted.map(x => this.describeModule(ctx, x))); | ||
const pkgs = await PackageUtil.visitPackages(new PackageModuleVisitor(ctx)); | ||
const modules = await Promise.all([...pkgs].map(x => this.describeModule(ctx, x))); | ||
return Object.fromEntries(modules.map(m => [m.name, m])); | ||
@@ -224,0 +210,0 @@ } |
import { createRequire } from 'node:module'; | ||
import { execSync } from 'node:child_process'; | ||
import type { ManifestContext, NodePackageManager, Package, PackageVisitor, PackageVisitReq, PackageWorkspaceEntry } from './types'; | ||
import { path } from './path'; | ||
import { ManifestFileUtil } from './file'; | ||
import { PackagePath, type Package, type PackageVisitor, type PackageWorkspaceEntry } from './types/package'; | ||
import type { ManifestContext } from './types/context'; | ||
import type { NodePackageManager } from './types/common'; | ||
/** | ||
@@ -34,5 +37,5 @@ * Utilities for querying, traversing and reading package.json files. | ||
*/ | ||
static resolveVersionPath(rootPath: string, ver: string): string | undefined { | ||
static resolveVersionPath(root: Package, ver: string): string | undefined { | ||
if (ver.startsWith('file:')) { | ||
return path.resolve(rootPath, ver.replace('file:', '')); | ||
return path.resolve(this.getPackagePath(root), ver.replace('file:', '')); | ||
} else { | ||
@@ -59,27 +62,2 @@ return; | ||
/** | ||
* Build a package visit req | ||
*/ | ||
static packageReq<T>(sourcePath: string, prod: boolean, topLevel?: boolean): PackageVisitReq<T> { | ||
return { pkg: this.readPackage(sourcePath), sourcePath, prod, topLevel }; | ||
} | ||
/** | ||
* Extract all dependencies from a package | ||
*/ | ||
static getAllDependencies<T = unknown>(modulePath: string, local: boolean): PackageVisitReq<T>[] { | ||
const pkg = this.readPackage(modulePath); | ||
const children: Record<string, PackageVisitReq<T>> = {}; | ||
for (const [deps, prod] of [ | ||
[pkg.dependencies, true], | ||
...(local ? [[pkg.devDependencies, false] as const] : []), | ||
] as const) { | ||
for (const [name, version] of Object.entries(deps ?? {})) { | ||
const depPath = this.resolveVersionPath(modulePath, version) ?? this.resolvePackagePath(name); | ||
children[`${name}#${version}`] = this.packageReq<T>(depPath, prod, false); | ||
} | ||
} | ||
return Object.values(children).sort((a, b) => a.pkg.name.localeCompare(b.pkg.name)); | ||
} | ||
/** | ||
* Read a package.json from a given folder | ||
@@ -97,2 +75,3 @@ */ | ||
res[PackagePath] = modulePath; | ||
return res; | ||
@@ -102,6 +81,6 @@ } | ||
/** | ||
* import a package.json from a given module name | ||
* Get the package path | ||
*/ | ||
static importPackage(moduleName: string): Package { | ||
return this.readPackage(this.resolvePackagePath(moduleName)); | ||
static getPackagePath(pkg: Package): string { | ||
return pkg[PackagePath]; | ||
} | ||
@@ -112,38 +91,29 @@ | ||
*/ | ||
static async visitPackages<T>(visitor: PackageVisitor<T>): Promise<Set<T>> { | ||
static async visitPackages<T>(visitor: PackageVisitor<T>): Promise<Iterable<T>> { | ||
const seen = new Set<T>(); | ||
const queue = [...await visitor.init()]; | ||
const root = this.packageReq<T>(visitor.rootPath, false, true); | ||
const seen = new Map<string, T>(); | ||
const queue: PackageVisitReq<T>[] = [...await visitor.init?.(root) ?? [], root]; | ||
const out = new Set<T>(); | ||
while (queue.length) { | ||
const req = queue.pop(); | ||
const node = queue.shift()!; // Visit initial set first | ||
if (!req || (visitor.valid && !visitor.valid(req))) { | ||
if (!visitor.valid(node)) { | ||
continue; | ||
} | ||
const key = req.sourcePath; | ||
if (seen.has(key)) { | ||
await visitor.visit?.(req, seen.get(key)!); | ||
visitor.visit(node); | ||
if (seen.has(node.value)) { | ||
continue; | ||
} else { | ||
const dep = await visitor.create(req); | ||
out.add(dep); | ||
await visitor.visit?.(req, dep); | ||
seen.set(key, dep); | ||
const children = this.getAllDependencies<T>( | ||
req.sourcePath, | ||
// We consider a module local if its not in the node_modules | ||
!req.sourcePath.includes('node_modules') && ( | ||
// And its the root or we are in a monorepo | ||
root.sourcePath === req.sourcePath || | ||
!!root.pkg.workspaces | ||
) | ||
); | ||
queue.push(...children.map(x => ({ ...x, parent: dep }))); | ||
seen.add(node.value); | ||
} | ||
const children = Object.entries(node.children) | ||
.map(([n, v]) => this.readPackage(this.resolveVersionPath(node.pkg, v) ?? this.resolvePackagePath(n))) | ||
.map(pkg => ({ ...visitor.create(pkg), parent: node.value })); | ||
queue.push(...children); | ||
} | ||
return (await visitor.complete?.(out)) ?? out; | ||
return await visitor.complete(seen); | ||
} | ||
@@ -154,28 +124,19 @@ | ||
*/ | ||
static async resolveWorkspaces(ctx: ManifestContext, rootPath: string): Promise<PackageWorkspaceEntry[]> { | ||
if (!this.#workspaces[rootPath]) { | ||
const cache = path.resolve(ctx.workspace.path, ctx.build.outputFolder, 'workspaces.json'); | ||
try { | ||
return await ManifestFileUtil.readAsJson(cache); | ||
} catch (err) { | ||
static async resolveWorkspaces(ctx: ManifestContext): Promise<PackageWorkspaceEntry[]> { | ||
const rootPath = ctx.workspace.path; | ||
const cache = path.resolve(rootPath, ctx.build.outputFolder, 'workspaces.json'); | ||
return this.#workspaces[rootPath] ??= await ManifestFileUtil.readAsJson<PackageWorkspaceEntry[]>(cache) | ||
.catch(async () => { | ||
let out: PackageWorkspaceEntry[]; | ||
switch (ctx.workspace.manager) { | ||
case 'yarn': | ||
case 'npm': { | ||
const res = await this.#exec<{ location: string, name: string }[]>(rootPath, 'npm query .workspace'); | ||
out = res.map(d => ({ sourcePath: d.location, name: d.name })); | ||
out = res.map(d => ({ path: path.resolve(ctx.workspace.path, d.location), name: d.name })); | ||
break; | ||
} | ||
case 'yarn': { | ||
const res = await this.#exec<Record<string, { location: string }>>(rootPath, 'npm query .workspace'); | ||
out = Object.entries(res).map(([name, { location }]) => ({ sourcePath: location, name })); | ||
break; | ||
} | ||
} | ||
this.#workspaces[rootPath] = out; | ||
await ManifestFileUtil.bufferedFileWrite(cache, out); | ||
} | ||
} | ||
return this.#workspaces[rootPath]; | ||
return out; | ||
}); | ||
} | ||
@@ -182,0 +143,0 @@ |
import { path } from './path'; | ||
import { IndexedModule, ManifestIndex } from './manifest-index'; | ||
import { FunctionMetadata, ManifestContext, ManifestModule } from './types'; | ||
import { ManifestIndex } from './manifest-index'; | ||
import type { FunctionMetadata } from './types/common'; | ||
import type { IndexedModule, ManifestModule } from './types/manifest'; | ||
import type { ManifestContext } from './types/context'; | ||
const METADATA = Symbol.for('@travetto/manifest:metadata'); | ||
@@ -125,3 +128,17 @@ type Metadated = { [METADATA]: FunctionMetadata }; | ||
return path.resolve(RuntimeIndex.manifest.workspace.path, ...rel); | ||
}, | ||
/** | ||
* Produce a workspace path for tooling, with '@' being replaced by node_module/name folder | ||
* @param rel The relative path | ||
*/ | ||
toolPath(...rel: string[]): string { | ||
rel = rel.flatMap(x => x === '@' ? ['node_modules', RuntimeIndex.manifest.main.name] : [x]); | ||
return path.resolve(RuntimeIndex.manifest.workspace.path, RuntimeIndex.manifest.build.toolFolder, ...rel); | ||
}, | ||
/** | ||
* Are we running from a mono-root? | ||
*/ | ||
get monoRoot(): boolean { | ||
return !!RuntimeIndex.manifest.workspace.mono && !RuntimeIndex.manifest.main.folder; | ||
} | ||
}, ['main', 'workspace']); |
import { path } from './path'; | ||
import { ManifestContext, ManifestRoot } from './types'; | ||
import { ManifestModuleUtil } from './module'; | ||
@@ -7,2 +6,5 @@ import { ManifestFileUtil } from './file'; | ||
import type { ManifestContext } from './types/context'; | ||
import type { ManifestRoot } from './types/manifest'; | ||
const MANIFEST_FILE = 'manifest.json'; | ||
@@ -19,5 +21,7 @@ | ||
return { | ||
generated: Date.now(), | ||
workspace: ctx.workspace, | ||
build: ctx.build, | ||
main: ctx.main, | ||
modules: await ManifestModuleUtil.produceModules(ctx), | ||
generated: Date.now(), | ||
...ctx | ||
}; | ||
@@ -27,13 +31,17 @@ } | ||
/** | ||
* Produce a manifest location given a current context and a module name | ||
*/ | ||
static getManifestLocation(ctx: ManifestContext, module?: string): string { | ||
return path.resolve(ctx.workspace.path, ctx.build.outputFolder, 'node_modules', module ?? ctx.workspace.name); | ||
} | ||
/** | ||
* Produce a production manifest from a given manifest | ||
*/ | ||
static createProductionManifest(manifest: ManifestRoot): ManifestRoot { | ||
const prodModules = Object.values(manifest.modules).filter(x => x.prod); | ||
const prodModNames = new Set([...prodModules.map(x => x.name)]); | ||
return { | ||
...manifest, | ||
// If in prod mode, only include std modules | ||
modules: Object.fromEntries( | ||
Object.values(manifest.modules) | ||
.filter(x => x.prod) | ||
.map(m => [m.name, m]) | ||
), | ||
generated: manifest.generated, | ||
workspace: manifest.workspace, | ||
build: { | ||
@@ -43,3 +51,9 @@ ...manifest.build, | ||
outputFolder: '$$PRODUCTION$$', | ||
} | ||
}, | ||
main: manifest.main, | ||
modules: Object.fromEntries( | ||
prodModules.map(m => [m.name, Object.assign(m, { | ||
parents: m.parents.filter(x => prodModNames.has(x)) | ||
})]) | ||
), | ||
}; | ||
@@ -54,3 +68,3 @@ } | ||
*/ | ||
static readManifestSync(file: string): { manifest: ManifestRoot, file: string } { | ||
static readManifestSync(file: string): ManifestRoot { | ||
file = path.resolve(file); | ||
@@ -66,3 +80,3 @@ if (!file.endsWith('.json')) { | ||
} | ||
return { manifest, file }; | ||
return manifest; | ||
} | ||
@@ -98,3 +112,4 @@ | ||
return ctx.workspace.mono ? { | ||
...ctx, | ||
workspace: ctx.workspace, | ||
build: ctx.build, | ||
main: { | ||
@@ -116,3 +131,4 @@ name: ctx.workspace.name, | ||
return { | ||
...ctx, | ||
workspace: ctx.workspace, | ||
build: ctx.build, | ||
main: { | ||
@@ -126,2 +142,41 @@ name: pkg.name, | ||
} | ||
/** | ||
* Efficient lookup for path-based graphs | ||
*/ | ||
static lookupTrie<T>( | ||
inputs: T[], getPath: (v: T) => string[], validateUnknown?: (pth: string[]) => boolean | ||
): (pth: string[]) => T | undefined { | ||
type TrieNode = { value?: T, subs: Record<string, TrieNode> }; | ||
const root: TrieNode = { subs: {} }; | ||
for (const item of inputs) { | ||
const pth = getPath(item); | ||
let node = root; | ||
for (const sub of pth) { | ||
if (sub) { | ||
node = node.subs[sub] ??= { subs: {} }; | ||
} | ||
} | ||
node.value = item; | ||
} | ||
return pth => { | ||
let node = root; | ||
let value = node.value; | ||
let i = 0; | ||
for (const sub of pth) { | ||
i += 1; | ||
if (node) { | ||
node = node.subs[sub]; | ||
value = node?.value ?? value; | ||
} else if (validateUnknown && !node && !validateUnknown(pth.slice(0, i))) { | ||
value = undefined; | ||
break; | ||
} | ||
} | ||
return value; | ||
}; | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
70826
21
1598
236
5