@uiwwsw/hangulscript 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import ts from 'typescript';
2
+
3
+ const hints = {
4
+ 2304: '이름을 먼저 선언했는지 확인하세요.',
5
+ 2307: '모듈 경로와 파일이 있는지 확인하세요.',
6
+ 2322: '선언한 타입과 대입한 값의 타입을 확인하세요.',
7
+ 2339: '이 값에 해당 속성이나 메서드가 있는지 확인하세요.',
8
+ 2345: '함수에 전달한 값의 타입을 확인하세요.',
9
+ 2554: '함수에 전달하는 인자의 개수를 확인하세요.',
10
+ 7006: '공개 함수의 매개변수 타입을 명시하세요.'
11
+ };
12
+
13
+ export class HangulScriptError extends Error {
14
+ constructor(diagnostics) {
15
+ super(formatSourceDiagnostics(diagnostics));
16
+ this.name = 'HangulScriptError';
17
+ this.diagnostics = diagnostics;
18
+ }
19
+ }
20
+
21
+ export function syntaxDiagnostic(error, source, filePath) {
22
+ const lines = source.split(/\r?\n/);
23
+ const match = error.message.match(/^(\d+)행: /);
24
+ const line = match ? Number(match[1]) : Math.max(1, lines.findLastIndex(line => line.trim()) + 1);
25
+ const text = lines[line - 1] ?? '';
26
+ return { filePath, line, column: text.length - text.trimStart().length + 1, length: Math.max(1, text.trim().length), code: 'HGS1001', message: error.message.replace(/^\d+행: /, ''), severity: 'error' };
27
+ }
28
+
29
+ export function mapDiagnostic(diagnostic, documents) {
30
+ const file = diagnostic.file;
31
+ const position = file && diagnostic.start !== undefined ? file.getLineAndCharacterOfPosition(diagnostic.start) : { line: 0, character: 0 };
32
+ const document = file && documents.get(file.fileName);
33
+ let line = position.line + 1;
34
+ let column = position.character + 1;
35
+ let length = diagnostic.length ?? 1;
36
+ if (document) {
37
+ line = document.lineMap[position.line] || 1;
38
+ const original = document.source.split(/\r?\n/)[line - 1] ?? '';
39
+ const token = file.text.slice(diagnostic.start, diagnostic.start + length);
40
+ const generatedLine = file.text.split('\n')[position.line] ?? '';
41
+ const occurrence = token ? generatedLine.slice(0, position.character).split(token).length - 1 : 0;
42
+ let found = -1;
43
+ for (let i = 0; i <= occurrence; i++) {
44
+ const next = original.indexOf(token, found + 1);
45
+ if (next < 0) break;
46
+ found = next;
47
+ }
48
+ column = found >= 0 ? found + 1 : original.length - original.trimStart().length + 1;
49
+ length = Math.max(1, Math.min(length, original.length - column + 1));
50
+ }
51
+ const detail = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
52
+ return {
53
+ filePath: document?.filePath ?? file?.fileName ?? 'input.hgs', line, column, length,
54
+ code: diagnostic.code, message: hints[diagnostic.code] ? `${hints[diagnostic.code]}\n${detail}` : detail,
55
+ severity: diagnostic.category === ts.DiagnosticCategory.Error ? 'error' : 'warning'
56
+ };
57
+ }
58
+
59
+ export function formatSourceDiagnostics(diagnostics) {
60
+ return diagnostics.map(d => `${d.filePath}:${d.line}:${d.column} [${typeof d.code === 'number' ? 'TS' : ''}${d.code}] ${d.message}`).join('\n');
61
+ }
@@ -0,0 +1,5 @@
1
+ import type { CheckSettings } from './index.js';
2
+ export interface EditorLocation { filePath: string; line: number; column: number; length: number; }
3
+ export interface EditorInspection { hover?: string; definitions: EditorLocation[]; completions: { name: string; detail: string }[]; }
4
+ /** One-based UTF-16 positions. Does not execute source. */
5
+ export function inspectSource(source: string, filePath: string, line: number, column: number, settings?: CheckSettings): EditorInspection;
package/dist/editor.js ADDED
@@ -0,0 +1,50 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ import { analyzeSource } from './checker.js';
4
+ import { mapDiagnostic } from './diagnostics.js';
5
+
6
+ /** One-based source positions; analysis only, never executes user code. */
7
+ export function inspectSource(source, filePath, line, column, settings = {}) {
8
+ const analysis = analyzeSource(source, filePath, settings);
9
+ const file = analysis.program.getSourceFile(analysis.virtualRoot);
10
+ const document = analysis.documents.get(analysis.virtualRoot);
11
+ if (!file || !document) return { completions: [], definitions: [] };
12
+ let target = file;
13
+ let matched;
14
+ const original = source.split(/\r?\n/)[line - 1] ?? '';
15
+ const visit = node => {
16
+ if (ts.isIdentifier(node)) {
17
+ const pos = file.getLineAndCharacterOfPosition(node.getStart(file));
18
+ if (document.lineMap[pos.line] === line) {
19
+ target = node;
20
+ const generatedLine = file.text.split('\n')[pos.line];
21
+ const occurrence = generatedLine.slice(0, pos.character).split(node.text).length - 1;
22
+ let found = -1;
23
+ for (let i=0; i<=occurrence; i++) { const next=original.indexOf(node.text,found+1); if(next<0) break; found=next; }
24
+ if (found >= 0 && column - 1 >= found && column - 1 <= found + node.text.length) matched = node;
25
+ }
26
+ }
27
+ ts.forEachChild(node,visit);
28
+ };
29
+ visit(file);
30
+ const checker = analysis.program.getTypeChecker();
31
+ target = matched ?? target;
32
+ const completions = checker.getSymbolsInScope(target, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Alias)
33
+ .filter(symbol => symbol.declarations?.some(d => analysis.documents.has(d.getSourceFile().fileName) && !['화면연결'].includes(symbol.name)))
34
+ .map(symbol => ({ name: symbol.name, detail: checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,target)) }));
35
+ if (!matched) return { completions, definitions: [] };
36
+ let symbol = checker.getSymbolAtLocation(matched);
37
+ if (!symbol) return { completions, definitions: [] };
38
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
39
+ const type = checker.typeToString(checker.getTypeOfSymbolAtLocation(symbol,matched),matched,ts.TypeFormatFlags.NoTruncation);
40
+ const definitions = (symbol.declarations ?? []).flatMap(declaration => {
41
+ const name = declaration.name ?? declaration;
42
+ const sourceFile=name.getSourceFile();
43
+ const doc=analysis.documents.get(sourceFile.fileName);
44
+ const position=sourceFile.getLineAndCharacterOfPosition(name.getStart(sourceFile));
45
+ if (!doc || !doc.lineMap[position.line]) return [];
46
+ const location=mapDiagnostic({file:sourceFile,start:name.getStart(sourceFile),length:name.getWidth(sourceFile),code:0,messageText:'',category:ts.DiagnosticCategory.Message},analysis.documents);
47
+ return [{filePath:path.resolve(location.filePath),line:location.line,column:location.column,length:location.length}];
48
+ });
49
+ return { hover: `${symbol.name}: ${type}`, completions, definitions };
50
+ }
@@ -0,0 +1,39 @@
1
+ import ts from 'typescript';
2
+ import path from 'node:path';
3
+ import { analyzeSource } from './checker.js';
4
+ import { HangulScriptError, mapDiagnostic } from './diagnostics.js';
5
+
6
+ // Keep JavaScript and declaration imports consistent with emitted filenames.
7
+ function rewriteImports(context) {
8
+ const visit = node => {
9
+ if (ts.isStringLiteral(node) && node.parent && node.text.endsWith('.hgs') &&
10
+ ((ts.isImportDeclaration(node.parent) || ts.isExportDeclaration(node.parent)) && node.parent.moduleSpecifier === node ||
11
+ ts.isCallExpression(node.parent) && node.parent.expression.kind === ts.SyntaxKind.ImportKeyword && node.parent.arguments[0] === node)) {
12
+ return context.factory.createStringLiteral(node.text.replace(/\.hgs$/, '.js'));
13
+ }
14
+ // afterDeclarations nodes do not always carry parent links.
15
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.endsWith('.hgs')) {
16
+ return context.factory.updateImportDeclaration(node, node.modifiers, node.importClause, context.factory.createStringLiteral(node.moduleSpecifier.text.replace(/\.hgs$/, '.js')), node.attributes);
17
+ }
18
+ if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.endsWith('.hgs')) {
19
+ return context.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, context.factory.createStringLiteral(node.moduleSpecifier.text.replace(/\.hgs$/, '.js')), node.attributes);
20
+ }
21
+ return ts.visitEachChild(node, visit, context);
22
+ };
23
+ return node => ts.visitNode(node, visit);
24
+ }
25
+
26
+ export function emitSource(source, filePath) {
27
+ const result = analyzeSource(source, filePath);
28
+ if (result.hasErrors) throw new HangulScriptError(result.diagnostics);
29
+ const files = new Map();
30
+ const emitted = result.program.emit(undefined, (name, content) => files.set(name, content), undefined, undefined, { before: [rewriteImports], afterDeclarations: [rewriteImports] });
31
+ if (emitted.emitSkipped) throw new HangulScriptError(emitted.diagnostics.map(d => mapDiagnostic(d, result.documents)));
32
+ const modules = [...result.documents].map(([virtual, doc]) => ({
33
+ sourcePath: doc.filePath,
34
+ javascript: files.get(virtual.replace(/\.ts$/, '.js')) ?? '',
35
+ declaration: files.get(virtual.replace(/\.ts$/, '.d.ts')) ?? ''
36
+ }));
37
+ const root = modules.find(module => module.sourcePath === path.resolve(filePath));
38
+ return { javascript: root.javascript, declaration: root.declaration, modules };
39
+ }
@@ -0,0 +1,59 @@
1
+ import ts from 'typescript';
2
+
3
+ const literals = new Map([['참', 'true'], ['거짓', 'false'], ['비어있음', 'null'], ['정의안됨', 'undefined'], ['자신', 'this'], ['부모', 'super']]);
4
+ const types = new Map([['숫자', 'number'], ['문자열', 'string'], ['논리', 'boolean'], ['불리언', 'boolean'], ['아무거나', 'any'], ['모름', 'unknown'], ['무효', 'void']]);
5
+
6
+ function identifiers(source, replacements) {
7
+ const file = ts.createSourceFile('expression.ts', source, ts.ScriptTarget.Latest, true);
8
+ const edits = [];
9
+ const visit = node => {
10
+ if (ts.isIdentifier(node) && replacements.has(node.text)) {
11
+ const p = node.parent;
12
+ const property = (ts.isPropertyAccessExpression(p) && p.name === node) ||
13
+ ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertySignature(p)) && p.name === node);
14
+ if (!property) edits.push([node.getStart(file), node.end, replacements.get(node.text)]);
15
+ }
16
+ ts.forEachChild(node, visit);
17
+ };
18
+ visit(file);
19
+ for (const [start, end, value] of edits.reverse()) source = source.slice(0, start) + value + source.slice(end);
20
+ return source;
21
+ }
22
+
23
+ export function expression(source) {
24
+ source = source.replace(/^새\s+/, 'new ').replace(/^기다리기\s+/, 'await ');
25
+ if (/[참거비정자부]/.test(source)) {
26
+ const prefix = 'const __expression = (';
27
+ source = identifiers(prefix + source + ');', literals).slice(prefix.length, -2);
28
+ }
29
+ return source;
30
+ }
31
+
32
+ export function typeExpression(source) {
33
+ const prefix = 'type __Type = ';
34
+ return identifiers(prefix + source, types).slice(prefix.length);
35
+ }
36
+
37
+ export function signature(source, name = '__fn') {
38
+ const file = ts.createSourceFile('signature.ts', `function ${name}${source} {}`, ts.ScriptTarget.Latest, true);
39
+ const fn = file.statements[0];
40
+ if (file.parseDiagnostics.length || !ts.isFunctionDeclaration(fn)) throw new SyntaxError('함수 이름·매개변수·타입을 확인하세요.');
41
+ return fn.parameters.map(param => {
42
+ let printed = param.getText(file);
43
+ const start = param.getStart(file);
44
+ const edits = [];
45
+ if (param.type) edits.push([param.type.getStart(file) - start, param.type.end - start, typeExpression(param.type.getText(file))]);
46
+ if (param.initializer) edits.push([param.initializer.getStart(file) - start, param.initializer.end - start, expression(param.initializer.getText(file))]);
47
+ for (const [from, to, value] of edits.sort((a, b) => b[0] - a[0])) printed = printed.slice(0, from) + value + printed.slice(to);
48
+ if (!param.type && !param.initializer) printed += param.dotDotDotToken ? ': any[]' : ': any';
49
+ return printed;
50
+ }).join(', ');
51
+ }
52
+
53
+ export function statement(source) {
54
+ const file = ts.createSourceFile('statement.ts', source, ts.ScriptTarget.Latest, true);
55
+ if (file.parseDiagnostics.length || file.statements.length !== 1 || !ts.isExpressionStatement(file.statements[0])) {
56
+ throw new SyntaxError('문장형 구문을 사용하세요. 선언은 「이름은 값이다」, 함수는 「이름(...)를 정의한다:」로 작성합니다.');
57
+ }
58
+ return source.replace(/;$/, '') + ';';
59
+ }
@@ -0,0 +1,2 @@
1
+ /** Preserve expression/literal contents while normalizing indentation and whitespace. */
2
+ export function formatSource(source: string, options?: { tabSize?: number }): string;
@@ -0,0 +1,29 @@
1
+ import { compileSource } from './transformer.js';
2
+
3
+ /** Normalize indentation and whitespace without rewriting expressions or literals. */
4
+ export function formatSource(source, { tabSize = 4 } = {}) {
5
+ if (!Number.isInteger(tabSize) || tabSize < 1 || tabSize > 8) throw new RangeError('들여쓰기 폭은 1~8 사이의 정수여야 합니다.');
6
+ const eol = source.includes('\r\n') ? '\r\n' : '\n';
7
+ const expanded = source.replace(/^[ \t]*\t[ \t]*/gm, prefix => {
8
+ let width = 0;
9
+ for (const c of prefix) width += c === '\t' ? tabSize - width % tabSize : 1;
10
+ return ' '.repeat(width);
11
+ });
12
+ compileSource(expanded); // Refuse to guess the intended structure of invalid code.
13
+ const levels = [0];
14
+ const output = [];
15
+ for (const raw of expanded.split(/\r?\n/)) {
16
+ const text = raw.trim();
17
+ if (!text) { if (output.length && output.at(-1) !== '') output.push(''); continue; }
18
+ const indent = raw.length - raw.trimStart().length;
19
+ if (text.startsWith('#')) {
20
+ const depth = levels.filter(level => level < indent).length;
21
+ output.push(' '.repeat(depth * tabSize) + text); continue;
22
+ }
23
+ while (levels.at(-1) > indent) levels.pop();
24
+ if (indent > levels.at(-1)) levels.push(indent);
25
+ output.push(' '.repeat((levels.length - 1) * tabSize) + raw.trimStart().trimEnd());
26
+ }
27
+ while (output.at(-1) === '') output.pop();
28
+ return output.length ? output.join(eol) + eol : '';
29
+ }
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export function compileFile(inputPath: string, outputPath: string): Promise<stri
5
5
  export function compileFileToDefaultDestination(inputPath: string): Promise<string>;
