ventojs 2.0.0-canary.1 → 2.0.0-canary.3
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.
- package/CHANGELOG.md +3 -0
- package/core/environment.js +40 -31
- package/core/errors.js +216 -133
- package/core/js.js +3 -1
- package/core/reserved.js +0 -10
- package/loaders/module.js +30 -23
- package/package.json +1 -1
- package/plugins/export.js +3 -3
- package/plugins/for.js +8 -5
- package/plugins/function.js +4 -4
- package/plugins/if.js +3 -3
- package/plugins/import.js +6 -6
- package/plugins/include.js +5 -4
- package/plugins/layout.js +36 -20
- package/plugins/set.js +3 -3
- package/types/core/environment.d.ts +2 -3
- package/types/core/errors.d.ts +23 -15
- package/types/loaders/module.d.ts +7 -9
package/CHANGELOG.md
CHANGED
@@ -13,6 +13,8 @@ Vento 2.0 is now dependency-free and compatible with browsers without a build st
|
|
13
13
|
- Support for precompiled templates.
|
14
14
|
- New filesystem loader to use File System API.
|
15
15
|
- Better errors reporting [#131], [#137]
|
16
|
+
- `core/errors.ts` module to format errors.
|
17
|
+
- New `{{ slot }}` tag to pass extra variables to `{{ layout }}` [#140]
|
16
18
|
|
17
19
|
### Changed
|
18
20
|
- Renamed `src` directory to `core`.
|
@@ -47,3 +49,4 @@ Vento 2.0 is now dependency-free and compatible with browsers without a build st
|
|
47
49
|
[#131]: https://github.com/ventojs/vento/issues/131
|
48
50
|
[#134]: https://github.com/ventojs/vento/issues/134
|
49
51
|
[#137]: https://github.com/ventojs/vento/issues/137
|
52
|
+
[#140]: https://github.com/ventojs/vento/issues/140
|
package/core/environment.js
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import iterateTopLevel from "./js.js";
|
2
2
|
import tokenize from "./tokenizer.js";
|
3
|
-
import { createError,
|
3
|
+
import { createError, SourceError } from "./errors.js";
|
4
4
|
export class Environment {
|
5
5
|
cache = new Map();
|
6
6
|
options;
|
@@ -20,8 +20,8 @@ export class Environment {
|
|
20
20
|
use(plugin) {
|
21
21
|
plugin(this);
|
22
22
|
}
|
23
|
-
async run(file, data, from) {
|
24
|
-
const template = await this.load(file, from);
|
23
|
+
async run(file, data, from, position) {
|
24
|
+
const template = await this.load(file, from, position);
|
25
25
|
return await template(data);
|
26
26
|
}
|
27
27
|
async runString(source, data, file) {
|
@@ -41,25 +41,21 @@ export class Environment {
|
|
41
41
|
if (typeof source !== "string") {
|
42
42
|
throw new TypeError(`The source code of "${path}" must be a string. Got ${typeof source}`);
|
43
43
|
}
|
44
|
-
const
|
45
|
-
const tokens = [...allTokens];
|
44
|
+
const tokens = this.tokenize(source, path);
|
46
45
|
const lastToken = tokens.at(-1);
|
47
46
|
if (lastToken[0] != "string") {
|
48
|
-
throw new
|
47
|
+
throw new SourceError("Unclosed tag", lastToken[2], path, source);
|
49
48
|
}
|
50
49
|
let code = "";
|
51
50
|
try {
|
52
51
|
code = this.compileTokens(tokens).join("\n");
|
53
52
|
}
|
54
53
|
catch (error) {
|
55
|
-
if (
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
tokens: allTokens,
|
61
|
-
path,
|
62
|
-
});
|
54
|
+
if (error instanceof SourceError) {
|
55
|
+
error.file ??= path;
|
56
|
+
error.source ??= source;
|
57
|
+
}
|
58
|
+
throw error;
|
63
59
|
}
|
64
60
|
const { dataVarname, autoDataVarname } = this.options;
|
65
61
|
if (autoDataVarname) {
|
@@ -77,32 +73,32 @@ export class Environment {
|
|
77
73
|
}
|
78
74
|
try {
|
79
75
|
const constructor = new Function("__env", `return async function __template(${dataVarname}) {
|
76
|
+
let __pos=0;
|
80
77
|
try {
|
81
78
|
${dataVarname} = Object.assign({}, __template.defaults, ${dataVarname});
|
82
79
|
const __exports = { content: "" };
|
83
80
|
${code}
|
84
81
|
return __exports;
|
85
82
|
} catch (error) {
|
86
|
-
throw __env.utils.createError(error, __template);
|
83
|
+
throw __env.utils.createError(error, __template, __pos);
|
87
84
|
}
|
88
85
|
}`);
|
89
86
|
const template = constructor(this);
|
90
87
|
template.path = path;
|
91
88
|
template.code = constructor.toString();
|
92
89
|
template.source = source;
|
93
|
-
template.tokens = allTokens;
|
94
90
|
template.defaults = defaults || {};
|
95
91
|
return template;
|
96
92
|
}
|
97
93
|
catch (error) {
|
98
|
-
if (
|
99
|
-
throw error;
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
94
|
+
if (error instanceof SyntaxError) {
|
95
|
+
throw createError(error, { source, code, path });
|
96
|
+
}
|
97
|
+
if (error instanceof SourceError) {
|
98
|
+
error.file ??= path;
|
99
|
+
error.source ??= source;
|
100
|
+
}
|
101
|
+
throw error;
|
106
102
|
}
|
107
103
|
}
|
108
104
|
tokenize(source, path) {
|
@@ -115,7 +111,15 @@ export class Environment {
|
|
115
111
|
}
|
116
112
|
return tokens;
|
117
113
|
}
|
118
|
-
async load(file, from) {
|
114
|
+
async load(file, from, position) {
|
115
|
+
if (!file) {
|
116
|
+
throw position !== undefined
|
117
|
+
? new SourceError(`Invalid template (${typeof file})`, position, from)
|
118
|
+
: new Error(`Invalid template (${typeof file})`);
|
119
|
+
}
|
120
|
+
if (typeof file !== "string") {
|
121
|
+
throw new TypeError(`The template filename must be a string. Got ${typeof file}`);
|
122
|
+
}
|
119
123
|
const path = this.options.loader.resolve(from || "", file);
|
120
124
|
let cached = this.cache.get(path);
|
121
125
|
if (cached) {
|
@@ -126,6 +130,11 @@ export class Environment {
|
|
126
130
|
.split("?")[0]
|
127
131
|
.split("#")[0];
|
128
132
|
cached = this.options.loader.load(cleanPath)
|
133
|
+
.catch((error) => {
|
134
|
+
throw position !== undefined
|
135
|
+
? new SourceError(`Error loading template: ${error.message}`, position, from)
|
136
|
+
: error;
|
137
|
+
})
|
129
138
|
.then((result) => {
|
130
139
|
if (typeof result === "function") {
|
131
140
|
return result(this);
|
@@ -141,7 +150,7 @@ export class Environment {
|
|
141
150
|
let openToken;
|
142
151
|
tokens: while (tokens.length > 0) {
|
143
152
|
const token = tokens.shift();
|
144
|
-
const [type, code,
|
153
|
+
const [type, code, position] = token;
|
145
154
|
openToken ??= token;
|
146
155
|
// We found the closing tag, so we stop compiling
|
147
156
|
if (closeToken && type === "tag" && closeToken === code) {
|
@@ -157,7 +166,7 @@ export class Environment {
|
|
157
166
|
continue;
|
158
167
|
}
|
159
168
|
if (type === "tag") {
|
160
|
-
compiled.push(
|
169
|
+
compiled.push(`__pos=${position};`);
|
161
170
|
for (const tag of this.tags) {
|
162
171
|
const compiledTag = tag(this, token, outputVar, tokens);
|
163
172
|
if (typeof compiledTag === "string") {
|
@@ -170,11 +179,11 @@ export class Environment {
|
|
170
179
|
compiled.push(`${outputVar} += (${expression}) ?? "";`);
|
171
180
|
continue;
|
172
181
|
}
|
173
|
-
throw new
|
182
|
+
throw new SourceError(`Unknown token type "${type}"`, position);
|
174
183
|
}
|
175
184
|
// If we reach here, it means we have an open token that wasn't closed
|
176
185
|
if (closeToken) {
|
177
|
-
throw new
|
186
|
+
throw new SourceError(`Missing closing tag ("${closeToken}" tag is expected)`, openToken[2]);
|
178
187
|
}
|
179
188
|
return compiled;
|
180
189
|
}
|
@@ -185,7 +194,7 @@ export class Environment {
|
|
185
194
|
const [, code, position] = token;
|
186
195
|
const match = code.match(/^(await\s+)?([\w.]+)(?:\((.*)\))?$/);
|
187
196
|
if (!match) {
|
188
|
-
throw new
|
197
|
+
throw new SourceError(`Invalid filter: ${code}`, position);
|
189
198
|
}
|
190
199
|
const [_, isAsync, name, args] = match;
|
191
200
|
if (!Object.hasOwn(this.filters, name)) {
|
@@ -235,7 +244,7 @@ thisObject, method, ...args) {
|
|
235
244
|
if (typeof thisObject[method] === "function") {
|
236
245
|
return thisObject[method](...args);
|
237
246
|
}
|
238
|
-
throw new
|
247
|
+
throw new SourceError(`Method "${method}" is not a function of ${typeof thisObject} variable`, position);
|
239
248
|
}
|
240
249
|
function checkAsync(fn) {
|
241
250
|
return fn.constructor?.name === "AsyncFunction";
|
package/core/errors.js
CHANGED
@@ -1,182 +1,265 @@
|
|
1
1
|
export class VentoError extends Error {
|
2
2
|
}
|
3
|
-
export class
|
4
|
-
|
5
|
-
source;
|
3
|
+
export class SourceError extends VentoError {
|
4
|
+
position;
|
6
5
|
file;
|
7
|
-
|
6
|
+
source;
|
7
|
+
constructor(message, position, file, source) {
|
8
8
|
super(message);
|
9
|
-
this.name = "
|
10
|
-
this.
|
11
|
-
this.source = source;
|
9
|
+
this.name = "SourceError";
|
10
|
+
this.position = position;
|
12
11
|
this.file = file;
|
12
|
+
this.source = source;
|
13
13
|
}
|
14
14
|
getContext() {
|
15
|
-
if (!this.source || this.token === undefined) {
|
16
|
-
return;
|
17
|
-
}
|
18
15
|
return {
|
19
16
|
type: this.name,
|
20
17
|
message: this.message,
|
21
|
-
|
22
|
-
position: typeof this.token === "number" ? this.token : this.token[2],
|
18
|
+
position: this.position,
|
23
19
|
file: this.file,
|
20
|
+
source: this.source,
|
24
21
|
};
|
25
22
|
}
|
26
23
|
}
|
27
24
|
export class RuntimeError extends VentoError {
|
28
|
-
context;
|
29
|
-
|
25
|
+
#context;
|
26
|
+
position;
|
27
|
+
constructor(error, context, position) {
|
30
28
|
super(error.message);
|
31
29
|
this.name = error.name || "JavaScriptError";
|
32
|
-
this
|
30
|
+
this.#context = context;
|
33
31
|
this.cause = error;
|
32
|
+
this.position = position;
|
34
33
|
}
|
35
|
-
getContext() {
|
36
|
-
|
37
|
-
|
34
|
+
async getContext() {
|
35
|
+
const { code, source, path } = this.#context;
|
36
|
+
// If we don't have the position, we cannot provide a context
|
37
|
+
// Try to get the context from a SyntaxError
|
38
|
+
if (this.position === undefined) {
|
39
|
+
try {
|
40
|
+
return (await getSyntaxErrorContext(this.cause, this.#context)) ??
|
41
|
+
{
|
42
|
+
type: this.name || "JavaScriptError",
|
43
|
+
message: this.message,
|
44
|
+
source,
|
45
|
+
code,
|
46
|
+
file: path,
|
47
|
+
};
|
48
|
+
}
|
49
|
+
catch {
|
50
|
+
return {
|
51
|
+
type: this.name || "JavaScriptError",
|
52
|
+
message: this.message,
|
53
|
+
source,
|
54
|
+
code,
|
55
|
+
file: path,
|
56
|
+
};
|
57
|
+
}
|
38
58
|
}
|
39
|
-
|
40
|
-
|
59
|
+
// Capture the exact position of the error in the compiled code
|
60
|
+
for (const frame of getStackFrames(this.cause)) {
|
61
|
+
if (frame.file !== "<anonymous>" &&
|
62
|
+
path &&
|
63
|
+
![path + ".js", path + ".mjs"].some((p) => frame.file.endsWith(p))) {
|
64
|
+
continue;
|
65
|
+
}
|
66
|
+
return {
|
67
|
+
type: this.name || "JavaScriptError",
|
68
|
+
message: this.message,
|
69
|
+
source,
|
70
|
+
position: this.position,
|
71
|
+
code,
|
72
|
+
line: frame.line,
|
73
|
+
column: frame.column,
|
74
|
+
file: path,
|
75
|
+
};
|
41
76
|
}
|
77
|
+
// As a fallback, return the error with the available context
|
78
|
+
return {
|
79
|
+
type: this.name || "JavaScriptError",
|
80
|
+
message: this.message,
|
81
|
+
source,
|
82
|
+
position: this.position,
|
83
|
+
code,
|
84
|
+
file: path,
|
85
|
+
};
|
42
86
|
}
|
43
87
|
}
|
44
|
-
|
88
|
+
/** Create or complete VentoError with extra info from the template */
|
89
|
+
export function createError(error, context, position) {
|
45
90
|
if (error instanceof RuntimeError)
|
46
91
|
return error;
|
47
|
-
// If the error is a
|
48
|
-
if (error instanceof
|
92
|
+
// If the error is a SourceError, we can fill the missing context information
|
93
|
+
if (error instanceof SourceError) {
|
49
94
|
error.file ??= context.path;
|
50
95
|
error.source ??= context.source;
|
96
|
+
error.position ??= position;
|
51
97
|
return error;
|
52
98
|
}
|
53
99
|
// JavaScript syntax errors can be parsed to get accurate position
|
54
|
-
return new RuntimeError(error, context);
|
100
|
+
return new RuntimeError(error, context, position);
|
55
101
|
}
|
56
|
-
|
102
|
+
const colors = {
|
103
|
+
number: (n) => `\x1b[33m${n}\x1b[39m`,
|
104
|
+
dim: (line) => `\x1b[2m${line}\x1b[22m`,
|
105
|
+
error: (msg) => `\x1b[31m${msg}\x1b[39m`,
|
106
|
+
};
|
107
|
+
const plain = {
|
108
|
+
number: (n) => n,
|
109
|
+
dim: (line) => line,
|
110
|
+
error: (msg) => msg,
|
111
|
+
};
|
112
|
+
const formats = {
|
113
|
+
colors,
|
114
|
+
plain,
|
115
|
+
};
|
116
|
+
/** Prints an error to the console in a formatted way. */
|
117
|
+
export async function printError(error, format = plain) {
|
57
118
|
if (error instanceof VentoError) {
|
58
119
|
const context = await error.getContext();
|
120
|
+
const fmt = typeof format === "string" ? formats[format] || plain : format;
|
59
121
|
if (context) {
|
60
|
-
console.error(
|
122
|
+
console.error(stringifyError(context, fmt));
|
61
123
|
return;
|
62
124
|
}
|
63
125
|
}
|
64
126
|
console.error(error);
|
65
127
|
}
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
return;
|
70
|
-
const row = Number(stackMatch[1]) - 1;
|
71
|
-
const col = Number(stackMatch[2]);
|
72
|
-
const position = getAccurateErrorPosition(row, col, context);
|
73
|
-
if (position == -1)
|
74
|
-
return;
|
75
|
-
return {
|
76
|
-
type: error.name || "JavaScriptError",
|
77
|
-
message: error.message,
|
78
|
-
source: context.source,
|
79
|
-
position,
|
80
|
-
file: context.path,
|
81
|
-
};
|
82
|
-
}
|
83
|
-
async function parseSyntaxError(error, context) {
|
84
|
-
const code = `()=>{${context.code}}`;
|
85
|
-
const dataUrl = "data:application/javascript;base64," + btoa(code);
|
86
|
-
const stack = await import(dataUrl).catch(({ stack }) => stack);
|
87
|
-
if (!stack)
|
88
|
-
return;
|
89
|
-
const stackMatch = stack?.match(/:(\d+):(\d+)$/m);
|
90
|
-
if (!stackMatch)
|
91
|
-
return;
|
92
|
-
const row = Number(stackMatch[1]) - 1;
|
93
|
-
const col = Number(stackMatch[2]);
|
94
|
-
const position = getAccurateErrorPosition(row, col, context);
|
95
|
-
if (position == -1)
|
96
|
-
return;
|
97
|
-
return {
|
98
|
-
type: "SyntaxError",
|
99
|
-
message: error.message,
|
100
|
-
source: context.source,
|
101
|
-
position,
|
102
|
-
file: context.path,
|
103
|
-
};
|
104
|
-
}
|
105
|
-
function getAccurateErrorPosition(row, col, context) {
|
106
|
-
const { code, tokens, source } = context;
|
107
|
-
if (!tokens)
|
108
|
-
return -1;
|
109
|
-
const linesAndDelims = code.split(/(\r\n?|[\n\u2028\u2029])/);
|
110
|
-
const linesAndDelimsUntilIssue = linesAndDelims.slice(0, row * 2);
|
111
|
-
const issueIndex = linesAndDelimsUntilIssue.join("").length + col;
|
112
|
-
const posLine = linesAndDelimsUntilIssue.findLast((line) => {
|
113
|
-
return /^\/\*__pos:(\d+)\*\/$/.test(line);
|
114
|
-
});
|
115
|
-
if (!posLine)
|
116
|
-
return -1;
|
117
|
-
const position = Number(posLine.slice(8, -2));
|
118
|
-
const token = tokens.findLast((token) => {
|
119
|
-
if (token[2] == undefined)
|
120
|
-
return false;
|
121
|
-
return token[2] <= position;
|
122
|
-
});
|
123
|
-
if (!token)
|
124
|
-
return -1;
|
125
|
-
const isJS = token[1].startsWith(">");
|
126
|
-
const tag = isJS ? token[1].slice(1).trimStart() : token[1];
|
127
|
-
const issueStartIndex = code.lastIndexOf(tag, issueIndex);
|
128
|
-
if (issueStartIndex == -1)
|
129
|
-
return -1;
|
130
|
-
const sourceIssueStartIndex = source.indexOf(tag, position);
|
131
|
-
return sourceIssueStartIndex + issueIndex - issueStartIndex - 1;
|
132
|
-
}
|
133
|
-
const terminal = {
|
134
|
-
number: (n) => `\x1b[33m${n}\x1b[39m`,
|
135
|
-
dim: (line) => `\x1b[2m${line}\x1b[22m`,
|
136
|
-
error: (msg) => `\x1b[31m${msg}\x1b[39m`,
|
137
|
-
};
|
138
|
-
const LINE_TERMINATOR = /\r\n?|[\n\u2028\u2029]/;
|
139
|
-
export function stringifyContext(context, format = terminal) {
|
140
|
-
const { type, message, source, position, file } = context;
|
141
|
-
const sourceAfterIssue = source.slice(position);
|
142
|
-
const newlineMatch = sourceAfterIssue.match(LINE_TERMINATOR);
|
143
|
-
const endIndex = position + (newlineMatch?.index ?? sourceAfterIssue.length);
|
144
|
-
const lines = source.slice(0, endIndex).split(LINE_TERMINATOR);
|
145
|
-
const displayedLineEntries = [...lines.entries()].slice(-3);
|
146
|
-
const endLineIndex = lines.at(-1).length + position - endIndex;
|
147
|
-
const numberLength = (displayedLineEntries.at(-1)[0] + 1).toString().length;
|
148
|
-
const displayedCode = displayedLineEntries.map(([index, line]) => {
|
149
|
-
const number = `${index + 1}`.padStart(numberLength);
|
150
|
-
const sidebar = ` ${format.number(number)} ${format.dim("|")} `;
|
151
|
-
return sidebar + line;
|
152
|
-
}).join("\n");
|
153
|
-
const sidebarWidth = numberLength + 4;
|
154
|
-
const tooltipIndex = sidebarWidth + endLineIndex;
|
155
|
-
const tooltipIndent = " ".repeat(tooltipIndex);
|
156
|
-
const tooltip = tooltipIndent + format.error(`^ ${message}`);
|
128
|
+
/** Converts an error context into a formatted string representation. */
|
129
|
+
export function stringifyError(context, format = plain) {
|
130
|
+
const { type, message, source, position, code, line, column, file } = context;
|
157
131
|
const output = [];
|
132
|
+
// Print error type and message
|
158
133
|
output.push(`${format.error(type)}: ${message}`);
|
134
|
+
// If we don't know the position, we cannot print the source code
|
135
|
+
if (position === undefined || source === undefined) {
|
136
|
+
if (file) {
|
137
|
+
output.push(format.dim(file));
|
138
|
+
}
|
139
|
+
return output.join("\n");
|
140
|
+
}
|
141
|
+
const sourceLines = codeToLines(source);
|
142
|
+
const [sourceLine, sourceColumn] = getSourceLineColumn(sourceLines, position);
|
143
|
+
// Print file location if available
|
159
144
|
if (file) {
|
160
|
-
output.push(format.dim(
|
145
|
+
output.push(format.dim(`${file}:${sourceLine}:${sourceColumn}`));
|
161
146
|
}
|
162
|
-
|
147
|
+
const pad = sourceLine.toString().length;
|
148
|
+
// Print the latest lines of the source code before the error
|
149
|
+
for (let line = Math.max(sourceLine - 3, 1); line <= sourceLine; line++) {
|
150
|
+
const sidebar = ` ${format.number(`${line}`.padStart(pad))} ${format.dim("|")} `;
|
151
|
+
output.push(sidebar + sourceLines[line - 1].trimEnd());
|
152
|
+
}
|
153
|
+
// If we don't have the compiled code, return the tag position
|
154
|
+
const indent = ` ${" ".repeat(pad)} ${format.dim("|")}`;
|
155
|
+
// If we don't have the compiled code, print the tag position
|
156
|
+
if (!code || line === undefined || column === undefined) {
|
157
|
+
output.push(`${indent} ${" ".repeat(sourceColumn - 1)}${format.error(`^ ${message}`)}`);
|
158
|
+
return output.join("\n");
|
159
|
+
}
|
160
|
+
// Print the compiled code with the error position
|
161
|
+
const codeLines = codeToLines(code);
|
162
|
+
output.push(`${indent} ${" ".repeat(sourceColumn - 1)}${format.error("^")}`);
|
163
|
+
output.push(`${indent} ${format.dim(codeLines[line - 1]?.trimEnd() || "")}`);
|
164
|
+
output.push(`${indent} ${" ".repeat(column)} ${format.error(`^ ${message}`)}`);
|
163
165
|
return output.join("\n");
|
164
166
|
}
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
167
|
+
/**
|
168
|
+
* Extracts the context from a SyntaxError
|
169
|
+
* It does not work on Node.js and Bun due to the lack of position information
|
170
|
+
* in the stack trace of a dynamic import error.
|
171
|
+
*/
|
172
|
+
async function getSyntaxErrorContext(error, context) {
|
173
|
+
const { source, code } = context;
|
174
|
+
const url = URL.createObjectURL(new Blob([code], { type: "application/javascript" }));
|
175
|
+
const err = await import(url).catch((e) => e);
|
176
|
+
URL.revokeObjectURL(url);
|
177
|
+
for (const frame of getStackFrames(err)) {
|
178
|
+
if (!frame.file.startsWith("blob:")) {
|
179
|
+
continue;
|
180
|
+
}
|
181
|
+
return {
|
182
|
+
type: "SyntaxError",
|
183
|
+
message: error.message,
|
184
|
+
source,
|
185
|
+
position: searchPosition(frame, code) ?? 0,
|
186
|
+
code,
|
187
|
+
line: frame.line,
|
188
|
+
column: frame.column,
|
189
|
+
file: context.path,
|
190
|
+
};
|
191
|
+
}
|
192
|
+
}
|
193
|
+
const LINE_TERMINATOR = /(\r\n?|[\n\u2028\u2029])/;
|
194
|
+
/** Convert the source code into an array of lines */
|
195
|
+
function codeToLines(code) {
|
196
|
+
const doubleLines = code.split(LINE_TERMINATOR);
|
197
|
+
const lines = [];
|
198
|
+
for (let i = 0; i < doubleLines.length; i += 2) {
|
199
|
+
lines.push(`${doubleLines[i]}${doubleLines[i + 1] ?? ""}`);
|
200
|
+
}
|
201
|
+
return lines;
|
202
|
+
}
|
203
|
+
const POSITION_VARIABLE = /^__pos=(\d+);$/;
|
204
|
+
/** Search the closest token to an error */
|
205
|
+
function searchPosition(frame, code) {
|
206
|
+
const posLine = codeToLines(code)
|
207
|
+
.slice(0, frame.line - 1)
|
208
|
+
.findLast((line) => POSITION_VARIABLE.test(line.trim()));
|
209
|
+
if (posLine) {
|
210
|
+
return Number(posLine.trim().slice(6, -1));
|
211
|
+
}
|
212
|
+
}
|
213
|
+
/** Get the line and column number of a position in the code */
|
214
|
+
function getSourceLineColumn(lines, position) {
|
215
|
+
if (position < 0) {
|
216
|
+
return [1, 1]; // Position is before the start of the source
|
217
|
+
}
|
218
|
+
let index = 0;
|
219
|
+
for (const [line, content] of lines.entries()) {
|
220
|
+
const length = content.length;
|
221
|
+
if (position < index + length) {
|
222
|
+
return [line + 1, position - index + 1];
|
176
223
|
}
|
177
|
-
|
178
|
-
|
224
|
+
index += content.length;
|
225
|
+
}
|
226
|
+
throw new Error(`Position ${position} is out of bounds for the provided source lines.`);
|
227
|
+
}
|
228
|
+
/** Returns every combination of file, line and column of an error stack */
|
229
|
+
// deno-lint-ignore no-explicit-any
|
230
|
+
function* getStackFrames(error) {
|
231
|
+
// Firefox specific
|
232
|
+
const { columnNumber, lineNumber, fileName } = error;
|
233
|
+
if (columnNumber !== undefined && lineNumber !== undefined && fileName) {
|
234
|
+
yield {
|
235
|
+
file: normalizeFile(fileName),
|
236
|
+
line: lineNumber,
|
237
|
+
column: columnNumber,
|
238
|
+
};
|
239
|
+
}
|
240
|
+
const { stack } = error;
|
241
|
+
if (!stack) {
|
242
|
+
return;
|
243
|
+
}
|
244
|
+
const matches = stack.matchAll(/([^(\s,]+):(\d+):(\d+)/g);
|
245
|
+
for (const match of matches) {
|
246
|
+
const [_, file, line, column] = match;
|
247
|
+
// Skip Node, Bun & Deno internal stack frames
|
248
|
+
if (file.startsWith("node:") || file.startsWith("ext:") || file === "native") {
|
249
|
+
continue;
|
179
250
|
}
|
251
|
+
yield {
|
252
|
+
file: normalizeFile(file),
|
253
|
+
line: Number(line),
|
254
|
+
column: Number(column),
|
255
|
+
};
|
180
256
|
}
|
181
|
-
|
257
|
+
}
|
258
|
+
function normalizeFile(file) {
|
259
|
+
if (!file)
|
260
|
+
return "<anonymous>";
|
261
|
+
// Firefox may return "Function" for anonymous functions
|
262
|
+
if (file === "Function")
|
263
|
+
return "<anonymous>";
|
264
|
+
return file;
|
182
265
|
}
|
package/core/js.js
CHANGED
@@ -30,8 +30,10 @@ export default function* iterateTopLevel(source, start = 0) {
|
|
30
30
|
const [stop, variable] = match;
|
31
31
|
if (variable) {
|
32
32
|
cursor += variable.length;
|
33
|
-
|
33
|
+
// Words used internally by Vento start with two underscores
|
34
|
+
if (!reserved.has(variable) && !variable.startsWith("__")) {
|
34
35
|
variables.add(variable);
|
36
|
+
}
|
35
37
|
continue;
|
36
38
|
}
|
37
39
|
// Check the type of the stopping point.
|
package/core/reserved.js
CHANGED
@@ -1,14 +1,4 @@
|
|
1
1
|
const variables = new Set([
|
2
|
-
// Words reserved by Vento, used internally. In general, don't use variable
|
3
|
-
// names starting with two underscores to be future-proof and avoid clashes.
|
4
|
-
"__file",
|
5
|
-
"__template",
|
6
|
-
"__env",
|
7
|
-
"__defaults",
|
8
|
-
"__err",
|
9
|
-
"__exports",
|
10
|
-
"__pos",
|
11
|
-
"__tmp",
|
12
2
|
// JS reserved words, and some "dangerous" words like `let`, `async`, `of` or
|
13
3
|
// `undefined`, which aren't technically reserved but don't name your
|
14
4
|
// variables that.
|
package/loaders/module.js
CHANGED
@@ -4,11 +4,13 @@
|
|
4
4
|
*/
|
5
5
|
export class ModuleLoader {
|
6
6
|
#root;
|
7
|
-
|
7
|
+
#extension;
|
8
|
+
constructor(root, extension = ".js") {
|
8
9
|
this.#root = root;
|
10
|
+
this.#extension = extension;
|
9
11
|
}
|
10
12
|
async load(file) {
|
11
|
-
const url = new URL(join(this.#root.pathname, file +
|
13
|
+
const url = new URL(join(this.#root.pathname, file + this.#extension), this.#root);
|
12
14
|
const module = await import(url.toString());
|
13
15
|
return module.default;
|
14
16
|
}
|
@@ -18,33 +20,38 @@ export class ModuleLoader {
|
|
18
20
|
}
|
19
21
|
return join("/", file);
|
20
22
|
}
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
export function
|
34
|
-
|
35
|
-
throw new Error("Template source is not defined");
|
36
|
-
}
|
37
|
-
const exportCode = `export default function (__env) {
|
23
|
+
/**
|
24
|
+
* Outputs a template as a string that can be used in an ES module.
|
25
|
+
* This is useful for precompiled templates.
|
26
|
+
* @returns A tuple with the path and the content of the module.
|
27
|
+
*/
|
28
|
+
output(template, source = false) {
|
29
|
+
if (!template.source) {
|
30
|
+
throw new Error("Template source is not defined");
|
31
|
+
}
|
32
|
+
if (!template.path) {
|
33
|
+
throw new Error("Template path is not defined");
|
34
|
+
}
|
35
|
+
const content = `export default function (__env
|
36
|
+
) {
|
38
37
|
${template.toString()};
|
39
38
|
|
40
|
-
${
|
41
|
-
|
39
|
+
${source
|
40
|
+
? `__template.path = ${JSON.stringify(template.path)};
|
42
41
|
__template.code = ${JSON.stringify(template.code)};
|
43
42
|
__template.source = ${JSON.stringify(template.source)};`
|
44
|
-
|
43
|
+
: ""}
|
45
44
|
__template.defaults = ${JSON.stringify(template.defaults || {})};
|
46
45
|
|
47
46
|
return __template;
|
48
47
|
}`;
|
49
|
-
|
48
|
+
return [`${template.path}${this.#extension}`, content];
|
49
|
+
}
|
50
|
+
}
|
51
|
+
function join(...parts) {
|
52
|
+
return parts.join("/").replace(/\/+/g, "/");
|
53
|
+
}
|
54
|
+
function dirname(path) {
|
55
|
+
const lastSlash = path.lastIndexOf("/");
|
56
|
+
return lastSlash === -1 ? "." : path.slice(0, lastSlash);
|
50
57
|
}
|
package/package.json
CHANGED
package/plugins/export.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(exportTag);
|
@@ -10,7 +10,7 @@ const INLINE_NAMED_EXPORT = /^([a-zA-Z_]\w*)\s*=([^]*)$/;
|
|
10
10
|
const NAMED_EXPORTS = /^{[^]*?}$/;
|
11
11
|
const AS = /\s+\bas\b\s+/;
|
12
12
|
function exportTag(env, token, _output, tokens) {
|
13
|
-
const [, code] = token;
|
13
|
+
const [, code, position] = token;
|
14
14
|
const exportStart = code.match(EXPORT_START);
|
15
15
|
if (!exportStart) {
|
16
16
|
return;
|
@@ -58,7 +58,7 @@ function exportTag(env, token, _output, tokens) {
|
|
58
58
|
compiled.push(`__exports["${rename}"] = ${value};`);
|
59
59
|
}
|
60
60
|
else {
|
61
|
-
throw new
|
61
|
+
throw new SourceError("Invalid export", position);
|
62
62
|
}
|
63
63
|
}
|
64
64
|
return compiled.join("\n");
|
package/plugins/for.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
import iterateTopLevel from "../core/js.js";
|
3
3
|
export default function () {
|
4
4
|
return (env) => {
|
@@ -7,7 +7,7 @@ export default function () {
|
|
7
7
|
};
|
8
8
|
}
|
9
9
|
function forTag(env, token, output, tokens) {
|
10
|
-
const [, code] = token;
|
10
|
+
const [, code, position] = token;
|
11
11
|
if (code === "break" || code === "continue") {
|
12
12
|
return `${code};`;
|
13
13
|
}
|
@@ -17,7 +17,7 @@ function forTag(env, token, output, tokens) {
|
|
17
17
|
const compiled = [];
|
18
18
|
const match = code.match(/^for\s+(await\s+)?([\s\S]*)$/);
|
19
19
|
if (!match) {
|
20
|
-
throw new
|
20
|
+
throw new SourceError("Invalid for loop", position);
|
21
21
|
}
|
22
22
|
let [, aw, tagCode] = match;
|
23
23
|
let var1;
|
@@ -29,7 +29,7 @@ function forTag(env, token, output, tokens) {
|
|
29
29
|
else {
|
30
30
|
const parts = tagCode.match(/(^[^\s,]+)([\s|\S]+)$/);
|
31
31
|
if (!parts) {
|
32
|
-
throw new
|
32
|
+
throw new SourceError("Invalid for loop", position);
|
33
33
|
}
|
34
34
|
var1 = parts[1].trim();
|
35
35
|
tagCode = parts[2].trim();
|
@@ -43,7 +43,7 @@ function forTag(env, token, output, tokens) {
|
|
43
43
|
else {
|
44
44
|
const parts = tagCode.match(/^([\w]+)\s+of\s+([\s|\S]+)$/);
|
45
45
|
if (!parts) {
|
46
|
-
throw new
|
46
|
+
throw new SourceError("Invalid for loop", position);
|
47
47
|
}
|
48
48
|
var2 = parts[1].trim();
|
49
49
|
collection = parts[2].trim();
|
@@ -52,6 +52,9 @@ function forTag(env, token, output, tokens) {
|
|
52
52
|
else if (tagCode.startsWith("of ")) {
|
53
53
|
collection = tagCode.slice(3).trim();
|
54
54
|
}
|
55
|
+
else {
|
56
|
+
throw new SourceError("Invalid for loop", position);
|
57
|
+
}
|
55
58
|
if (var2) {
|
56
59
|
compiled.push(`for ${aw || ""}(let [${var1}, ${var2}] of __env.utils.toIterator(${env.compileFilters(tokens, collection)}, true)) {`);
|
57
60
|
}
|
package/plugins/function.js
CHANGED
@@ -1,17 +1,17 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(functionTag);
|
5
5
|
};
|
6
6
|
}
|
7
7
|
function functionTag(env, token, _output, tokens) {
|
8
|
-
const [, code] = token;
|
8
|
+
const [, code, position] = token;
|
9
9
|
if (!code.match(/^(export\s+)?(async\s+)?function\s/)) {
|
10
10
|
return;
|
11
11
|
}
|
12
|
-
const match = code.match(/^(export\s+)?(async\s+)?function\s+(\w+)\s*(\([^
|
12
|
+
const match = code.match(/^(export\s+)?(async\s+)?function\s+(\w+)\s*(\([^]*\))?$/);
|
13
13
|
if (!match) {
|
14
|
-
throw new
|
14
|
+
throw new SourceError("Invalid function tag", position);
|
15
15
|
}
|
16
16
|
const [_, exp, as, name, args] = match;
|
17
17
|
const compiled = [];
|
package/plugins/if.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(ifTag);
|
@@ -18,13 +18,13 @@ function ifTag(env, [, code], output, tokens) {
|
|
18
18
|
return compiled.join("\n");
|
19
19
|
}
|
20
20
|
function elseTag(_env, token) {
|
21
|
-
const [, code] = token;
|
21
|
+
const [, code, position] = token;
|
22
22
|
if (!code.startsWith("else ") && code !== "else") {
|
23
23
|
return;
|
24
24
|
}
|
25
25
|
const match = code.match(/^else(\s+if\s+(.*))?$/);
|
26
26
|
if (!match) {
|
27
|
-
throw new
|
27
|
+
throw new SourceError("Invalid else tag", position);
|
28
28
|
}
|
29
29
|
const [_, ifTag, condition] = match;
|
30
30
|
if (ifTag) {
|
package/plugins/import.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(importTag);
|
@@ -9,13 +9,13 @@ const DEFAULT_IMPORT = /^\b[a-zA-Z_]\w*\b$/i;
|
|
9
9
|
const NAMED_IMPORTS = /^{[^]*?}$/;
|
10
10
|
const AS = /\s+\bas\b\s+/;
|
11
11
|
function importTag(env, token) {
|
12
|
-
const [, code] = token;
|
12
|
+
const [, code, position] = token;
|
13
13
|
if (!code.startsWith("import ")) {
|
14
14
|
return;
|
15
15
|
}
|
16
16
|
const match = code.match(IMPORT_STATEMENT);
|
17
17
|
if (!match) {
|
18
|
-
throw new
|
18
|
+
throw new SourceError("Invalid import tag", position);
|
19
19
|
}
|
20
20
|
const compiled = [];
|
21
21
|
const variables = [];
|
@@ -43,18 +43,18 @@ function importTag(env, token) {
|
|
43
43
|
return `${name}: ${rename}`;
|
44
44
|
}
|
45
45
|
else {
|
46
|
-
throw new
|
46
|
+
throw new SourceError("Invalid named import", position);
|
47
47
|
}
|
48
48
|
});
|
49
49
|
compiled.push(`({${chunks.join(",")}} = __tmp);`);
|
50
50
|
}
|
51
51
|
else {
|
52
|
-
throw new
|
52
|
+
throw new SourceError("Invalid import tag", position);
|
53
53
|
}
|
54
54
|
}
|
55
55
|
const { dataVarname } = env.options;
|
56
56
|
return `let ${variables.join(",")}; {
|
57
|
-
let __tmp = await __env.run(${specifier}, {...${dataVarname}}, __template.path);
|
57
|
+
let __tmp = await __env.run(${specifier}, {...${dataVarname}}, __template.path, ${position});
|
58
58
|
${compiled.join("\n")}
|
59
59
|
}`;
|
60
60
|
}
|
package/plugins/include.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
import iterateTopLevel from "../core/js.js";
|
3
3
|
export default function () {
|
4
4
|
return (env) => {
|
@@ -6,7 +6,7 @@ export default function () {
|
|
6
6
|
};
|
7
7
|
}
|
8
8
|
function includeTag(env, token, output, tokens) {
|
9
|
-
const [, code] = token;
|
9
|
+
const [, code, position] = token;
|
10
10
|
if (!code.startsWith("include ")) {
|
11
11
|
return;
|
12
12
|
}
|
@@ -21,7 +21,7 @@ function includeTag(env, token, output, tokens) {
|
|
21
21
|
bracketIndex = index;
|
22
22
|
}
|
23
23
|
if (bracketIndex == -1) {
|
24
|
-
throw new
|
24
|
+
throw new SourceError("Invalid include tag", position);
|
25
25
|
}
|
26
26
|
file = tagCode.slice(0, bracketIndex).trim();
|
27
27
|
data = tagCode.slice(bracketIndex).trim();
|
@@ -30,7 +30,8 @@ function includeTag(env, token, output, tokens) {
|
|
30
30
|
return `{
|
31
31
|
const __tmp = await __env.run(${file},
|
32
32
|
{...${dataVarname}${data ? `, ...${data}` : ""}},
|
33
|
-
__template.path
|
33
|
+
__template.path,
|
34
|
+
${position}
|
34
35
|
);
|
35
36
|
${output} += ${env.compileFilters(tokens, "__tmp.content")};
|
36
37
|
}`;
|
package/plugins/layout.js
CHANGED
@@ -1,34 +1,50 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(layoutTag);
|
5
|
+
env.tags.push(slotTag);
|
5
6
|
};
|
6
7
|
}
|
8
|
+
const LAYOUT_TAG = /^layout\s+([^{]+|`[^`]+`)+(?:\{([^]*)\})?$/;
|
9
|
+
const SLOT_NAME = /^[a-z_]\w*$/i;
|
7
10
|
function layoutTag(env, token, output, tokens) {
|
8
|
-
const [, code] = token;
|
11
|
+
const [, code, position] = token;
|
9
12
|
if (!code.startsWith("layout ")) {
|
10
13
|
return;
|
11
14
|
}
|
12
|
-
const match = code?.match(
|
15
|
+
const match = code?.match(LAYOUT_TAG);
|
13
16
|
if (!match) {
|
14
|
-
throw new
|
17
|
+
throw new SourceError("Invalid layout tag", position);
|
15
18
|
}
|
16
19
|
const [_, file, data] = match;
|
17
|
-
const
|
18
|
-
? output + "_layout"
|
19
|
-
: "__layout";
|
20
|
-
const compiled = [];
|
21
|
-
const compiledFilters = env.compileFilters(tokens, varname);
|
22
|
-
compiled.push("{");
|
23
|
-
compiled.push(`let ${varname} = "";`);
|
24
|
-
compiled.push(...env.compileTokens(tokens, varname, "/layout"));
|
25
|
-
compiled.push(`${varname} = ${compiledFilters};`);
|
20
|
+
const compiledFilters = env.compileFilters(tokens, "__slots.content");
|
26
21
|
const { dataVarname } = env.options;
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
);
|
31
|
-
${
|
32
|
-
|
33
|
-
|
22
|
+
return `${output} += (await (async () => {
|
23
|
+
const __slots = { content: "" };
|
24
|
+
${env.compileTokens(tokens, "__slots.content", "/layout").join("\n")}
|
25
|
+
__slots.content = __env.utils.safeString(${compiledFilters});
|
26
|
+
return __env.run(${file}, {
|
27
|
+
...${dataVarname},
|
28
|
+
...__slots,
|
29
|
+
${data ?? ""}
|
30
|
+
}, __template.path, ${position});
|
31
|
+
})()).content;`;
|
32
|
+
}
|
33
|
+
function slotTag(env, token, _output, tokens) {
|
34
|
+
const [, code, position] = token;
|
35
|
+
if (!code.startsWith("slot ")) {
|
36
|
+
return;
|
37
|
+
}
|
38
|
+
const name = code.slice(4).trim();
|
39
|
+
if (!SLOT_NAME.test(name)) {
|
40
|
+
throw new SourceError(`Invalid slot name "${name}"`, position);
|
41
|
+
}
|
42
|
+
const compiledFilters = env.compileFilters(tokens, "__tmp");
|
43
|
+
return `{
|
44
|
+
let __tmp = '';
|
45
|
+
${env.compileTokens(tokens, "__tmp", "/slot").join("\n")}
|
46
|
+
__slots.${name} ??= '';
|
47
|
+
__slots.${name} += ${compiledFilters};
|
48
|
+
__slots.${name} = __env.utils.safeString(__slots.${name});
|
49
|
+
}`;
|
34
50
|
}
|
package/plugins/set.js
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
import {
|
1
|
+
import { SourceError } from "../core/errors.js";
|
2
2
|
export default function () {
|
3
3
|
return (env) => {
|
4
4
|
env.tags.push(setTag);
|
5
5
|
};
|
6
6
|
}
|
7
7
|
function setTag(env, token, _output, tokens) {
|
8
|
-
const [, code] = token;
|
8
|
+
const [, code, position] = token;
|
9
9
|
if (!code.startsWith("set ")) {
|
10
10
|
return;
|
11
11
|
}
|
@@ -15,7 +15,7 @@ function setTag(env, token, _output, tokens) {
|
|
15
15
|
if (expression.includes("=")) {
|
16
16
|
const match = code.match(/^set\s+([\w]+)\s*=\s*([\s\S]+)$/);
|
17
17
|
if (!match) {
|
18
|
-
throw new
|
18
|
+
throw new SourceError("Invalid set tag", position);
|
19
19
|
}
|
20
20
|
const [, variable, value] = match;
|
21
21
|
const val = env.compileFilters(tokens, value);
|
@@ -8,7 +8,6 @@ export interface TemplateContext {
|
|
8
8
|
code: string;
|
9
9
|
path?: string;
|
10
10
|
defaults?: Record<string, unknown>;
|
11
|
-
tokens?: Token[];
|
12
11
|
}
|
13
12
|
export interface Template extends TemplateContext {
|
14
13
|
(data?: Record<string, unknown>): Promise<TemplateResult>;
|
@@ -45,11 +44,11 @@ export declare class Environment {
|
|
45
44
|
utils: Record<string, unknown>;
|
46
45
|
constructor(options: Options);
|
47
46
|
use(plugin: Plugin): void;
|
48
|
-
run(file: string, data?: Record<string, unknown>, from?: string): Promise<TemplateResult>;
|
47
|
+
run(file: string, data?: Record<string, unknown>, from?: string, position?: number): Promise<TemplateResult>;
|
49
48
|
runString(source: string, data?: Record<string, unknown>, file?: string): Promise<TemplateResult>;
|
50
49
|
compile(source: string, path?: string, defaults?: Record<string, unknown>): Template;
|
51
50
|
tokenize(source: string, path?: string): Token[];
|
52
|
-
load(file: string, from?: string): Promise<Template>;
|
51
|
+
load(file: string, from?: string, position?: number): Promise<Template>;
|
53
52
|
compileTokens(tokens: Token[], outputVar?: string, closeToken?: string): string[];
|
54
53
|
compileFilters(tokens: Token[], output: string, autoescape?: boolean): string;
|
55
54
|
}
|
package/types/core/errors.d.ts
CHANGED
@@ -1,38 +1,46 @@
|
|
1
|
-
import type { Token } from "./tokenizer.d.ts";
|
2
1
|
import type { TemplateContext } from "./environment.d.ts";
|
3
2
|
export interface ErrorContext {
|
4
3
|
type: string;
|
5
4
|
message: string;
|
6
|
-
source
|
7
|
-
position
|
5
|
+
source?: string;
|
6
|
+
position?: number;
|
7
|
+
code?: string;
|
8
|
+
line?: number;
|
9
|
+
column?: number;
|
8
10
|
file?: string;
|
9
11
|
}
|
10
12
|
export declare abstract class VentoError extends Error {
|
11
|
-
abstract getContext(): ErrorContext |
|
13
|
+
abstract getContext(): ErrorContext | Promise<ErrorContext>;
|
12
14
|
}
|
13
|
-
export declare class
|
14
|
-
|
15
|
-
source?: string;
|
15
|
+
export declare class SourceError extends VentoError {
|
16
|
+
position?: number;
|
16
17
|
file?: string;
|
17
|
-
|
18
|
+
source?: string;
|
19
|
+
constructor(message: string, position?: number, file?: string, source?: string);
|
18
20
|
getContext(): {
|
19
21
|
type: string;
|
20
22
|
message: string;
|
21
|
-
source: string;
|
22
23
|
position: number;
|
23
24
|
file: string;
|
25
|
+
source: string;
|
24
26
|
};
|
25
27
|
}
|
26
28
|
export declare class RuntimeError extends VentoError {
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
#private;
|
30
|
+
position?: number;
|
31
|
+
constructor(error: Error, context: TemplateContext, position?: number);
|
32
|
+
getContext(): Promise<ErrorContext>;
|
30
33
|
}
|
31
|
-
|
32
|
-
export declare function
|
34
|
+
/** Create or complete VentoError with extra info from the template */
|
35
|
+
export declare function createError(error: Error, context: TemplateContext, position?: number): VentoError;
|
33
36
|
export interface ErrorFormat {
|
34
37
|
number: (n: string) => string;
|
35
38
|
dim: (line: string) => string;
|
36
39
|
error: (msg: string) => string;
|
37
40
|
}
|
38
|
-
|
41
|
+
declare const formats: Record<string, ErrorFormat>;
|
42
|
+
/** Prints an error to the console in a formatted way. */
|
43
|
+
export declare function printError(error: unknown, format?: ErrorFormat | keyof typeof formats): Promise<void>;
|
44
|
+
/** Converts an error context into a formatted string representation. */
|
45
|
+
export declare function stringifyError(context: ErrorContext, format?: ErrorFormat): string;
|
46
|
+
export {};
|
@@ -5,15 +5,13 @@ import type { Loader, PrecompiledTemplate, Template } from "../core/environment.
|
|
5
5
|
*/
|
6
6
|
export declare class ModuleLoader implements Loader {
|
7
7
|
#private;
|
8
|
-
constructor(root: URL);
|
8
|
+
constructor(root: URL, extension?: string);
|
9
9
|
load(file: string): Promise<PrecompiledTemplate>;
|
10
10
|
resolve(from: string, file: string): string;
|
11
|
+
/**
|
12
|
+
* Outputs a template as a string that can be used in an ES module.
|
13
|
+
* This is useful for precompiled templates.
|
14
|
+
* @returns A tuple with the path and the content of the module.
|
15
|
+
*/
|
16
|
+
output(template: Template, source?: boolean): [string, string];
|
11
17
|
}
|
12
|
-
export interface ExportOptions {
|
13
|
-
source?: boolean;
|
14
|
-
}
|
15
|
-
/**
|
16
|
-
* Exports a template as a string that can be used in an ES module.
|
17
|
-
* This is useful for precompiled templates.
|
18
|
-
*/
|
19
|
-
export declare function exportTemplate(template: Template, options?: ExportOptions): string;
|