mirror of
https://github.com/robonen/questlang.git
synced 2026-03-20 02:44:47 +00:00
feat: enhance module system with host interface for file operations
This commit is contained in:
@@ -54,7 +54,11 @@ describe('module system', () => {
|
|||||||
`;
|
`;
|
||||||
fs.writeFileSync(questPath, quest);
|
fs.writeFileSync(questPath, quest);
|
||||||
|
|
||||||
const interpreter = QuestLang.interpret(quest, questPath);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const interpreter = QuestLang.interpret(quest, questPath, host);
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
expect(validation.isValid).toBe(true);
|
expect(validation.isValid).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -100,7 +104,11 @@ describe('module system', () => {
|
|||||||
конец;
|
конец;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const interpreter = QuestLang.interpret(fs.readFileSync(qPath, 'utf8'), qPath);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const interpreter = QuestLang.interpret(fs.readFileSync(qPath, 'utf8'), qPath, host);
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
expect(validation.isValid).toBe(true);
|
expect(validation.isValid).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -133,7 +141,11 @@ describe('module system', () => {
|
|||||||
`;
|
`;
|
||||||
fs.writeFileSync(questPath, quest);
|
fs.writeFileSync(questPath, quest);
|
||||||
|
|
||||||
const interpreter = QuestLang.interpret(quest, questPath);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const interpreter = QuestLang.interpret(quest, questPath, host);
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
expect(validation.isValid).toBe(false);
|
expect(validation.isValid).toBe(false);
|
||||||
expect(validation.errors.some(e => e.includes('non-existent') || e.includes('not exported'))).toBe(true);
|
expect(validation.errors.some(e => e.includes('non-existent') || e.includes('not exported'))).toBe(true);
|
||||||
|
|||||||
19
src/cli.ts
19
src/cli.ts
@@ -3,6 +3,7 @@
|
|||||||
import type { QuestInterpreter } from '.';
|
import type { QuestInterpreter } from '.';
|
||||||
import type { InitialNode, NodeDefinition } from './ast.js';
|
import type { InitialNode, NodeDefinition } from './ast.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import * as p from '@clack/prompts';
|
import * as p from '@clack/prompts';
|
||||||
import { QuestLang } from '.';
|
import { QuestLang } from '.';
|
||||||
@@ -122,7 +123,11 @@ class ClackCLI {
|
|||||||
spinner.start('Loading quest...');
|
spinner.start('Loading quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const interpreter = QuestLang.interpret(source, filename);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const interpreter = QuestLang.interpret(source, filename, host);
|
||||||
|
|
||||||
// Validate first
|
// Validate first
|
||||||
const validation = interpreter.validate();
|
const validation = interpreter.validate();
|
||||||
@@ -237,7 +242,11 @@ class ClackCLI {
|
|||||||
spinner.start('Validating quest...');
|
spinner.start('Validating quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const validation = QuestLang.validate(source, filename);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const validation = QuestLang.validate(source, filename, host);
|
||||||
|
|
||||||
if (validation.isValid) {
|
if (validation.isValid) {
|
||||||
spinner.stop('✅ Validation completed');
|
spinner.stop('✅ Validation completed');
|
||||||
@@ -268,7 +277,11 @@ class ClackCLI {
|
|||||||
spinner.start('Analyzing quest...');
|
spinner.start('Analyzing quest...');
|
||||||
|
|
||||||
const source = this.readFile(filename);
|
const source = this.readFile(filename);
|
||||||
const interpreter = QuestLang.interpret(source, filename);
|
const host = {
|
||||||
|
readFile: (file: string) => fs.readFileSync(file, 'utf8'),
|
||||||
|
resolve: (fromFile: string, spec: string) => path.resolve(path.dirname(fromFile), spec),
|
||||||
|
} as const;
|
||||||
|
const interpreter = QuestLang.interpret(source, filename, host);
|
||||||
const questInfo = interpreter.getQuestInfo();
|
const questInfo = interpreter.getQuestInfo();
|
||||||
|
|
||||||
spinner.stop('✅ Analysis completed');
|
spinner.stop('✅ Analysis completed');
|
||||||
|
|||||||
10
src/index.ts
10
src/index.ts
@@ -1,5 +1,6 @@
|
|||||||
import type { QuestProgram } from './ast';
|
import type { QuestProgram } from './ast';
|
||||||
import { QuestInterpreter } from './interpreter';
|
import { QuestInterpreter } from './interpreter';
|
||||||
|
import type { ModuleHost } from './module-loader';
|
||||||
import { Lexer } from './lexer';
|
import { Lexer } from './lexer';
|
||||||
import { Parser } from './parser';
|
import { Parser } from './parser';
|
||||||
|
|
||||||
@@ -21,17 +22,17 @@ export class QuestLang {
|
|||||||
/**
|
/**
|
||||||
* Create interpreter from source code
|
* Create interpreter from source code
|
||||||
*/
|
*/
|
||||||
public static interpret(source: string, filePath?: string): QuestInterpreter {
|
public static interpret(source: string, filePath?: string, host?: ModuleHost): QuestInterpreter {
|
||||||
const ast = this.parse(source);
|
const ast = this.parse(source);
|
||||||
return new QuestInterpreter(ast, filePath);
|
return new QuestInterpreter(ast, filePath, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate QuestLang source code
|
* Validate QuestLang source code
|
||||||
*/
|
*/
|
||||||
public static validate(source: string, filePath?: string): { isValid: boolean; errors: string[] } {
|
public static validate(source: string, filePath?: string, host?: ModuleHost): { isValid: boolean; errors: string[] } {
|
||||||
try {
|
try {
|
||||||
const interpreter = this.interpret(source, filePath);
|
const interpreter = this.interpret(source, filePath, host);
|
||||||
return interpreter.validate();
|
return interpreter.validate();
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -45,5 +46,6 @@ export class QuestLang {
|
|||||||
|
|
||||||
export * from './ast';
|
export * from './ast';
|
||||||
export { QuestInterpreter } from './interpreter';
|
export { QuestInterpreter } from './interpreter';
|
||||||
|
export type { ModuleHost } from './module-loader';
|
||||||
export { Lexer } from './lexer';
|
export { Lexer } from './lexer';
|
||||||
export { Parser } from './parser';
|
export { Parser } from './parser';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import type {
|
|||||||
OptionChoice,
|
OptionChoice,
|
||||||
QuestProgram,
|
QuestProgram,
|
||||||
} from './ast';
|
} from './ast';
|
||||||
import path from 'node:path';
|
|
||||||
import { ModuleLoader } from './module-loader';
|
import { ModuleLoader } from './module-loader';
|
||||||
|
import type { ModuleHost } from './module-loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime state of the quest
|
* Runtime state of the quest
|
||||||
@@ -45,7 +45,7 @@ export class QuestInterpreter {
|
|||||||
private currentState: QuestState;
|
private currentState: QuestState;
|
||||||
private moduleLoader?: ModuleLoader;
|
private moduleLoader?: ModuleLoader;
|
||||||
|
|
||||||
constructor(program: QuestProgram, questFilePath?: string) {
|
constructor(program: QuestProgram, questFilePath?: string, host?: ModuleHost) {
|
||||||
this.program = program;
|
this.program = program;
|
||||||
this.currentState = {
|
this.currentState = {
|
||||||
currentNode: program.graph.start,
|
currentNode: program.graph.start,
|
||||||
@@ -54,8 +54,8 @@ export class QuestInterpreter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Initialize module loader if imports present and quest file path provided
|
// Initialize module loader if imports present and quest file path provided
|
||||||
if (questFilePath && program.imports && program.imports.length > 0) {
|
if (questFilePath && host && program.imports && program.imports.length > 0) {
|
||||||
this.moduleLoader = new ModuleLoader(path.dirname(questFilePath));
|
this.moduleLoader = new ModuleLoader(host);
|
||||||
// Load modules
|
// Load modules
|
||||||
this.moduleLoader.loadQuest(questFilePath);
|
this.moduleLoader.loadQuest(questFilePath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { ImportNode, ModuleNode, QuestProgram } from './ast';
|
import type { ImportNode, ModuleNode, QuestProgram } from './ast';
|
||||||
import fs from 'node:fs';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { Lexer } from './lexer';
|
import { Lexer } from './lexer';
|
||||||
import { Parser } from './parser';
|
import { Parser } from './parser';
|
||||||
|
|
||||||
|
export interface ModuleHost {
|
||||||
|
readFile(file: string): string;
|
||||||
|
resolve(fromFile: string, specifier: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
export enum VisitState {
|
export enum VisitState {
|
||||||
Unvisited,
|
Unvisited,
|
||||||
Visiting,
|
Visiting,
|
||||||
@@ -23,17 +26,17 @@ export class ModuleLoader {
|
|||||||
private byFile = new Map<string, LoadedModule>();
|
private byFile = new Map<string, LoadedModule>();
|
||||||
private visit = new Map<string, VisitState>();
|
private visit = new Map<string, VisitState>();
|
||||||
|
|
||||||
constructor(private baseDir: string) {}
|
constructor(private host: ModuleHost) {}
|
||||||
|
|
||||||
public loadQuest(questFile: string): { program: QuestProgram; modules: LoadedModule[] } {
|
public loadQuest(questFile: string): { program: QuestProgram; modules: LoadedModule[] } {
|
||||||
const source = fs.readFileSync(questFile, 'utf8');
|
const source = this.host.readFile(questFile);
|
||||||
const program = this.parseQuest(source);
|
const program = this.parseQuest(source);
|
||||||
|
|
||||||
// Parse imports (if any)
|
// Parse imports (if any)
|
||||||
const imports: ImportNode[] = program.imports || [];
|
const imports: ImportNode[] = program.imports || [];
|
||||||
|
|
||||||
for (const imp of imports) {
|
for (const imp of imports) {
|
||||||
const abs = path.resolve(path.dirname(questFile), imp.modulePath);
|
const abs = this.host.resolve(questFile, imp.modulePath);
|
||||||
this.dfsParse(abs);
|
this.dfsParse(abs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,14 +55,14 @@ export class ModuleLoader {
|
|||||||
this.visit.set(file, VisitState.Visiting);
|
this.visit.set(file, VisitState.Visiting);
|
||||||
|
|
||||||
if (!this.byFile.has(file)) {
|
if (!this.byFile.has(file)) {
|
||||||
const src = fs.readFileSync(file, 'utf8');
|
const src = this.host.readFile(file);
|
||||||
const ast = this.parseModule(src, file);
|
const ast = this.parseModule(src, file);
|
||||||
this.byFile.set(file, { name: ast.name, file, ast });
|
this.byFile.set(file, { name: ast.name, file, ast });
|
||||||
|
|
||||||
// Follow module's own imports (if any)
|
// Follow module's own imports (if any)
|
||||||
const ownImports = ast.imports || [];
|
const ownImports = ast.imports || [];
|
||||||
for (const imp of ownImports) {
|
for (const imp of ownImports) {
|
||||||
const dep = path.resolve(path.dirname(file), imp.modulePath);
|
const dep = this.host.resolve(file, imp.modulePath);
|
||||||
this.dfsParse(dep);
|
this.dfsParse(dep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user