6
6
  export interface LintResult {
7
7
  diagnostics: readonly ts.Diagnostic[];
8
+ sourceDiagnostics: readonly SourceDiagnostic[];
8
9
  formattedDiagnostics: string;
9
10
  hasErrors: boolean;
10
11
  }
@@ -13,3 +14,11 @@ export function lintFile(inputPath: string, compilerOptions?: ts.CompilerOptions
13
14
  export interface BuildResult { javascript: string; declaration: string; }
14
15
  export function buildSource(source: string, filePath?: string): BuildResult;
15
16
  export function buildFile(inputPath: string, outputDirectory?: string): Promise<BuildResult & { javascriptPath: string; declarationPath: string }>;
17
+
18
+ export interface SourceDiagnostic { filePath: string; line: number; column: number; length: number; code: number | string; message: string; severity: 'error' | 'warning'; }
19
+ export interface CheckSettings { files?: ReadonlyMap<string, string>; allFiles?: boolean; libDirectory?: string; compilerOptions?: ts.CompilerOptions; }
20
+ export function checkSource(source: string, filePath?: string, settings?: CheckSettings): { diagnostics: SourceDiagnostic[]; hasErrors: boolean };
21
+ export function formatSourceDiagnostics(diagnostics: readonly SourceDiagnostic[]): string;
22
+ export class HangulScriptError extends Error { diagnostics: SourceDiagnostic[]; constructor(diagnostics: SourceDiagnostic[]); }
23
+
24
+ export { formatSource } from './formatter.js';
package/dist/index.js CHANGED
@@ -1,102 +1,11 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
3
  import { compileSource } from './transformer.js';
5
-
6
- const require = createRequire(import.meta.url);
7
- let tsModuleCache;
8
- let defaultCompilerOptions;
9
-
10
- function loadTypeScriptModule() {
11
- if (tsModuleCache) {
12
- return tsModuleCache;
13
- }
14
- let typescript;
15
- try {
16
- typescript = require('typescript');
17
- } catch (initialError) {
18
- const globalCandidate = path.join(process.execPath, '..', '..', 'lib', 'node_modules', 'typescript');
19
- try {
20
- typescript = require(globalCandidate);
21
- } catch (fallbackError) {
22
- throw new Error('TypeScript 모듈을 불러올 수 없습니다. Bun 또는 Node 환경에서 TypeScript를 설치해주세요.');
23
- }
24
- }
25
- tsModuleCache = typescript.default ?? typescript;
26
- return tsModuleCache;
27
- }
28
-
29
- function getDefaultCompilerOptions(ts) {
30
- if (!defaultCompilerOptions) {
31
- defaultCompilerOptions = {
32
- allowNonTsExtensions: true,
33
- noEmit: true,
34
- module: ts.ModuleKind.NodeNext,
35
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
36
- target: ts.ScriptTarget.ES2020,
37
- strict: true,
38
- esModuleInterop: true
39
- };
40
- }
41
- return defaultCompilerOptions;
42
- }
43
-
44
- function createLintHost(ts, compilerOptions, filePath, sourceText) {
45
- const resolvedFilePath = path.resolve(filePath);
46
- const baseHost = ts.createCompilerHost(compilerOptions, true);
47
- const sourceFile = ts.createSourceFile(
48
- resolvedFilePath,
49
- sourceText,
50
- compilerOptions.target ?? ts.ScriptTarget.ES2020,
51
- true,
52
- ts.ScriptKind.TS
53
- );
54
-
55
- const getSourceFile = baseHost.getSourceFile.bind(baseHost);
56
- const fileExists = baseHost.fileExists.bind(baseHost);
57
- const readFile = baseHost.readFile.bind(baseHost);
58
-
59
- baseHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
60
- if (path.resolve(fileName) === resolvedFilePath) {
61
- return shouldCreateNewSourceFile ? ts.createSourceFile(
62
- resolvedFilePath,
63
- sourceText,
64
- languageVersion,
65
- true,
66
- ts.ScriptKind.TS
67
- ) : sourceFile;
68
- }
69
- return getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
70
- };
71
-
72
- baseHost.fileExists = (fileName) => {
73
- if (path.resolve(fileName) === resolvedFilePath) {
74
- return true;
75
- }
76
- return fileExists(fileName);
77
- };
78
-
79
- baseHost.readFile = (fileName) => {
80
- if (path.resolve(fileName) === resolvedFilePath) {
81
- return sourceText;
82
- }
83
- return readFile(fileName);
84
- };
85
-
86
- return baseHost;
87
- }
88
-
89
- function formatDiagnostics(ts, diagnostics) {
90
- if (diagnostics.length === 0) {
91
- return '';
92
- }
93
- const formatHost = {
94
- getCanonicalFileName: (fileName) => fileName,
95
- getCurrentDirectory: () => process.cwd(),
96
- getNewLine: () => '\n'
97
- };
98
- return ts.formatDiagnosticsWithColorAndContext(diagnostics, formatHost);
99
- }
4
+ import { analyzeSource } from './checker.js';
5
+ import { formatSourceDiagnostics } from './diagnostics.js';
6
+ import { emitSource } from './emitter.js';
7
+ export { checkSource } from './checker.js';
8
+ export { HangulScriptError, formatSourceDiagnostics } from './diagnostics.js';
100
9
 
