Last active
May 6, 2023 08:59
-
-
Save BeSpunky/c9139cdedfa349c501a70febea3c46d5 to your computer and use it in GitHub Desktop.
ScriptKit Script: Creates a graph of scripts which are dependent on each ts file in the lib folder.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Name: Update Scripts Dependency Graph | |
// Description: Creates a graph of scripts which are dependent on each ts file in the lib folder. | |
import '@johnlindquist/kit'; | |
import { ensureIsTsFilePath } from '../lib/utils'; | |
import { ScriptDependencyGraphManager } from '../lib/dependency-graph'; | |
const triggeringFile = await arg({ | |
strict: false, | |
placeholder: `Full path to ts file`, | |
hint: `(Optional) Entering a file path will trigger a partial rebuild of the dependency graph, using the file as the starting point.` | |
}); | |
log(`👀 Dependency graph update triggered by '${ triggeringFile }'.`); | |
const graphManager = await ScriptDependencyGraphManager.init(); | |
if (triggeringFile) | |
{ | |
ensureIsTsFilePath(triggeringFile); | |
log('⛓️ Performing a partial graph rebuild...'); | |
await graphManager.partialRebuild(triggeringFile); | |
} | |
else | |
{ | |
log('⛓️ Performing a full graph rebuild...'); | |
await graphManager.rebuild(); | |
} | |
log('✅ Done!'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { TsFilePath } from './types/paths.types'; | |
import { isTsFilePath } from './utils'; | |
export type ScriptDependencyGraph = Record<TsFilePath, TsFilePath[]>; | |
const DbName = '_update-scripts-dependency-graph'; | |
export class ScriptDependencyGraphManager | |
{ | |
private static storage: ReturnType<typeof db<{ graph: ScriptDependencyGraph }>> extends Promise<infer T> ? T : never; | |
private static graph: ScriptDependencyGraph; | |
private static instance: ScriptDependencyGraphManager; | |
public static async init(): Promise<ScriptDependencyGraphManager> | |
{ | |
this.storage ??= await db(DbName, { graph: {} as ScriptDependencyGraph }); | |
this.graph ??= this.storage.graph; | |
this.instance ??= new ScriptDependencyGraphManager(); | |
return this.instance; | |
} | |
public async getDependantScripts(libraryPath: TsFilePath): Promise<TsFilePath[]> | |
{ | |
return ScriptDependencyGraphManager.graph[libraryPath] ?? []; | |
} | |
public async updateDependantScripts(libraryPath: TsFilePath, dependantScriptPaths: TsFilePath[], saveIt: boolean = true): Promise<ScriptDependencyGraph> | |
{ | |
// Aggregate instead of overwriting the library's dependencies to make sure we don't lose any dependencies that were not discovered in the current run. | |
const existingUnrelatedScriptPaths = (ScriptDependencyGraphManager.graph[libraryPath] ?? []).filter(path => !dependantScriptPaths.includes(path)); | |
ScriptDependencyGraphManager.graph[libraryPath] = [...new Set([...existingUnrelatedScriptPaths, ...dependantScriptPaths])]; | |
saveIt && ScriptDependencyGraphManager.storage.write(); | |
return ScriptDependencyGraphManager.graph; | |
} | |
public async rebuild(saveIt: boolean = true): Promise<ScriptDependencyGraph> | |
{ | |
const scriptPaths = await getAllTsScripts(); | |
const graph = await createDependencyGraph(scriptPaths); | |
ScriptDependencyGraphManager.graph = graph; | |
ScriptDependencyGraphManager.storage.graph = graph; | |
saveIt && ScriptDependencyGraphManager.storage.write(); | |
return graph; | |
} | |
public async partialRebuild(libOrScriptPath: TsFilePath, saveIt: boolean = true): Promise<ScriptDependencyGraph> | |
{ | |
const isScriptPath = (/.*\.kenv[\/\\]scripts[\/\\].*\.ts$/g.test(libOrScriptPath)) | |
const scriptPaths = isScriptPath ? [libOrScriptPath] : await this.getDependantScripts(libOrScriptPath); | |
const partialGraph = await createDependencyGraph(scriptPaths); | |
await Promise.all( | |
Object.entries(partialGraph) | |
.map(([libraryPath, dependantScriptPaths]) => this.updateDependantScripts(libraryPath as TsFilePath, dependantScriptPaths, false)) | |
); | |
saveIt && this.save(); | |
return ScriptDependencyGraphManager.graph; | |
} | |
public async save(): Promise<void> | |
{ | |
ScriptDependencyGraphManager.storage.write(); | |
} | |
} | |
export async function getAllTsScripts(): Promise<TsFilePath[]> | |
{ | |
const scripts = await readdir('./scripts'); | |
return scripts.filter(isTsFilePath) | |
.map(fileName => kenvPath('scripts', fileName) as TsFilePath); | |
} | |
export async function extractImportsFromFile(path: TsFilePath): Promise<TsFilePath[]> | |
{ | |
const scriptContent = await readFile(path, 'utf-8'); | |
const libImportRegex = /import.+from ['"]\.\.\/lib\/(?<dependency>.*)['"]/gm; | |
const libs = []; | |
let match: RegExpMatchArray | null; | |
while ((match = libImportRegex.exec(scriptContent)) !== null) | |
{ | |
// This is necessary to avoid infinite loops with zero-width matches | |
if (match.index === libImportRegex.lastIndex) | |
++libImportRegex.lastIndex; | |
libs.push(match.groups?.dependency); | |
} | |
return libs ?? []; | |
} | |
export function toLibFilePath(libImportExpression: string, relative: boolean = false): TsFilePath | |
{ | |
return relative | |
? `../lib/${ libImportExpression }.ts` | |
: kenvPath('lib', `${ libImportExpression }.ts`) as TsFilePath; | |
} | |
export async function extractDependenciesRecoursively(path: TsFilePath, visited: Set<TsFilePath> = new Set()): Promise<TsFilePath[]> | |
{ | |
if (visited.has(path)) return []; | |
visited.add(path); | |
const discoveredImports = await extractImportsFromFile(path); | |
if (!discoveredImports.length) return []; | |
const guessedFilePaths = discoveredImports.map(dep => toLibFilePath(dep)); | |
const tsFiles = await filterNonExistingFiles(guessedFilePaths); | |
const dependencies = await Promise.all(tsFiles.map(tsFile => extractDependenciesRecoursively(tsFile, visited))); | |
return [...discoveredImports, ...dependencies.flat()]; | |
} | |
export async function filterNonExistingFiles(libFilePaths: TsFilePath[]) | |
{ | |
const existingFiles = []; | |
for (const libFilePath of libFilePaths) | |
await isFile(libFilePath) && existingFiles.push(libFilePath); | |
return existingFiles; | |
} | |
export async function createDependencyGraph(scriptPaths: TsFilePath[]): Promise<ScriptDependencyGraph> | |
{ | |
const dependencyGraph: ScriptDependencyGraph = {}; | |
for (const scriptPath of scriptPaths) | |
{ | |
const dependencies = await extractDependenciesRecoursively(scriptPath); | |
for (const dependency of dependencies) | |
(dependencyGraph[toLibFilePath(dependency)] ??= []).push(scriptPath); | |
} | |
return dependencyGraph; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { Choice, Script } from '@johnlindquist/kit'; | |
import type { TsFilePath } from './types/paths.types'; | |
export function getRandomItemFromArray<T>(array: T[]): T | |
{ | |
return array[Math.floor(Math.random() * array.length)]; | |
} | |
export function randomId(): number | |
{ | |
return Math.round(Math.random() * 1000000000); | |
} | |
export function isTsFilePath(fileName: string): fileName is TsFilePath | |
{ | |
return fileName.endsWith('.ts'); | |
} | |
export function isTsScript(script: Script): script is Script & { filePath: TsFilePath } | |
{ | |
return isTsFilePath(script.filePath); | |
} | |
export function ensureIsTsFilePath(path: string, message?: string): asserts path is TsFilePath | |
{ | |
if (!isTsFilePath(path)) | |
{ | |
console.log(message ?? `🤔 '${ path }' is not a ts file. Exiting...`); | |
exit(); | |
} | |
} | |
export function ensureIsWatchChangeEvent(event: string): asserts event is 'change' | |
{ | |
if (event !== 'change') | |
{ | |
console.log(`🤔 '${ event }' is not a watch change event. Exiting...`); | |
exit(); | |
} | |
} | |
interface ScriptChoicesConfig<ValueAs extends keyof Script | ((script: Script) => any)> | |
{ | |
scripts: Script[]; | |
valueAs: ValueAs; | |
tsOnly: boolean; | |
addPreview: boolean | |
} | |
type ScriptChoice<Value extends keyof Script | ((script: Script) => any)> = Choice< | |
Value extends keyof Script ? Script[Value] : | |
Value extends (script: Script) => any ? ReturnType<Value> : never | |
> | |
export async function scriptChoices<ValueAs extends keyof Script | ((script: Script) => any)>( | |
{ | |
scripts, | |
valueAs, | |
tsOnly = true, | |
addPreview = true | |
}: Partial<ScriptChoicesConfig<ValueAs>> = {} | |
): Promise<ScriptChoice<ValueAs>[]> | |
{ | |
scripts ??= await getScripts(); | |
const createValue = (script: Script) => | |
typeof valueAs === 'function' | |
? valueAs(script) | |
: script[valueAs as keyof Script ?? 'fullPath']; | |
return (tsOnly ? scripts.filter(isTsScript) : scripts) | |
.map(script => ({ | |
name: script.name, | |
description: script.description, | |
value: createValue(script), | |
preview: addPreview ? () => highlightScript(script) : undefined | |
})); | |
} | |
export async function highlightScript(script: Script, language: string = 'ts'): Promise<string> | |
{ | |
const scriptContent = await readFile(script.filePath, 'utf-8'); | |
const markdown = ` | |
\`\`\`${ language } | |
${ scriptContent } | |
\`\`\` | |
`; | |
return highlight(md(markdown, 'p-0 m-0 h-full w-full')); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment