ventojs 2.0.0-canary.0 → 2.0.0-canary.1

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 CHANGED
@@ -5,20 +5,45 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
7
  ## 2.0.0 - Unreleased
8
- Vento 2.0 is now dependency-free and compatible with browsers without requiring a build process.
8
+ Vento 2.0 is now dependency-free and compatible with browsers without a build step.
9
9
 
10
10
  ### Added
11
11
  - Build-less browser support.
12
12
  - `plugins/mod.ts` module to register all default plugins easily.
13
+ - Support for precompiled templates.
14
+ - New filesystem loader to use File System API.
15
+ - Better errors reporting [#131], [#137]
13
16
 
14
17
  ### Changed
15
18
  - Renamed `src` directory to `core`.
16
19
  - Moved all loaders to the `loaders` root directory.
17
- - Implemented a different approach to resolve the variables without using `meriyah` to analyze the code. [#128]
20
+ - Implemented a different approach to resolve the variables without using `meriyah` to analyze the code [#128].
21
+ - The signature of `tag` plugins has changed:
22
+ ```diff
23
+ -- (env: Environment, code: string, output: string, tokens: Tokens[])
24
+ ++ (env: Environment, token: Token, output: string, tokens: Tokens[])
25
+ ```
26
+ - The `compileTokens` function has changed. The third argument is a string with the closing tag and now it throws an error if its not found:
27
+ ```diff
28
+ -- env.compileTokens(tokens, tmpOutput, ["/code"]);
29
+ -- if (tokens.length && (tokens[0][0] !== "tag" || tokens[0][1] !== "/code")) {
30
+ -- throw new Error("missing closing tag");
31
+ -- }
32
+ ++ env.compileTokens(tokens, tmpOutput, "/code");
33
+ ```
18
34
 
19
35
  ### Removed
36
+ - `runStringSync` function.
20
37
  - Deprecated option `useWith`.
21
38
  - All extenal dependencies (`meriyah`, `estree`, etc).
22
39
  - `bare.ts` file since now it's useless.
23
40
 
41
+ ### Fixed
42
+ - Functions output when the autoescape is enabled [#95]
43
+ - Improved escape filter performance [#134]
44
+
45
+ [#95]: https://github.com/ventojs/vento/issues/95
24
46
  [#128]: https://github.com/ventojs/vento/issues/128
47
+ [#131]: https://github.com/ventojs/vento/issues/131
48
+ [#134]: https://github.com/ventojs/vento/issues/134
49
+ [#137]: https://github.com/ventojs/vento/issues/137
@@ -1,6 +1,6 @@
1
1
  import iterateTopLevel from "./js.js";
2
2
  import tokenize from "./tokenizer.js";
3
- import { TemplateError } from "./errors.js";
3
+ import { createError, TokenError } from "./errors.js";
4
4
  export class Environment {
5
5
  cache = new Map();
6
6
  options;
@@ -9,6 +9,10 @@ export class Environment {
9
9
  filters = {};
10
10
  utils = {
11
11
  callMethod,
12
+ createError,
13
+ safeString(str) {
14
+ return new SafeString(str);
15
+ },
12
16
  };
13
17
  constructor(options) {
14
18
  this.options = options;
@@ -33,16 +37,30 @@ export class Environment {
33
37
  const template = this.compile(source, file);
34
38
  return await template(data);
35
39
  }
36
- runStringSync(source, data) {
37
- const template = this.compile(source, "", {}, true);
38
- return template(data);
39
- }
40
- compile(source, path, defaults, sync = false) {
40
+ compile(source, path, defaults) {
41
41
  if (typeof source !== "string") {
42
- throw new Error(`The source code of "${path}" must be a string. Got ${typeof source}`);
42
+ throw new TypeError(`The source code of "${path}" must be a string. Got ${typeof source}`);
43
+ }
44
+ const allTokens = this.tokenize(source, path);
45
+ const tokens = [...allTokens];
46
+ const lastToken = tokens.at(-1);
47
+ if (lastToken[0] != "string") {
48
+ throw new TokenError("Unclosed tag", lastToken, source, path);
49
+ }
50
+ let code = "";
51
+ try {
52
+ code = this.compileTokens(tokens).join("\n");
53
+ }
54
+ catch (error) {
55
+ if (!(error instanceof Error))
56
+ throw error;
57
+ throw createError(error, {
58
+ source,
59
+ code,
60
+ tokens: allTokens,
61
+ path,
62
+ });
43
63
  }
44
- const tokens = this.tokenize(source, path);
45
- let code = this.compileTokens(tokens).join("\n");
46
64
  const { dataVarname, autoDataVarname } = this.options;
47
65
  if (autoDataVarname) {
48
66
  const generator = iterateTopLevel(code);
@@ -57,24 +75,35 @@ export class Environment {
57
75
  `;
58
76
  }
59
77
  }
60
- const constructor = new Function("__file", "__env", "__defaults", "__err", `return${sync ? "" : " async"} function (${dataVarname}) {
61
- let __pos = 0;
62
78
  try {
63
- ${dataVarname} = Object.assign({}, __defaults, ${dataVarname});
64
- const __exports = { content: "" };
65
- ${code}
66
- return __exports;
67
- } catch (cause) {
68
- const template = ${sync ? "" : "await"} __env.cache.get(__file);
69
- throw new __err(__file, template?.source, __pos, cause);
79
+ const constructor = new Function("__env", `return async function __template(${dataVarname}) {
80
+ try {
81
+ ${dataVarname} = Object.assign({}, __template.defaults, ${dataVarname});
82
+ const __exports = { content: "" };
83
+ ${code}
84
+ return __exports;
85
+ } catch (error) {
86
+ throw __env.utils.createError(error, __template);
87
+ }
88
+ }`);
89
+ const template = constructor(this);
90
+ template.path = path;
91
+ template.code = constructor.toString();
92
+ template.source = source;
93
+ template.tokens = allTokens;
94
+ template.defaults = defaults || {};
95
+ return template;
96
+ }
97
+ catch (error) {
98
+ if (!(error instanceof Error))
99
+ throw error;
100
+ throw createError(error, {
101
+ source,
102
+ code,
103
+ tokens: allTokens,
104
+ path,
105
+ });
70
106
  }
71
- }
72
- `);
73
- const template = constructor(path, this, defaults, TemplateError);
74
- template.file = path;
75
- template.code = code;
76
- template.source = source;
77
- return template;
78
107
  }
79
108
  tokenize(source, path) {
80
109
  let tokens = tokenize(source);
@@ -97,17 +126,27 @@ export class Environment {
97
126
  .split("?")[0]
98
127
  .split("#")[0];
99
128
  cached = this.options.loader.load(cleanPath)
100
- .then(({ source, data }) => this.compile(source, path, data));
129
+ .then((result) => {
130
+ if (typeof result === "function") {
131
+ return result(this);
132
+ }
133
+ const { source, data } = result;
134
+ return this.compile(source, path, data);
135
+ });
101
136
  this.cache.set(path, cached);
102
137
  return await cached;
103
138
  }
104
- compileTokens(tokens, outputVar = "__exports.content", stopAt) {
139
+ compileTokens(tokens, outputVar = "__exports.content", closeToken) {
105
140
  const compiled = [];
141
+ let openToken;
106
142
  tokens: while (tokens.length > 0) {
107
- if (stopAt && tokens[0][0] === "tag" && stopAt.includes(tokens[0][1])) {
108
- break;
143
+ const token = tokens.shift();
144
+ const [type, code, pos] = token;
145
+ openToken ??= token;
146
+ // We found the closing tag, so we stop compiling
147
+ if (closeToken && type === "tag" && closeToken === code) {
148
+ return compiled;
109
149
  }
110
- const [type, code, pos] = tokens.shift();
111
150
  if (type === "comment") {
112
151
  continue;
113
152
  }
@@ -118,9 +157,9 @@ export class Environment {
118
157
  continue;
119
158
  }
120
159
  if (type === "tag") {
121
- compiled.push(`__pos = ${pos};`);
160
+ compiled.push(`/*__pos:${pos}*/`);
122
161
  for (const tag of this.tags) {
123
- const compiledTag = tag(this, code, outputVar, tokens);
162
+ const compiledTag = tag(this, token, outputVar, tokens);
124
163
  if (typeof compiledTag === "string") {
125
164
  compiled.push(compiledTag);
126
165
  continue tokens;
@@ -131,17 +170,22 @@ export class Environment {
131
170
  compiled.push(`${outputVar} += (${expression}) ?? "";`);
132
171
  continue;
133
172
  }
134
- throw new Error(`Unknown token type "${type}"`);
173
+ throw new TokenError(`Unknown token type "${type}"`, token);
174
+ }
175
+ // If we reach here, it means we have an open token that wasn't closed
176
+ if (closeToken) {
177
+ throw new TokenError(`Missing closing tag ("${closeToken}" tag is expected)`, openToken);
135
178
  }
136
179
  return compiled;
137
180
  }
138
181
  compileFilters(tokens, output, autoescape = false) {
139
182
  let unescaped = false;
140
183
  while (tokens.length > 0 && tokens[0][0] === "filter") {
141
- const [, code] = tokens.shift();
184
+ const token = tokens.shift();
185
+ const [, code, position] = token;
142
186
  const match = code.match(/^(await\s+)?([\w.]+)(?:\((.*)\))?$/);
143
187
  if (!match) {
144
- throw new Error(`Invalid filter: ${code}`);
188
+ throw new TokenError(`Invalid filter: ${code}`, token);
145
189
  }
146
190
  const [_, isAsync, name, args] = match;
147
191
  if (!Object.hasOwn(this.filters, name)) {
@@ -154,7 +198,7 @@ export class Environment {
154
198
  }
155
199
  else {
156
200
  // It's a prototype's method (e.g. `String.toUpperCase()`)
157
- output = `${isAsync ? "await " : ""}__env.utils.callMethod(${output}, "${name}", ${args ? args : ""})`;
201
+ output = `${isAsync ? "await " : ""}__env.utils.callMethod(${position}, ${output}, "${name}", ${args ? args : ""})`;
158
202
  }
159
203
  }
160
204
  else {
@@ -182,16 +226,19 @@ function isGlobal(name) {
182
226
  return Object.hasOwn(globalThis[obj], prop);
183
227
  }
184
228
  }
229
+ function callMethod(position,
185
230
  // deno-lint-ignore no-explicit-any
186
- function callMethod(thisObject, method, ...args) {
231
+ thisObject, method, ...args) {
187
232
  if (thisObject === null || thisObject === undefined) {
188
233
  return thisObject;
189
234
  }
190
235
  if (typeof thisObject[method] === "function") {
191
236
  return thisObject[method](...args);
192
237
  }
193
- throw new Error(`"${method}" is not a valid filter, global object or a method of a ${typeof thisObject} variable`);
238
+ throw new TokenError(`Method "${method}" is not a function of ${typeof thisObject} variable`, position);
194
239
  }
195
240
  function checkAsync(fn) {
196
241
  return fn.constructor?.name === "AsyncFunction";
197
242
  }
243
+ export class SafeString extends String {
244
+ }
package/core/errors.js CHANGED
@@ -1,20 +1,168 @@
1
- class VentoBaseError extends Error {
2
- name = this.constructor.name;
1
+ export class VentoError extends Error {
3
2
  }
4
- export class TemplateError extends VentoBaseError {
5
- path;
3
+ export class TokenError extends VentoError {
4
+ token;
6
5
  source;
7
- position;
8
- constructor(path = "<unknown>", source = "<empty file>", position = 0, cause) {
9
- const { line, column, code } = errorLine(source, position);
10
- super(`Error in template ${path}:${line}:${column}\n\n${code.trim()}\n\n`, { cause });
11
- this.path = path;
6
+ file;
7
+ constructor(message, token, source, file) {
8
+ super(message);
9
+ this.name = "TokenError";
10
+ this.token = token;
12
11
  this.source = source;
13
- this.position = position;
12
+ this.file = file;
14
13
  }
14
+ getContext() {
15
+ if (!this.source || this.token === undefined) {
16
+ return;
17
+ }
18
+ return {
19
+ type: this.name,
20
+ message: this.message,
21
+ source: this.source,
22
+ position: typeof this.token === "number" ? this.token : this.token[2],
23
+ file: this.file,
24
+ };
25
+ }
26
+ }
27
+ export class RuntimeError extends VentoError {
28
+ context;
29
+ constructor(error, context) {
30
+ super(error.message);
31
+ this.name = error.name || "JavaScriptError";
32
+ this.context = context;
33
+ this.cause = error;
34
+ }
35
+ getContext() {
36
+ if (this.cause instanceof SyntaxError) {
37
+ return parseSyntaxError(this.cause, this.context);
38
+ }
39
+ if (this.cause instanceof Error) {
40
+ return parseError(this.cause, this.context);
41
+ }
42
+ }
43
+ }
44
+ export function createError(error, context) {
45
+ if (error instanceof RuntimeError)
46
+ return error;
47
+ // If the error is a TokenError, we can enhance it with the context information
48
+ if (error instanceof TokenError) {
49
+ error.file ??= context.path;
50
+ error.source ??= context.source;
51
+ return error;
52
+ }
53
+ // JavaScript syntax errors can be parsed to get accurate position
54
+ return new RuntimeError(error, context);
55
+ }
56
+ export async function printError(error) {
57
+ if (error instanceof VentoError) {
58
+ const context = await error.getContext();
59
+ if (context) {
60
+ console.error(stringifyContext(context));
61
+ return;
62
+ }
63
+ }
64
+ console.error(error);
65
+ }
66
+ function parseError(error, context) {
67
+ const stackMatch = error.stack?.match(/<anonymous>:(\d+):(\d+)/);
68
+ if (!stackMatch)
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}`);
157
+ const output = [];
158
+ output.push(`${format.error(type)}: ${message}`);
159
+ if (file) {
160
+ output.push(format.dim(getLocation(file, source, position)), "");
161
+ }
162
+ output.push(displayedCode, tooltip);
163
+ return output.join("\n");
15
164
  }
16
- /** Returns the number and code of the errored line */
17
- export function errorLine(source, position) {
165
+ function getLocation(file, source, position) {
18
166
  let line = 1;
19
167
  let column = 1;
20
168
  for (let index = 0; index < position; index++) {
@@ -30,5 +178,5 @@ export function errorLine(source, position) {
30
178
  column++;
31
179
  }
32
180
  }
33
- return { line, column, code: source.split("\n")[line - 1] };
181
+ return `${file}:${line}:${column}`;
34
182
  }
package/core/reserved.js CHANGED
@@ -2,6 +2,7 @@ const variables = new Set([
2
2
  // Words reserved by Vento, used internally. In general, don't use variable
3
3
  // names starting with two underscores to be future-proof and avoid clashes.
4
4
  "__file",
5
+ "__template",
5
6
  "__env",
6
7
  "__defaults",
7
8
  "__err",
package/core/tokenizer.js CHANGED
@@ -8,12 +8,12 @@ export default function tokenize(source) {
8
8
  const index = source.indexOf("{{");
9
9
  const code = index === -1 ? source : source.slice(0, index);
10
10
  tokens.push([type, code, position]);
11
- if (index === -1) {
12
- break;
13
- }
14
11
  position += index;
15
12
  source = source.slice(index);
16
13
  type = source.startsWith("{{#") ? "comment" : "tag";
14
+ if (index === -1) {
15
+ break;
16
+ }
17
17
  continue;
18
18
  }
19
19
  if (type === "comment") {
@@ -42,9 +42,11 @@ export default function tokenize(source) {
42
42
  return curr;
43
43
  }
44
44
  // Filters
45
- tokens.push(["filter", code]);
45
+ tokens.push(["filter", code, position + prev]);
46
46
  return curr;
47
47
  });
48
+ if (indexes[lastIndex] == Infinity)
49
+ return tokens;
48
50
  position += indexes[lastIndex];
49
51
  source = source.slice(indexes[lastIndex]);
50
52
  type = "string";
@@ -64,6 +66,9 @@ export default function tokenize(source) {
64
66
  continue;
65
67
  }
66
68
  }
69
+ if (type == "string") {
70
+ tokens.push([type, "", position]);
71
+ }
67
72
  return tokens;
68
73
  }
69
74
  /**
@@ -82,5 +87,6 @@ export function parseTag(source) {
82
87
  indexes.push(index + 2);
83
88
  return indexes;
84
89
  }
85
- throw new Error("Unclosed tag");
90
+ indexes.push(Infinity);
91
+ return indexes;
86
92
  }
package/loaders/file.js CHANGED
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import process from "node:process";
4
4
  /**
5
5
  * Vento file loader for loading templates from the file system.
6
- * Used by JS runtimes like Node, Deno, and Bun.
6
+ * Used by Node-like runtimes (Node, Deno, Bun, ...)
7
7
  */
8
8
  export class FileLoader {
9
9
  #root;
@@ -0,0 +1,36 @@
1
+ import { join } from "./utils.js";
2
+ /**
3
+ * Vento FileSystem API loader for loading templates.
4
+ * Used by browser environments.
5
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/File_System_API
6
+ */
7
+ export class FileSystemLoader {
8
+ #handle;
9
+ constructor(handle) {
10
+ this.#handle = handle;
11
+ }
12
+ async load(file) {
13
+ const parts = file.split("/");
14
+ let currentHandle = this.#handle;
15
+ while (parts.length > 1) {
16
+ const part = parts.shift();
17
+ if (part) {
18
+ currentHandle = await currentHandle.getDirectoryHandle(part, {
19
+ create: false,
20
+ });
21
+ }
22
+ }
23
+ const entry = await currentHandle.getFileHandle(parts[0], {
24
+ create: false,
25
+ });
26
+ const fileHandle = await entry.getFile();
27
+ const source = await fileHandle.text();
28
+ return { source };
29
+ }
30
+ resolve(from, file) {
31
+ if (file.startsWith(".")) {
32
+ return join(from, "..", file);
33
+ }
34
+ return join(file);
35
+ }
36
+ }
package/loaders/memory.js CHANGED
@@ -1,24 +1,24 @@
1
- import path from "node:path";
1
+ import { join } from "./utils.js";
2
2
  /**
3
3
  * Vento loader for loading templates from an in-memory object.
4
4
  * Used for testing or in-memory operations.
5
5
  */
6
6
  export class MemoryLoader {
7
- files = {};
7
+ files;
8
8
  constructor(files) {
9
- this.files = files;
9
+ this.files = new Map(Object.entries(files));
10
10
  }
11
11
  load(file) {
12
- if (!(file in this.files)) {
12
+ const source = this.files.get(file);
13
+ if (source === undefined) {
13
14
  throw new Error(`File not found: ${file}`);
14
15
  }
15
- const source = this.files[file];
16
16
  return Promise.resolve({ source });
17
17
  }
18
18
  resolve(from, file) {
19
19
  if (file.startsWith(".")) {
20
- return path.join(path.dirname(from), file).replace(/\\/g, "/");
20
+ return join(from, "..", file);
21
21
  }
22
- return path.join("/", file).replace(/\\/g, "/");
22
+ return join(file);
23
23
  }
24
24
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Vento loader for loading templates from a ES modules.
3
+ * Used to load precompiled templates.
4
+ */
5
+ export class ModuleLoader {
6
+ #root;
7
+ constructor(root) {
8
+ this.#root = root;
9
+ }
10
+ async load(file) {
11
+ const url = new URL(join(this.#root.pathname, file + ".js"), this.#root);
12
+ const module = await import(url.toString());
13
+ return module.default;
14
+ }
15
+ resolve(from, file) {
16
+ if (file.startsWith(".")) {
17
+ return join("/", dirname(from), file);
18
+ }
19
+ return join("/", file);
20
+ }
21
+ }
22
+ function join(...parts) {
23
+ return parts.join("/").replace(/\/+/g, "/");
24
+ }
25
+ function dirname(path) {
26
+ const lastSlash = path.lastIndexOf("/");
27
+ return lastSlash === -1 ? "." : path.slice(0, lastSlash);
28
+ }
29
+ /**
30
+ * Exports a template as a string that can be used in an ES module.
31
+ * This is useful for precompiled templates.
32
+ */
33
+ export function exportTemplate(template, options) {
34
+ if (!template.source) {
35
+ throw new Error("Template source is not defined");
36
+ }
37
+ const exportCode = `export default function (__env) {
38
+ ${template.toString()};
39
+
40
+ ${options?.source
41
+ ? `__template.path = ${JSON.stringify(template.path)};
42
+ __template.code = ${JSON.stringify(template.code)};
43
+ __template.source = ${JSON.stringify(template.source)};`
44
+ : ""}
45
+ __template.defaults = ${JSON.stringify(template.defaults || {})};
46
+
47
+ return __template;
48
+ }`;
49
+ return exportCode;
50
+ }
package/loaders/url.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { join } from "./utils.js";
1
2
  /**
2
3
  * Vento URL loader for loading templates from a URL.
3
4
  * Used by browser environments.
@@ -14,15 +15,8 @@ export class UrlLoader {
14
15
  }
15
16
  resolve(from, file) {
16
17
  if (file.startsWith(".")) {
17
- return join("/", dirname(from), file);
18
+ return join(from, "..", file);
18
19
  }
19
- return join("/", file);
20
+ return join(file);
20
21
  }
21
22
  }
22
- function join(...parts) {
23
- return parts.join("/").replace(/\/+/g, "/");
24
- }
25
- function dirname(path) {
26
- const lastSlash = path.lastIndexOf("/");
27
- return lastSlash === -1 ? "." : path.slice(0, lastSlash);
28
- }
@@ -0,0 +1,17 @@
1
+ // Adapted from https://gist.github.com/creationix/7435851
2
+ export function join(...paths) {
3
+ const parts = []
4
+ .concat(...paths.map((path) => path.split("/")))
5
+ .filter((part) => part && part !== ".");
6
+ const newParts = [];
7
+ for (const part of parts) {
8
+ if (part === "..") {
9
+ newParts.pop();
10
+ }
11
+ else {
12
+ newParts.push(part);
13
+ }
14
+ }
15
+ newParts.unshift(""); // Ensure always a leading slash
16
+ return newParts.join("/");
17
+ }