UNPKG

@zern/tsls

Version:

TypeScript Language Service plugin for Zern Kernel virtual type augmentations

120 lines (119 loc) 4.49 kB
"use strict"; const DEFAULT_GLOBS = ['**/*.plugin.ts', 'plugins/**/specs/*.ts', 'src/**/specs/*.{ts,tsx}']; const VIRTUAL_FILE = '__zern_virtual_augmentations.d.ts'; function escapeKey(key) { return key.replace(/[^A-Za-z0-9_]/g, '_'); } function buildAugmentationSource(nsToKeys) { const blocks = []; const toRecord = (keys) => keys .map(k => ` ${escapeKey(k)}: { __type: '${k.includes('.') ? 'event-def' : 'hook-def'}' }`) .join('\n'); const evBody = Object.entries(nsToKeys.events) .map(([ns, keys]) => ` ${ns}: {\n${toRecord(keys)}\n }`) .join('\n'); const alBody = Object.entries(nsToKeys.alerts) .map(([ns, keys]) => ` ${ns}: {\n${toRecord(keys)}\n }`) .join('\n'); const hkBody = Object.entries(nsToKeys.hooks) .map(([ns, keys]) => ` ${ns}: {\n${toRecord(keys)}\n }`) .join('\n'); if (evBody) { blocks.push(`declare module '@events/types' {\n interface ZernEvents {\n${evBody}\n }\n}`); } if (alBody) { blocks.push(`declare module '@alerts/types' {\n interface ZernAlerts {\n${alBody}\n }\n}`); } if (hkBody) { blocks.push(`declare module '@hooks/types' {\n interface ZernHooks {\n${hkBody}\n }\n}`); } return blocks.join('\n\n'); } function collectSpecs(sys, basePath, globs) { const ns = { events: {}, alerts: {}, hooks: {}, }; const files = globs .flatMap(g => sys.readDirectory?.(basePath, ['.ts', '.tsx'], [g], undefined) ?? []) .filter((p, idx, arr) => arr.indexOf(p) === idx); const read = (p) => { try { return sys.readFile(p) ?? ''; } catch { return ''; } }; const evRe = /createEvents\(\s*['"]([A-Za-z0-9_.-]+)['"]\s*,\s*\{([\s\S]*?)\}\s*\)/g; const alRe = /createAlerts\(\s*['"]([A-Za-z0-9_.-]+)['"]\s*,\s*\{([\s\S]*?)\}\s*\)/g; const hkRe = /createHooks\(\s*['"]([A-Za-z0-9_.-]+)['"]\s*,\s*\{([\s\S]*?)\}\s*\)/g; const keyRe = /([A-Za-z0-9_]+)\s*:/g; for (const f of files) { const src = read(f); let m; while ((m = evRe.exec(src))) { const nsName = m[1]; const body = m[2]; const keys = []; let km; while ((km = keyRe.exec(body))) keys.push(km[1]); ns.events[nsName] = [...(ns.events[nsName] ?? []), ...keys]; } while ((m = alRe.exec(src))) { const nsName = m[1]; const body = m[2]; const keys = []; let km; while ((km = keyRe.exec(body))) keys.push(km[1]); ns.alerts[nsName] = [...(ns.alerts[nsName] ?? []), ...keys]; } while ((m = hkRe.exec(src))) { const nsName = m[1]; const body = m[2]; const keys = []; let km; while ((km = keyRe.exec(body))) keys.push(km[1]); ns.hooks[nsName] = [...(ns.hooks[nsName] ?? []), ...keys]; } } return ns; } function init(_modules) { function create(info) { const options = info.config ?? {}; const globs = options.pluginGlobs && options.pluginGlobs.length > 0 ? options.pluginGlobs : DEFAULT_GLOBS; const cwd = info.project.getCurrentDirectory(); let lastContent = ''; const sys = info.languageServiceHost; const refresh = () => { const nsToKeys = collectSpecs(sys, cwd, globs); lastContent = buildAugmentationSource(nsToKeys); }; refresh(); const origGetExternalFiles = info.languageServiceHost.getExternalFiles?.bind(info.languageServiceHost); info.languageServiceHost.getExternalFiles = (_proj) => { const base = origGetExternalFiles ? (origGetExternalFiles(_proj) ?? []) : []; return [...base, VIRTUAL_FILE]; }; const origReadFile = sys.readFile.bind(sys); sys.readFile = (path, encoding) => { if (path.endsWith(VIRTUAL_FILE)) return lastContent; return origReadFile(path, encoding); }; const origFileExists = sys.fileExists.bind(sys); sys.fileExists = (path) => { if (path.endsWith(VIRTUAL_FILE)) return true; return origFileExists(path); }; return info.languageService; } return { create }; } module.exports = init;