UNPKG

@convo-lang/convo-lang-cli

Version:
819 lines 32.6 kB
import { convoDescriptionToComment, convoJsDocTags, escapeConvo, escapeConvoTagValue } from "@convo-lang/convo-lang"; import { getDirectoryName, getObjKeyCount, joinPaths, normalizePath, strHashBase64 } from "@iyio/common"; import { pathExistsAsync, readDirAsync, readFileAsStringAsync } from "@iyio/node-common"; import { mkdir, realpath, watch, writeFile } from "fs/promises"; import { relative } from "path"; import { Project, SyntaxKind } from "ts-morph"; const ignoreTypes = ['ConvoComponentRendererContext']; const arrayTypeMap = { Int8Array: 'number', Uint8Array: 'number', Uint8ClampedArray: 'number', Int16Array: 'number', Uint16Array: 'number', Int32Array: 'number', Uint32Array: 'number', Float32Array: 'number', Float64Array: 'number', BigInt64Array: 'number', BigUint64Array: 'number', }; export const convertConvoInterfacesAsync = async (options, cancel) => { const { syncTsConfig, syncWatch, syncOut, } = options; if (!syncTsConfig?.length) { throw new Error('--sync-ts-config required'); } if (!syncOut) { throw new Error('--sync-out required'); } if (!await pathExistsAsync(syncOut)) { await mkdir(syncOut, { recursive: true }); } const outFullPath = await realpath(syncOut); const projects = await Promise.all(syncTsConfig.map(p => normalizePath(p)).map(async (p) => ({ path: p, fullPath: await realpath(getDirectoryName(p), { encoding: 'utf-8' }), project: new Project({ tsConfigFilePath: p, }), out: normalizePath(syncOut), outFullPath, options, zodOut: [], tsOut: [], convoOut: [], comps: [], fns: [], mods: [], cancel, isScanning: false, scanRequested: false, convoHash: '', refreshRequested: false, reloadPaths: [], typeSchemes: {}, log: (...msgs) => console.log(...msgs), }))); await Promise.all(projects.map(p => scanProjectAsync(p, syncWatch ?? false))); if (syncWatch) { await Promise.all(projects.map(p => watchProjectAsync(p))); } }; const modSortKey = (mod) => `${mod.uri ?? ''}::${mod.name}`; const isProjectEmpty = (project) => { return (project.tsOut.length === 0 && project.zodOut.length === 0 && project.convoOut.length === 0 && project.comps.length === 0 && project.fns.length === 0); }; const clearProject = (project) => { project.tsOut.splice(0, project.tsOut.length); project.zodOut.splice(0, project.zodOut.length); project.convoOut.splice(0, project.convoOut.length); project.comps.splice(0, project.comps.length); project.fns.splice(0, project.fns.length); project.typeSchemes = {}; }; const scanProjectAsync = async (project, catchErrors) => { if (project.isScanning) { project.scanRequested = true; return; } project.isScanning = true; const reloads = project.reloadPaths; project.reloadPaths = []; // project.tsOut.splice(0,project.tsOut.length); // project.zodOut.splice(0,project.zodOut.length); // project.convoOut.splice(0,project.convoOut.length); // project.comps.splice(0,project.comps.length); // project.fns.splice(0,project.fns.length); // project.mods.splice(0,project.mods.length); project.mods.splice(0, project.mods.length); clearProject(project); if (project.refreshRequested) { project.refreshRequested = false; project.project = new Project({ tsConfigFilePath: project.path }); } else if (reloads.length) { for (const p of reloads) { await project.project.getSourceFile(p)?.refreshFromFileSystem(); } } try { for (const file of project.project.getSourceFiles()) { const path = file.getFilePath(); if (path.startsWith(project.fullPath) && path !== project.outFullPath && !path.startsWith(project.outFullPath)) { await scanFileAsync(file, project); } } const convoFiles = await readDirAsync({ path: project.fullPath, recursive: true, include: 'file', filter: /\.convo$/ }); await Promise.all(convoFiles.map(file => scanConvoFileAsync(file, project))); project.mods.sort((a, b) => modSortKey(a).localeCompare(modSortKey(b))); const convoHash = strHashBase64(project.mods.map(m => m.hashSrc).join('>')); if (!await pathExistsAsync(project.out)) { await mkdir(project.out, { recursive: true }); } if (convoHash !== project.convoHash) { project.convoHash = convoHash; const prefix = '@/'; const info = (`export const convoPackagePaths=${JSON.stringify(project.mods.map(m => prefix + m.name), null, 4)} as const;${'\n'}export type ConvoPackagePath=(typeof convoPackagePaths[number])|(string & {});`); const infoPath = joinPaths(project.out, 'convoPackage.ts'); const tsPath = joinPaths(project.out, 'convoPackageModules.ts'); const tsxPath = joinPaths(project.out, 'convoPackageComponentModules.tsx'); await Promise.all([ writeFile(infoPath, info), writeFile(tsPath, modsToString(project.mods.filter(m => !m.components), 'convoPackageModules', '@/')), writeFile(tsxPath, modsToString(project.mods.filter(m => m.components), 'convoPackageComponentModules', '@/')), ]); project.log(`convo> sync complete - ${project.path}`); } // const tsPath=joinPaths(project.out,'convo-binding-interfaces.ts'); // const zodPath=joinPaths(project.out,'convo-schemes.ts'); // const convoPath=joinPaths(project.out,'types.convo'); // const convoTsPath=joinPaths(project.out,'convo.ts'); // const convoTsCompPath=joinPaths(project.out,'convo-comp-reg.tsx'); // project.comps.sort((a,b)=>a.name.localeCompare(b.name)); // const convoSource='> define\n\n'+project.convoOut.join(''); // const convoTsSource=`export const convoTypes=/*convo*/\`\n${ // convoSource.replace(tickReg,'\\`') // }\`;\n`; // const convoTsCompSource=`${ // project.comps.length?'import { ConvoComponentRendererContext, ConvoComponent } from "@convo-lang/convo-lang";\n':'// no components found' // }${ // project.comps.map(c=>c.importStatement).join('\n') // }\n\n// Components are generated using the convo-lang CLI which looks for components marked with the @${convoJsDocTags.convoComponent} JSDoc tag\n\nexport const convoCompReg={\n${ // project.comps.map(c=>( // ` ${c.name}:{\n${ // ' '}render:(comp:ConvoComponent,ctx:ConvoComponentRendererContext)=><${c.name} {...({comp,ctx,ctrl:ctx.ctrl,convo:ctx.ctrl.convo,...comp.atts} as any)} />,\n${ // ' '}convo:/*convo*/\`\n@transformComponent ${c.name} ${c.propsType // }\n@transformDescription ${ // escapeConvoTagValue(c.description) // }\n@transformOptional\n${ // c.keepSource?'@transformKeepSource\n':'' // }> system\n${ // escapeConvo(c.instructions) // }\n\`\n${ // ' }'},` // )).join('\n') // }\n} as const\n`; // const convoHash=strHashBase64(convoSource+convoTsCompSource); // if(!await pathExistsAsync(project.out)){ // await mkdir(project.out,{recursive:true}); // } // if(convoHash!==project.convoHash){ // project.convoHash=convoHash; // await Promise.all([ // writeFile(tsPath,project.tsOut.join('')), // writeFile(zodPath,`import { z } from "zod";\n\n${project.zodOut.join('')}`), // writeFile(convoTsPath,convoTsSource), // writeFile(convoTsCompPath,convoTsCompSource), // writeFile(convoPath,convoSource), // ]); // project.log(`convo> sync complete - ${project.path}`); // } } catch (ex) { project.log(`convo> sync failed - ${project.path}`, ex); if (!catchErrors) { throw ex; } } finally { project.isScanning = false; if (project.scanRequested) { project.scanRequested = false; scanProjectAsync(project, catchErrors); } } }; const modsToString = (mods, exportName, namePrefix) => { const out = []; let importZod = false; for (const mod of mods) { for (const i of mod.tsImports) { const s = i + '\n'; if (!out.includes(s)) { out.push(s); } } } const convoLangImports = { ConvoModule: true }; const importI = out.length; out.push(`\nexport const ${exportName}:ConvoModule[]=[\n`); for (const mod of mods) { out.push(` {\n`); out.push(` name:${JSON.stringify(namePrefix + mod.name)},\n`); if (mod.components) { convoLangImports['ConvoComponent'] = true; convoLangImports['ConvoComponentRendererContext'] = true; out.push(' components:{\n'); for (const name in mod.components) { const c = mod.components[name]; if (!c) { continue; } out.push(` ${name}:{\n`); out.push(` name:${JSON.stringify(name)},\n`); if (c.propsScheme) { importZod = true; out.push(` propsScheme:z.object({\n`); out.push(` ${c.propsScheme.trim().replace(/\n\s*/g, '\n ')}\n`); out.push(` }),\n`); } out.push(` renderer:(comp:ConvoComponent,ctx:ConvoComponentRendererContext)=><${c.name} {...({comp,ctx,ctrl:ctx.ctrl,convo:ctx.ctrl.convo,...comp.atts} as any)} />,\n`); out.push(` },\n`); } out.push(' },\n'); } if (mod.externFunctions) { out.push(' externFunctions:{\n'); for (const name in mod.externFunctions) { const fn = mod.externFunctions[name]; if (!fn) { continue; } out.push(` ${name},\n`); } out.push(' },\n'); out.push(' functionParamSchemes:{\n'); for (const name in mod.externFunctions) { const fn = mod.externFunctions[name]; if (!fn) { continue; } out.push(` ${name}:[\n`); for (const a of fn.args) { importZod = true; out.push(` ${a.zodType},\n`); } out.push(` ],\n`); } out.push(' },\n'); } if (mod.typeSchemes) { out.push(' typeSchemes:{\n'); for (const name in mod.typeSchemes) { const typeS = mod.typeSchemes[name]; if (!typeS) { continue; } importZod = true; out.push(` ${name}:z.object({\n${typeS}\n }),\n`); } out.push(' },\n'); } if (mod.convo) { out.push(` convo:/*convo*/\`\n\n\n${mod.convo.replace(tickReg, '\\`')}\n \`,\n`); } out.push(` },\n\n\n`); } out.push('];'); if (importZod) { out.splice(importI, 0, 'import { z } from "zod";\n'); } const convoImportNames = Object.keys(convoLangImports); convoImportNames.sort(); out.unshift(`import { ${convoImportNames.join(', ')} } from "@convo-lang/convo-lang";\n`); return out.join(''); }; const tickReg = /`/g; const parseTags = (tags) => { const map = {}; if (!tags) { return map; } let count = 0; for (const tag of tags) { count++; map[tag.getName()] = tag.getText().map(t => t.text).join('\n'); } return map; }; const scanConvoFileAsync = async (fullPath, project) => { clearProject(project); const content = await readFileAsStringAsync(fullPath); let relPath = relative(project.fullPath, fullPath); const mod = { name: relPath, convo: content, hashSrc: strHashBase64(content ?? ''), tsImports: [], }; project.mods.push(mod); }; const scanFileAsync = async (file, project) => { clearProject(project); const exportMap = file.getExportedDeclarations(); Array.from(exportMap.values()).forEach((c) => { c.forEach((c) => { const tags = parseTags(c.getSymbol()?.getJsDocTags()); if (convoJsDocTags.convoComponent in tags) { const fn = file.getFunction(c.getSymbol()?.getName() ?? ''); if (fn) { const propsType = fn.getParameters()[0]?.getType(); let propScheme; if (propsType) { propScheme = convertType(propsType, fn, project, false, true); } const name = fn.getName() ?? ''; const desc = getDescription(fn.getSymbol()) ?? ''; project.comps.push({ name, propsType: getSymbol(propsType)?.getName() ?? '', description: desc, instructions: (tags[convoJsDocTags.convoComponent] || `Generates props for a component based on the following description:\n<description>${desc}</description>`), propsScheme: propScheme, importStatement: getImportStatement(name, fn, file, project), keepSource: convoJsDocTags.convoKeepSource in tags }); } } if (convoJsDocTags.convoType in tags) { const type = c.getType(); const typeScheme = convertType(type, file, project, false, true); const name = type.getSymbol()?.getName(); if (name && typeScheme) { project.typeSchemes[name] = typeScheme; } } if (convoJsDocTags.convoFn in tags) { convertFunction(c.getType(), file, project, tags); } }); }); if (isProjectEmpty(project)) { return; } const relPath = relative(project.fullPath, file.getFilePath()); const mod = { name: relPath, convo: '', hashSrc: relPath, tsImports: [], }; if (getObjKeyCount(project.typeSchemes)) { mod.typeSchemes = { ...project.typeSchemes }; } if (project.convoOut) { mod.convo = '> define\n\n' + project.convoOut.join(''); } if (project.comps.length) { mod.components = {}; for (const c of project.comps) { mod.components[c.name] = c; if (!mod.tsImports.includes(c.importStatement)) { mod.tsImports.push(c.importStatement); } } mod.convo += '\n\n' + project.comps.map(c => (`@transformComponent ${c.name} ${c.propsType}\n@transformDescription ${escapeConvoTagValue(c.description)}\n${c.keepSource ? '@transformKeepSource\n' : ''}> system\n${escapeConvo(c.instructions)}`)).join('\n'); } if (project.fns.length) { mod.externFunctions = {}; for (const fn of project.fns) { mod.externFunctions[fn.name] = fn; if (!mod.tsImports.includes(fn.importStatement)) { mod.tsImports.push(fn.importStatement); } } mod.convo += '\n\n' + project.fns.map(c => c.local ? null : `${c.description ? convoDescriptionToComment(c.description) + '\n' : ''}${`> extern ${c.name}(${c.args.length ? c.args.map(arg => `\n${arg.description ? convoDescriptionToComment(arg.description, ' ') : ''}\n ${arg.name}${arg.optional ? '?' : ''}:${arg.type}`).join('') + '\n)' : ')'}`}`).filter(f => f).join('\n\n'); } project.mods.push(mod); mod.tsImports.sort(); const fnKeys = Object.keys(mod.externFunctions ?? {}); fnKeys.sort(); const compKeys = Object.keys(mod.components ?? {}); compKeys.sort(); mod.hashSrc = strHashBase64(`${mod.hashSrc}\n${mod.convo}\ncomps\n${compKeys.join('\n')}\fns\n${fnKeys.join('\n')}\nimports\n${mod.tsImports.join('\n')}`); }; const getImportStatement = (name, fn, file, project) => { let relPath = file.getFilePath(); relPath = relative(project.outFullPath, relPath); const i = relPath.lastIndexOf('.'); if (i !== -1) { relPath = relPath.substring(0, i); } return `import ${fn.isDefaultExport() ? '' : '{ '}${name}${fn.isDefaultExport() ? '' : ' }'} from '${relPath}';`; }; const getSymbol = (type) => { const n = type?.getSymbol(); if (n && n.getName() !== '__type') { return n; } return type?.getAliasSymbol(); }; const convertFunction = (type, file, project, tags) => { const callSig = type.getCallSignatures()?.[0]; if (!callSig) { return; } let sym = getSymbol(type); if (!sym) { return; } let name = sym.getName(); let importStatement; if (name === '__function') { const dec = sym?.getDeclarations()?.[0]; if (!dec) { return; } const parent = dec.getParent(); if (!parent || parent.getKind() !== SyntaxKind.VariableDeclaration) { return; } name = parent.getName(); importStatement = getImportStatement(name, parent, file, project); sym = parent.getSymbol(); } if (!name || !sym) { return; } const fnDec = file.getFunction(name); if (fnDec && !importStatement) { importStatement = getImportStatement(name, fnDec, file, project); } if (!importStatement) { return; } const params = callSig.getParameters(); const description = getDescription(sym, params.map(p => p.getName())); const fn = { name, description, args: [], importStatement, local: convoJsDocTags.convoLocal in tags, }; for (const param of params) { const pTags = parseTags(param.getJsDocTags()); if (convoJsDocTags.convoIgnore in pTags) { continue; } const paramDec = param.getDeclarations()?.[0]; const declaredType = paramDec?.getType(); if (!declaredType || !paramDec) { continue; } const pType = unwrapType(declaredType, false); const { convo, zod } = convertTypeToStrings(pType.type, file, project, true); const pName = param.getName(); const pDescription = getDescription(paramDec.getSymbol()); const pTag = tags[pName]; const optional = param.isOptional() || pType.optional; fn.args.push({ name: pName, description: pDescription || pTag, type: convo, optional, zodType: zod + (optional ? '.optional()' : ''), }); } project.fns.push(fn); }; const convertTypeToStrings = (type, locationNode, project, noWrap) => { const tsI = project.tsOut.length; const convoI = project.convoOut.length; const zodI = project.zodOut.length; _convertType(type, locationNode, project, true, 0, 0, false); const ts = project.tsOut.splice(tsI, project.tsOut.length - tsI).join(); const convo = project.convoOut.splice(convoI, project.convoOut.length - convoI).join(); const zod = project.zodOut.splice(zodI, project.zodOut.length - zodI).join(); return { ts, convo, zod }; }; const convertType = (type, locationNode, project, noWrap, getZodType) => { const sym = getSymbol(type); const description = getDescription(sym); writeTsConvoDescription(description, project, ''); const typeName = sym?.getName(); const tsI = project.tsOut.length; const convoI = project.convoOut.length; const zodI = project.zodOut.length; const { noWrap: _noWrap } = _convertType(type, locationNode, project, true, 0, 0, false); if (_noWrap) { noWrap = true; } const zod = getZodType ? project.zodOut.slice(zodI, project.zodOut.length - zodI).join('') : ''; if (noWrap) { project.tsOut.splice(tsI, 0, `export type ${typeName}ConvoBinding=`); project.zodOut.splice(zodI, 0, `export const ${typeName}ConvoBindingScheme=`); project.convoOut.splice(convoI, 0, `${typeName}=`); } else { project.tsOut.splice(tsI, 0, `export interface ${typeName}ConvoBinding{\n`); project.zodOut.splice(zodI, 0, `export const ${typeName}ConvoBindingScheme=z.object({\n`); project.convoOut.splice(convoI, 0, `${typeName}=struct(\n`); } project.tsOut.push(`${noWrap ? ';' : '}'}\n\n`); project.zodOut.push(`${noWrap ? '' : '})'}${description ? `.describe(${JSON.stringify(description)})` : ''};\n\n`); project.convoOut.push(`${noWrap ? '' : ')'}\n\n`); return zod || undefined; }; let maxDepth = 10; const _convertType = (type, locationNode, project, isRoot, depth, indentLevel, allowOptional) => { const { type: unwrappedType, optional, isAny, unionValues, } = unwrapType(type, depth > maxDepth); type = unwrappedType; const typeName = getSymbol(type)?.getName(); if (typeName && ignoreTypes.includes(typeName)) { return { optional: false, ignored: true }; } const tab = ' '.repeat(indentLevel); let convoType; let tsType; let zType; let noWrap = true; if (isAny || (optional && !allowOptional)) { convoType = 'any'; } else if (typeName === 'Record') { project.tsOut.push(`Record<string,any>`); project.convoOut.push(`object`); project.zodOut.push(`z.record(z.string(),z.any())`); return { optional }; } else if (typeName && arrayTypeMap[typeName]) { const t = arrayTypeMap[typeName] ?? 'any'; project.tsOut.push(`${t}[]`); project.convoOut.push(`array(${t})`); project.zodOut.push(`z.${t}().array()`); return { optional }; } else if (type.isArray()) { project.tsOut.push('('); project.convoOut.push('array('); const elemType = type.getArrayElementTypeOrThrow(); _convertType(elemType, locationNode, project, false, depth + 1, indentLevel, false); project.tsOut.push(')[]'); project.convoOut.push(')'); project.zodOut.push('.array()'); return { optional }; } else if (unionValues) { noWrap = true; convoType = `enum(${unionValues.map(v => JSON.stringify(v)).join(' ')})`; tsType = unionValues.map(v => JSON.stringify(v)).join('|'); zType = unionValues.length === 1 ? `z.literal(${JSON.stringify(unionValues[0])})` : `z.union([${unionValues.map(v => `z.literal(${JSON.stringify(v)})`).join(',')}])`; } else if (type.isClass()) { return { optional: false, ignored: true }; } else if (type.isString()) { convoType = 'string'; } else if (type.isNumber() || type.isBigInt()) { convoType = 'number'; } else if (type.isBoolean()) { convoType = 'boolean'; } else if (type.isNull()) { convoType = 'null'; } else if (type.isUndefined()) { convoType = 'undefined'; } else if (type.isLiteral()) { const v = type.getLiteralValue(); convoType = `enum(${JSON.stringify(v)})`; tsType = JSON.stringify(v); zType = `z.literal(${JSON.stringify(v)})`; } else { noWrap = false; if (!isRoot) { project.tsOut.push(`{\n`); project.zodOut.push(`z.object({\n`); project.convoOut.push(`struct(\n`); } const props = type.getProperties(); for (const propSym of props) { const tags = parseTags(propSym.getJsDocTags()); if ('convoIgnore' in tags) { continue; } const propType = propSym.getTypeAtLocation(locationNode); let description = getDescription(propSym); const name = propSym.getName(); project.zodOut.push(`${tab} ${name}:`); let tsI = project.tsOut.length; let convoI = project.convoOut.length; const { optional: propOpt, description: propDesc, ignored } = _convertType(propType, locationNode, project, false, depth + 1, indentLevel + 1, true); if (ignored) { project.zodOut.pop(); continue; } if (!description && propDesc) { description = propDesc; } if (description) { writeTsConvoDescription(description, project, tab + ' ', tsI, convoI); tsI++; convoI++; } project.tsOut.splice(tsI, 0, `${tab} ${name}${propOpt ? '?' : ''}:`); project.convoOut.splice(convoI, 0, `${tab} ${name}${propOpt ? '?' : ''}:`); project.tsOut.push(`;\n`); project.zodOut.push(`${propOpt ? '.optional()' : ''}${description ? `.describe(${JSON.stringify(description)})` : ''},\n`); project.convoOut.push(`\n`); } if (!isRoot) { project.tsOut.push(`${tab}}`); project.zodOut.push(`${tab}})`); project.convoOut.push(`${tab})`); } return { optional, description: getDescription(getSymbol(type)) }; } if (tsType === undefined) { tsType = convoType; } if (zType === undefined) { zType = 'z.' + convoType + '()'; } project.tsOut.push(tsType); project.convoOut.push(convoType); project.zodOut.push(zType); return { optional, noWrap }; }; const newlineReg = /\n/g; const toJsDoc = (value, tab) => { return `${tab}/**\n${tab} * ${value.replace(newlineReg, `\n${tab} * `)}\n${tab} */\n`; }; const unwrapType = (type, forceAny) => { if (!type.isUnion()) { return { type, isAny: type.isAny() || forceAny, optional: false }; } const unionTypes = type.getUnionTypes(); let optional = false; let unwrappedType; const unionValues = []; for (let i = 0; i < unionTypes.length; i++) { const t = unionTypes[i]; if (!t) { continue; } if (t.isUndefined()) { optional = true; } else if (t.isBooleanLiteral()) { unwrappedType = type.getNonNullableType(); } else if (t.isLiteral()) { const v = t.getLiteralValue(); if ((typeof v === 'string') || (typeof v === 'number')) { unionValues.push(v); } } else { unwrappedType = t; } } const isAny = forceAny || (unwrappedType ?? type).isAny(); return { type: unwrappedType ?? type, optional, unionValues: unionValues.length == 0 ? undefined : unionValues, isAny: isAny || (unwrappedType && unionValues.length > 0 ? true : false), }; }; const jsDocTagReg = /^\s*@(\w+)(.*)/; const commentReplace = /(^|\n|\r)\s*\/?\*+\/?[ \t]*/g; const descriptionLines = []; const getDescription = (sym, ignoreTags) => { if (!sym) { return undefined; } descriptionLines.splice(0, descriptionLines.length); const decs = sym.getDeclarations(); for (const d of decs) { let comments = d.getLeadingCommentRanges(); if (!comments.length && d.getKind() === SyntaxKind.VariableDeclaration) { const statement = d.getParent()?.getParent(); if (statement?.getKind() === SyntaxKind.VariableStatement) { comments = statement.getLeadingCommentRanges(); } } if (comments.length) { for (const c of comments) { const txt = c.getText().replace(commentReplace, '\n').trim(); if (txt.includes('\n')) { const split = txt.split('\n'); for (const s of split) { descriptionLines.push(s); } } else { descriptionLines.push(txt); } } } } ; let ignoreLine = false; for (let i = 0; i < descriptionLines.length; i++) { const match = jsDocTagReg.exec(descriptionLines[i]); const tagName = match?.[1]; const isConvoTag = tagName?.startsWith('convo'); const shouldIgnore = tagName && (isConvoTag || ignoreTags?.includes(tagName)); if (ignoreLine) { if (!tagName || shouldIgnore) { descriptionLines.splice(i, 1); i--; } else if (tagName) { ignoreLine = false; } } else { if (shouldIgnore) { ignoreLine = true; descriptionLines.splice(i, 1); i--; } } if (isConvoTag && !ignoreLine && match) { descriptionLines[i] = match[2] ?? ''; } } return descriptionLines.join('\n').trim() || undefined; }; const writeTsConvoDescription = (description, project, tab, tsIndex = project.tsOut.length, convoIndex = project.convoOut.length) => { if (!description) { return; } project.convoOut.splice(convoIndex, 0, convoDescriptionToComment(description, tab) + '\n'); project.tsOut.splice(tsIndex, 0, toJsDoc(description, tab)); }; const queueDelay = 300; const startDelay = 2000; const queueIvs = {}; const defaultIgnore = ['node_modules', '__pycache__', 'venv', /^\./]; const extReg = /\.(ts|tsx|convo)$/i; const watchProjectAsync = async (project) => { console.log(`Watching ${project.fullPath} for changes`); const start = Date.now(); const ac = new AbortController(); const watcher = watch(project.fullPath, { signal: ac.signal, recursive: true }); project.cancel.subscribe(() => { ac.abort(); }); try { const queueProjectScan = () => { clearTimeout(queueIvs[project.fullPath]); queueIvs[project.fullPath] = setTimeout(() => { if (project.cancel.isCanceled) { return; } scanProjectAsync(project, true); }, queueDelay); }; watchLoop: for await (const evt of watcher) { if (Date.now() - start < startDelay || !extReg.test(evt.filename)) { continue; } const parts = evt.filename.split(/[/\\]/); for (const n of parts) { for (const i of defaultIgnore) { if ((typeof i === 'string') ? i === n : i.test(n)) { continue watchLoop; } } } const fullPath = joinPaths(project.fullPath, evt.filename); if (fullPath.startsWith(project.outFullPath)) { continue; } let file = project.project.getSourceFile(fullPath); const exists = await pathExistsAsync(fullPath); const refresh = (exists && !file) || evt.eventType === 'rename'; if (!file && !refresh) { continue; } if (project.cancel.isCanceled) { return; } if (refresh) { project.refreshRequested = true; } else { if (!project.reloadPaths.includes(fullPath)) { project.reloadPaths.push(fullPath); } } queueProjectScan(); } } catch (ex) { if (!project.cancel.isCanceled) { console.error(`watchProjectAsync error`, project.outFullPath, ex); } } }; //# sourceMappingURL=convo-interface-converter.js.map