101
10
  export function compileString(source) {
102
11
  return compileSource(source);
@@ -118,16 +27,13 @@ export async function compileFileToDefaultDestination(inputPath) {
118
27
  }
119
28
 
120
29
  export function lintSource(source, filePath = 'input.hgs', compilerOptions) {
121
- const ts = loadTypeScriptModule();
122
- const options = compilerOptions ?? getDefaultCompilerOptions(ts);
123
- const compiled = compileSource(source);
124
- const host = createLintHost(ts, options, filePath, compiled);
125
- const program = ts.createProgram([path.resolve(filePath)], options, host);
126
- const diagnostics = ts.getPreEmitDiagnostics(program);
30
+ compileSource(source); // Keep syntax errors explicit for callers of the legacy lint API.
31
+ const result = analyzeSource(source, filePath, { compilerOptions });
127
32
  return {
128
- diagnostics,
129
- formattedDiagnostics: formatDiagnostics(ts, diagnostics),
130
- hasErrors: diagnostics.some((diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error)
33
+ diagnostics: result.nativeDiagnostics,
34
+ sourceDiagnostics: result.diagnostics,
35
+ formattedDiagnostics: formatSourceDiagnostics(result.diagnostics),
36
+ hasErrors: result.hasErrors
131
37
  };
132
38
  }
133
39
 
@@ -140,42 +46,30 @@ export { compileSource };
140
46
 
141
47
  /** Compile through TypeScript once, emitting executable JS and declarations in memory. */
142
48
  export function buildSource(source, filePath = 'input.hgs') {
143
- const ts = loadTypeScriptModule();
144
- const virtualPath = path.resolve(filePath).replace(/\.(?:hgs|ks)$/i, '') + '.ts';
145
- const compiled = compileSource(source);
146
- const options = {
147
- target: ts.ScriptTarget.ES2022,
148
- module: ts.ModuleKind.ESNext,
149
- moduleResolution: ts.ModuleResolutionKind.Bundler,
150
- strict: true,
151
- declaration: true,
152
- noEmitOnError: true,
153
- skipLibCheck: true
154
- };
155
- const host = createLintHost(ts, options, virtualPath, compiled);
156
- const files = new Map();
157
- host.writeFile = (name, content) => files.set(name, content);
158
- const program = ts.createProgram([virtualPath], options, host);
159
- const diagnostics = ts.getPreEmitDiagnostics(program);
160
- if (diagnostics.some(d => d.category === ts.DiagnosticCategory.Error)) {
161
- throw new Error(formatDiagnostics(ts, diagnostics));
162
- }
163
- const emitted = program.emit();
164
- if (emitted.emitSkipped) throw new Error(formatDiagnostics(ts, emitted.diagnostics) || '빌드에 실패했습니다.');
165
- return {
166
- javascript: files.get(virtualPath.replace(/\.ts$/, '.js')) ?? '',
167
- declaration: files.get(virtualPath.replace(/\.ts$/, '.d.ts')) ?? ''
168
- };
49
+ const { javascript, declaration } = emitSource(source, filePath);
50
+ return { javascript, declaration };
169
51
  }
170
52
 
171
53
  export async function buildFile(inputPath, outputDirectory = path.dirname(inputPath)) {
172
54
  const source = await readFile(inputPath, 'utf8');
173
- const result = buildSource(source, inputPath);
174
- const base = path.parse(inputPath).name;
175
- await mkdir(outputDirectory, { recursive: true });
176
- const javascriptPath = path.join(outputDirectory, base + '.js');
177
- const declarationPath = path.join(outputDirectory, base + '.d.ts');
178
- await writeFile(javascriptPath, result.javascript, 'utf8');
179
- await writeFile(declarationPath, result.declaration, 'utf8');
180
- return { ...result, javascriptPath, declarationPath };
55
+ const { javascript, declaration, modules } = emitSource(source, inputPath);
56
+ const rootDirectory = path.dirname(path.resolve(inputPath));
57
+ const destinations = modules.map(module => {
58
+ const relative = path.relative(rootDirectory, module.sourcePath);
59
+ if (relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
60
+ throw new Error('빌드 입력 파일의 폴더 밖에 있는 .hgs 모듈은 지원하지 않습니다. 공통 상위 폴더에 진입 파일을 두세요.');
61
+ }
62
+ const base = path.join(outputDirectory, relative.replace(/\.hgs$/, ''));
63
+ return { ...module, javascriptPath: base + '.js', declarationPath: base + '.d.ts' };
64
+ });
65
+ // Validate the complete graph before writing any output.
66
+ for (const module of destinations) {
67
+ await mkdir(path.dirname(module.javascriptPath), { recursive: true });
68
+ await writeFile(module.javascriptPath, module.javascript, 'utf8');
69
+ await writeFile(module.declarationPath, module.declaration, 'utf8');
70
+ }
71
+ const root = destinations.find(module => module.sourcePath === path.resolve(inputPath));
72
+ return { javascript, declaration, javascriptPath: root.javascriptPath, declarationPath: root.declarationPath };
181
73
  }
74
+
75
+ export { formatSource } from './formatter.js';
package/dist/loader.js ADDED
@@ -0,0 +1,16 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { fileURLToPath } from 'node:url';
3
+ import ts from 'typescript';
4
+ import { analyzeSource } from './checker.js';
5
+ import { HangulScriptError } from './diagnostics.js';
6
+ export async function load(url, context, nextLoad) {
7
+ if (url.startsWith('file:') && new URL(url).pathname.endsWith('.hgs')) {
8
+ const filePath=fileURLToPath(url);
9
+ const source=await readFile(filePath,'utf8');
10
+ const result=analyzeSource(source,filePath);
11
+ if(result.hasErrors) throw new HangulScriptError(result.diagnostics);
12
+ const compiled=ts.transpileModule(result.compiled,{compilerOptions:{module:ts.ModuleKind.ESNext,target:ts.ScriptTarget.ES2022}}).outputText;
13
+ return {format:'module',source:compiled,shortCircuit:true};
14
+ }
15
+ return nextLoad(url,context);
16
+ }
@@ -0,0 +1,2 @@
1
+ import { register } from 'node:module';
2
+ register('./loader.js',import.meta.url);
@@ -0,0 +1,44 @@
1
+ import { uiRuntime } from './ui.js';
2
+ export const sentenceRuntime = `${uiRuntime}
3
+
4
+ function 출력(...값: unknown[]): void { console.log(...값); }
5
+ function 길이(값: string | readonly unknown[]): number { return 값.length; }
6
+ function 추가<T>(목록: T[], 값: T): void { 목록.push(값); }
7
+ function 포함<T>(목록: readonly T[], 값: T): boolean { return 목록.includes(값); }
8
+ function 나누기(글: string, 구분: string): string[] { return 글.split(구분); }
9
+ function 잇기(목록: readonly unknown[], 구분 = ""): string { return 목록.join(구분); }
10
+ function 합계(값: readonly number[]): number { return 값.reduce((합, 항목) => 합 + 항목, 0); }
11
+ function 숫자로(값: string | number): number {
12
+ if (typeof 값 === "string" && 값.trim() === "") throw new Error("빈 문자열은 숫자로 바꿀 수 없습니다.");
13
+ const 결과 = Number(값);
14
+ if (!Number.isFinite(결과)) throw new Error("유한한 숫자를 입력하세요.");
15
+ return 결과;
16
+ }
17
+ function 다듬기(글: string): string { return 글.trim(); }
18
+ function 반올림(값: number, 자리 = 0): number {
19
+ if (!Number.isFinite(값) || !Number.isInteger(자리) || 자리 < -10 || 자리 > 10) throw new Error("유한한 수와 -10~10 사이의 정수 자리가 필요합니다.");
20
+ return Math.round(값 * 10 ** 자리) / 10 ** 자리;
21
+ }
22
+ function 평균(목록: readonly number[]): number {
23
+ if (!목록.length) throw new Error("빈 목록의 평균을 구할 수 없습니다.");
24
+ return 합계(목록) / 목록.length;
25
+ }
26
+ function 최솟값(목록: readonly number[]): number {
27
+ if (!목록.length) throw new Error("빈 목록의 최솟값을 구할 수 없습니다.");
28
+ let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 < 결과) 결과 = 항목;
29
+ return 결과;
30
+ }
31
+ function 최댓값(목록: readonly number[]): number {
32
+ if (!목록.length) throw new Error("빈 목록의 최댓값을 구할 수 없습니다.");
33
+ let 결과 = 목록[0]; for (const 항목 of 목록) if (항목 > 결과) 결과 = 항목;
34
+ return 결과;
35
+ }
36
+ function 정렬(값: readonly number[]): number[] { return [...값].sort((가, 나) => 가 - 나); }
37
+ function 범위(시작: number, 끝?: number, 간격 = 1): number[] {
38
+ if (끝 === undefined) { 끝 = 시작; 시작 = 0; }
39
+ if (!Number.isFinite(시작) || !Number.isFinite(끝) || !Number.isFinite(간격) || 간격 === 0) throw new Error("범위에는 유한한 수와 0이 아닌 간격이 필요합니다.");
40
+ const 개수 = Math.max(0, Math.ceil((끝 - 시작) / 간격));
41
+ if (개수 > 100000) throw new Error("범위는 최대 100000개까지 만들 수 있습니다.");
42
+ return Array.from({ length: 개수 }, (_, 순서) => 시작 + 순서 * 간격);
43
+ }
44
+ `;