@zern/tsls
Version:
TypeScript Language Service plugin for Zern Kernel virtual type augmentations
120 lines (119 loc) • 4.49 kB
JavaScript
;
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;