@rhinostone/swig 2.0.1 → 2.2.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,279 @@
1
+ var utils = require('../utils');
2
+
3
+ /*!
4
+ * Makes a string safe for a regular expression. Mirrors lib/parser.js.
5
+ * @private
6
+ */
7
+ function escapeRegExp(str) {
8
+ return str.replace(/[\-\/\\\^$*+?.()|\[\]{}]/g, '\\$&');
9
+ }
10
+
11
+ /*!
12
+ * Build the splitter regex from the controls trio. Mirrors the regex that
13
+ * parser.parse() builds at parse-time so the pre-walker chunks the same
14
+ * way the real parser would.
15
+ * @private
16
+ */
17
+ function buildSplitter(controls) {
18
+ var anyChar = '[\\s\\S]*?',
19
+ varOpen = escapeRegExp(controls.varControls[0]),
20
+ varClose = escapeRegExp(controls.varControls[1]),
21
+ tagOpen = escapeRegExp(controls.tagControls[0]),
22
+ tagClose = escapeRegExp(controls.tagControls[1]),
23
+ cmtOpen = escapeRegExp(controls.cmtControls[0]),
24
+ cmtClose = escapeRegExp(controls.cmtControls[1]);
25
+ return new RegExp(
26
+ '(' +
27
+ tagOpen + anyChar + tagClose + '|' +
28
+ varOpen + anyChar + varClose + '|' +
29
+ cmtOpen + anyChar + cmtClose +
30
+ ')'
31
+ );
32
+ }
33
+
34
+ /*!
35
+ * Strip tag controls and optional whitespace-control markers from a tag
36
+ * chunk, returning the trimmed tag body (e.g. `extends "x.html"`).
37
+ * @private
38
+ */
39
+ function stripTagBody(chunk, tagOpen, tagClose) {
40
+ var body = chunk.substr(tagOpen.length, chunk.length - tagOpen.length - tagClose.length);
41
+ if (body.charAt(0) === '-') {
42
+ body = body.substr(1);
43
+ }
44
+ if (body.charAt(body.length - 1) === '-') {
45
+ body = body.substr(0, body.length - 1);
46
+ }
47
+ return body.replace(/^\s+|\s+$/g, '');
48
+ }
49
+
50
+ /**
51
+ * Scan template source for static `{% extends|include|import|from "..." %}`
52
+ * targets. Pure function; performs no I/O.
53
+ *
54
+ * The scanner mirrors the real parser's chunk-splitter so it agrees on
55
+ * chunk boundaries even under non-default control characters. Dynamic
56
+ * paths (`{% extends parent_var %}`) and tag bodies whose first token
57
+ * isn't a string literal are silently skipped — they remain on the sync
58
+ * path, which throws appropriately at parse time.
59
+ *
60
+ * @example
61
+ * preWalker.scan('{% extends "layout.html" %}{% include "x" %}', {
62
+ * varControls: ['{{', '}}'],
63
+ * tagControls: ['{%', '%}'],
64
+ * cmtControls: ['{#', '#}'],
65
+ * rawTag: 'raw',
66
+ * keywords: ['extends', 'include', 'import']
67
+ * });
68
+ * // => [
69
+ * // { kind: 'extends', path: 'layout.html' },
70
+ * // { kind: 'include', path: 'x' }
71
+ * // ]
72
+ *
73
+ * @param {string} source
74
+ * @param {object} opts
75
+ * @param {array} opts.varControls e.g. <code>['{{', '}}']</code>.
76
+ * @param {array} opts.tagControls e.g. <code>['{%', '%}']</code>.
77
+ * @param {array} opts.cmtControls e.g. <code>['{#', '#}']</code>.
78
+ * @param {string} opts.rawTag Tag name that opens verbatim regions
79
+ * (<code>raw</code> for native swig).
80
+ * @param {array} opts.keywords Keywords whose first quoted argument is
81
+ * a template path. Native swig:
82
+ * <code>['extends', 'include', 'import']</code>.
83
+ * @return {array} List of <code>{ kind, path }</code> entries.
84
+ */
85
+ exports.scan = function (source, opts) {
86
+ source = source.replace(/\r\n/g, '\n');
87
+
88
+ var splitter = buildSplitter(opts),
89
+ tagOpen = opts.tagControls[0],
90
+ tagClose = opts.tagControls[1],
91
+ rawTag = opts.rawTag,
92
+ endRawTag = 'end' + rawTag,
93
+ keywordRegex = new RegExp(
94
+ '^(' + opts.keywords.join('|') + ')\\s+["\\\']([^"\\\']+)["\\\']'
95
+ ),
96
+ chunks = source.split(splitter),
97
+ results = [],
98
+ inRaw = false,
99
+ i,
100
+ chunk,
101
+ body,
102
+ name,
103
+ m;
104
+
105
+ for (i = 0; i < chunks.length; i += 1) {
106
+ chunk = chunks[i];
107
+ if (typeof chunk !== 'string' || !chunk) {
108
+ continue;
109
+ }
110
+
111
+ if (!utils.startsWith(chunk, tagOpen) || !utils.endsWith(chunk, tagClose)) {
112
+ continue;
113
+ }
114
+
115
+ body = stripTagBody(chunk, tagOpen, tagClose);
116
+ name = body.split(/\s+/)[0];
117
+
118
+ if (name === rawTag) {
119
+ inRaw = true;
120
+ continue;
121
+ }
122
+ if (name === endRawTag) {
123
+ inRaw = false;
124
+ continue;
125
+ }
126
+ if (inRaw) {
127
+ continue;
128
+ }
129
+
130
+ m = keywordRegex.exec(body);
131
+ if (m) {
132
+ results.push({ kind: m[1], path: m[2] });
133
+ }
134
+ }
135
+
136
+ return results;
137
+ };
138
+
139
+ /**
140
+ * Walk the dependency graph asynchronously starting from <var>entryPath</var>.
141
+ *
142
+ * Repeatedly loads, scans, and resolves child template paths in parallel
143
+ * via the user's async loader, until the dep graph closes. Returns a
144
+ * Promise resolving to a populated <code>{ resolvedPath: source }</code>
145
+ * map suitable for backing a memory loader.
146
+ *
147
+ * Cycles in the graph are tolerated — once a path is in the map or
148
+ * pending, subsequent enqueue requests are dropped. The synchronous
149
+ * renderer's existing circular-extends guard handles cycles at parse
150
+ * time on the second pass.
151
+ *
152
+ * @example
153
+ * preWalker.walk('/abs/entry.html', userLoader, scanOpts).then(function (memMap) {
154
+ * // memMap = { '/abs/entry.html': '...', '/abs/layout.html': '...', ... }
155
+ * });
156
+ *
157
+ * @param {string} entryPath Resolved path of the entry template.
158
+ * @param {object} loader User loader. Must expose:
159
+ * <code>resolve(to, from)</code> (sync, returns
160
+ * string) and
161
+ * <code>load(id, cb)</code> (async, calls
162
+ * <code>cb(err, source)</code>).
163
+ * @param {object} scanOpts Pass-through to {@link scan}.
164
+ * @return {Promise} Resolves to the populated memory map.
165
+ */
166
+ exports.walk = function (entryPath, loader, scanOpts) {
167
+ var memMap = {};
168
+ var pending = {};
169
+
170
+ return new Promise(function (resolve, reject) {
171
+ var inFlight = 0;
172
+ var queue = [];
173
+ var hasError = false;
174
+
175
+ function enqueue(path) {
176
+ if (memMap.hasOwnProperty(path) || pending[path]) {
177
+ return;
178
+ }
179
+ pending[path] = true;
180
+ queue.push(path);
181
+ }
182
+
183
+ function drain() {
184
+ while (queue.length > 0 && !hasError) {
185
+ var path = queue.shift();
186
+ inFlight += 1;
187
+ startLoad(path);
188
+ }
189
+ if (inFlight === 0 && !hasError && queue.length === 0) {
190
+ resolve(memMap);
191
+ }
192
+ }
193
+
194
+ function startLoad(resolvedPath) {
195
+ loader.load(resolvedPath, function (err, src) {
196
+ if (hasError) {
197
+ return;
198
+ }
199
+ if (err) {
200
+ hasError = true;
201
+ reject(err);
202
+ return;
203
+ }
204
+ if (typeof src !== 'string') {
205
+ hasError = true;
206
+ reject(new Error('Async loader returned non-string source for "' + resolvedPath + '"'));
207
+ return;
208
+ }
209
+ memMap[resolvedPath] = src;
210
+
211
+ var targets;
212
+ try {
213
+ targets = exports.scan(src, scanOpts);
214
+ } catch (e) {
215
+ hasError = true;
216
+ reject(e);
217
+ return;
218
+ }
219
+
220
+ var i, resolvedChild;
221
+ for (i = 0; i < targets.length; i += 1) {
222
+ try {
223
+ resolvedChild = loader.resolve(targets[i].path, resolvedPath);
224
+ } catch (e) {
225
+ hasError = true;
226
+ reject(e);
227
+ return;
228
+ }
229
+ enqueue(resolvedChild);
230
+ }
231
+
232
+ inFlight -= 1;
233
+ drain();
234
+ });
235
+ }
236
+
237
+ enqueue(entryPath);
238
+ drain();
239
+ });
240
+ };
241
+
242
+ /**
243
+ * Build a sync memory wrapper around a pre-populated
244
+ * <code>{ resolvedPath: source }</code> map. Delegates <code>resolve</code>
245
+ * to the user loader so cache keys match what the pre-walker produced.
246
+ *
247
+ * @example
248
+ * var mem = preWalker.makeMemoryWrapper(userLoader, memMap);
249
+ * swig.options.loader = mem;
250
+ * swig.renderFile('/abs/entry.html', locals); // sync, hits memMap
251
+ *
252
+ * @param {object} userLoader Original async loader (used for resolve).
253
+ * @param {object} memMap Pre-populated source map.
254
+ * @return {object} A loader exposing <code>resolve</code> and
255
+ * <code>load</code>.
256
+ */
257
+ exports.makeMemoryWrapper = function (userLoader, memMap) {
258
+ return {
259
+ resolve: function (to, from) {
260
+ return userLoader.resolve(to, from);
261
+ },
262
+ load: function (id, cb) {
263
+ var src = memMap[id];
264
+ if (typeof src !== 'string') {
265
+ var err = new Error('Pre-walked map missing path: "' + id + '"');
266
+ if (cb) {
267
+ cb(err);
268
+ return;
269
+ }
270
+ throw err;
271
+ }
272
+ if (cb) {
273
+ cb(null, src);
274
+ return;
275
+ }
276
+ return src;
277
+ }
278
+ };
279
+ };
package/lib/filters.js CHANGED
@@ -117,26 +117,33 @@ exports["default"] = function (input, def) {
117
117
  * @param {string} [type='html'] If you pass the string js in as the type, output will be escaped so that it is safe for JavaScript execution.
118
118
  * @return {string} Escaped string.
119
119
  */
120
+ function escapeHtmlRest(ch) {
121
+ return ch === '<' ? '&lt;' : ch === '>' ? '&gt;' : ch === '"' ? '&quot;' : '&#39;';
122
+ }
123
+
120
124
  exports.escape = function (input, type) {
121
- var out = iterateFilter.apply(exports.escape, arguments),
122
- inp = input,
123
- i = 0,
124
- code;
125
+ var t, inp, out, i, code;
125
126
 
126
- if (out !== undefined) {
127
- return out;
127
+ if (input === null || input === undefined) {
128
+ return input;
128
129
  }
129
130
 
130
- if (typeof input !== 'string') {
131
+ t = typeof input;
132
+
133
+ if (t !== 'string') {
134
+ if (t === 'object') {
135
+ out = iterateFilter.apply(exports.escape, arguments);
136
+ if (out !== undefined) {
137
+ return out;
138
+ }
139
+ }
131
140
  return input;
132
141
  }
133
142
 
134
- out = '';
135
-
136
- switch (type) {
137
- case 'js':
138
- inp = inp.replace(/\\/g, '\\u005C');
139
- for (i; i < inp.length; i += 1) {
143
+ if (type === 'js') {
144
+ inp = input.replace(/\\/g, '\\u005C');
145
+ out = '';
146
+ for (i = 0; i < inp.length; i += 1) {
140
147
  code = inp.charCodeAt(i);
141
148
  if (code < 32) {
142
149
  code = code.toString(16).toUpperCase();
@@ -154,14 +161,10 @@ exports.escape = function (input, type) {
154
161
  .replace(/\=/g, '\\u003D')
155
162
  .replace(/-/g, '\\u002D')
156
163
  .replace(/;/g, '\\u003B');
157
-
158
- default:
159
- return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&amp;')
160
- .replace(/</g, '&lt;')
161
- .replace(/>/g, '&gt;')
162
- .replace(/"/g, '&quot;')
163
- .replace(/'/g, '&#39;');
164
164
  }
165
+
166
+ return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&amp;')
167
+ .replace(/[<>"']/g, escapeHtmlRest);
165
168
  };
166
169
  exports.e = exports.escape;
167
170
 
package/lib/swig.js CHANGED
@@ -4,16 +4,17 @@ var utils = require('./utils'),
4
4
  parser = require('./parser'),
5
5
  dateformatter = require('./dateformatter'),
6
6
  loaders = require('./loaders'),
7
+ preWalker = require('./async/pre-walker'),
7
8
  engine = require('@rhinostone/swig-core/lib/engine');
8
9
 
9
10
  /**
10
11
  * Swig version number as a string.
11
12
  * @example
12
- * if (swig.version === "2.0.1") { ... }
13
+ * if (swig.version === "2.2.0") { ... }
13
14
  *
14
15
  * @type {String}
15
16
  */
16
- exports.version = "2.0.1";
17
+ exports.version = "2.2.0";
17
18
 
18
19
  /**
19
20
  * Swig Options Object. This object can be passed to many of the API-level Swig methods to control various aspects of the engine. All keys are optional.
@@ -177,6 +178,158 @@ exports.Swig = function (opts) {
177
178
  utils.throwError(err, null, options.filename);
178
179
  }
179
180
  });
181
+
182
+ var self = this;
183
+
184
+ function buildScanOpts() {
185
+ return {
186
+ varControls: self.options.varControls,
187
+ tagControls: self.options.tagControls,
188
+ cmtControls: self.options.cmtControls,
189
+ rawTag: 'raw',
190
+ keywords: ['extends', 'include', 'import']
191
+ };
192
+ }
193
+
194
+ /**
195
+ * Render a template file asynchronously, supporting async loaders.
196
+ *
197
+ * Pre-walks <var>extends</var> / <var>include</var> / <var>import</var>
198
+ * targets in parallel via the user loader, populates an in-memory map,
199
+ * then runs the existing sync render pipeline against the populated map.
200
+ * Dynamic paths (e.g. <code>{% extends parent_var %}</code>) are not
201
+ * pre-resolved and will throw at render time as they would on the sync
202
+ * path.
203
+ *
204
+ * @deprecated since 2.2.0 — use {@link Swig#renderFile} with a loader that
205
+ * sets <code>loader.async === true</code>. The async-codegen dispatch
206
+ * handles dynamic include paths the pre-walker cannot. This method will
207
+ * be removed in 3.0.
208
+ *
209
+ * @example
210
+ * swig.setDefaults({ loader: myAsyncLoader });
211
+ * swig.renderFileAsync('page.html', { name: 'world' }, function (err, output) {
212
+ * if (err) { return done(err); }
213
+ * res.end(output);
214
+ * });
215
+ *
216
+ * @param {string} pathName Template path; resolved via the active loader.
217
+ * @param {object} [locals] Locals to render with.
218
+ * @param {Function} cb Node-style callback <code>(err, output)</code>.
219
+ * @return {undefined}
220
+ */
221
+ this.renderFileAsync = function (pathName, locals, cb) {
222
+ if (typeof locals === 'function') {
223
+ cb = locals;
224
+ locals = undefined;
225
+ }
226
+
227
+ var loader = self.options.loader;
228
+ var entry;
229
+
230
+ try {
231
+ entry = loader.resolve(pathName);
232
+ } catch (e) {
233
+ cb(e);
234
+ return;
235
+ }
236
+
237
+ preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
238
+ var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
239
+ var origLoader = self.options.loader;
240
+ self.options.loader = memWrapper;
241
+ var output, error;
242
+ try {
243
+ output = self.renderFile(entry, locals);
244
+ } catch (e) {
245
+ error = e;
246
+ }
247
+ self.options.loader = origLoader;
248
+ if (error) {
249
+ cb(error);
250
+ return;
251
+ }
252
+ cb(null, output);
253
+ }, function (err) {
254
+ cb(err);
255
+ });
256
+ };
257
+
258
+ /**
259
+ * Compile a template file asynchronously, supporting async loaders.
260
+ *
261
+ * Same pre-walk / memory-wrapper / sync-pipeline shape as
262
+ * {@link Swig#renderFileAsync}. Returns the compiled function (via
263
+ * <var>cb</var>) that takes a locals object and yields a rendered
264
+ * string. The returned function captures the pre-walked memory map and
265
+ * temporarily swaps the loader on each call, so subsequent runtime
266
+ * <var>include</var>s resolve correctly without re-running the pre-walk.
267
+ *
268
+ * @deprecated since 2.2.0 — use {@link Swig#compileFile} with
269
+ * <code>options.codegenMode === 'async'</code> on a loader that sets
270
+ * <code>loader.async === true</code>. The returned compiled function
271
+ * yields a <code>Promise&lt;{output, exports}&gt;</code> instead of a
272
+ * string. This method will be removed in 3.0.
273
+ *
274
+ * @example
275
+ * swig.compileFileAsync('page.html', {}, function (err, fn) {
276
+ * if (err) { return done(err); }
277
+ * res.end(fn({ name: 'world' }));
278
+ * });
279
+ *
280
+ * @param {string} pathName Template path.
281
+ * @param {object} [options] Compilation options.
282
+ * @param {Function} cb Node-style callback <code>(err, fn)</code>.
283
+ * @return {undefined}
284
+ */
285
+ this.compileFileAsync = function (pathName, options, cb) {
286
+ if (typeof options === 'function') {
287
+ cb = options;
288
+ options = {};
289
+ }
290
+
291
+ var loader = self.options.loader;
292
+ var entry;
293
+
294
+ try {
295
+ entry = loader.resolve(pathName);
296
+ } catch (e) {
297
+ cb(e);
298
+ return;
299
+ }
300
+
301
+ preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
302
+ var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
303
+ var origLoader = self.options.loader;
304
+ self.options.loader = memWrapper;
305
+ var compiled, error;
306
+ try {
307
+ compiled = self.compileFile(entry, options);
308
+ } catch (e) {
309
+ error = e;
310
+ }
311
+ self.options.loader = origLoader;
312
+ if (error) {
313
+ cb(error);
314
+ return;
315
+ }
316
+ var wrapped = function (locals) {
317
+ var origInner = self.options.loader;
318
+ self.options.loader = memWrapper;
319
+ try {
320
+ var output = compiled(locals);
321
+ self.options.loader = origInner;
322
+ return output;
323
+ } catch (e) {
324
+ self.options.loader = origInner;
325
+ throw e;
326
+ }
327
+ };
328
+ cb(null, wrapped);
329
+ }, function (err) {
330
+ cb(err);
331
+ });
332
+ };
180
333
  };
181
334
 
182
335
  /*!
@@ -190,8 +343,10 @@ exports.parseFile = defaultInstance.parseFile;
190
343
  exports.precompile = defaultInstance.precompile;
191
344
  exports.compile = defaultInstance.compile;
192
345
  exports.compileFile = defaultInstance.compileFile;
346
+ exports.compileFileAsync = defaultInstance.compileFileAsync;
193
347
  exports.render = defaultInstance.render;
194
348
  exports.renderFile = defaultInstance.renderFile;
349
+ exports.renderFileAsync = defaultInstance.renderFileAsync;
195
350
  exports.run = defaultInstance.run;
196
351
  exports.invalidateCache = defaultInstance.invalidateCache;
197
352
  exports.loaders = loaders;
@@ -1,4 +1,5 @@
1
1
  var utils = require('../utils'),
2
+ ir = require('@rhinostone/swig-core/lib/ir'),
2
3
  backend = require('@rhinostone/swig-core/lib/backend');
3
4
 
4
5
  // CVE-2023-25345: prototype-chain properties that must not be used as import
@@ -26,7 +27,19 @@ var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousPro
26
27
  * @param {literal} as Literally, "as".
27
28
  * @param {literal} varname Local-accessible object name to assign the macros to.
28
29
  */
29
- exports.compile = function (compiler, args) {
30
+ exports.compile = function (compiler, args, content, parents, options) {
31
+ // Phase 2 (#T22): async-codegen branch. Parse stashed `[{path}, alias]`
32
+ // (no macro pre-render in async mode); emit IRImportDeferred so the
33
+ // backend's `_swig.getTemplate` + `.exports` bind happens at runtime.
34
+ if (options && options.codegenMode === 'async') {
35
+ var asyncAlias = args[args.length - 1];
36
+ var asyncPath = args[0].path;
37
+ return ir.importDeferred(
38
+ ir.literal('string', asyncPath),
39
+ asyncAlias,
40
+ options.filename || ''
41
+ );
42
+ }
30
43
  var ctx = args.pop(),
31
44
  allMacros = utils.map(args, function (arg) {
32
45
  return arg.name;
@@ -56,37 +69,45 @@ exports.parse = function (str, line, parser, types, stack, opts, swig) {
56
69
  var compiler = require('../parser').compile,
57
70
  parseOpts = { resolveFrom: opts.filename },
58
71
  compileOpts = utils.extend({}, opts, parseOpts),
59
- tokens,
72
+ isAsync = !!(opts && opts.codegenMode === 'async'),
73
+ importPath,
60
74
  ctx;
61
75
 
62
76
  parser.on(types.STRING, function (token) {
63
77
  var self = this;
64
- if (!tokens) {
65
- tokens = swig.parseFile(token.match.replace(/^("|')|("|')$/g, ''), parseOpts).tokens;
66
- utils.each(tokens, function (token) {
67
- var out = '',
68
- macroName;
69
- if (!token || token.name !== 'macro' || !token.compile) {
70
- return;
71
- }
72
- macroName = token.args[0];
73
- // Phase 2 (#T15): macro.compile now returns an IRMacro node
74
- // rather than a JS source string. Render it through the shared
75
- // backend so import.js still gets the JS source it performs
76
- // regex-surgery on for namespace-prefixing. The +'\n' trailing
77
- // newline matches the pre-Phase-2 compile output exactly.
78
- out += backend.compile([token.compile(compiler, token.args, token.content, [], compileOpts)], [], compileOpts) + '\n';
79
- self.out.push({compiled: out, name: macroName});
80
- });
78
+ if (importPath !== undefined) {
79
+ throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
80
+ }
81
+ importPath = token.match.replace(/^("|')|("|')$/g, '');
82
+
83
+ if (isAsync) {
84
+ // Async mode: skip the sync parseFile + macro pre-render. Stash
85
+ // just the path; compile() emits IRImportDeferred.
86
+ self.out.push({ path: importPath });
81
87
  return;
82
88
  }
83
89
 
84
- throw new Error('Unexpected string ' + token.match + ' on line ' + line + '.');
90
+ var tokens = swig.parseFile(importPath, parseOpts).tokens;
91
+ utils.each(tokens, function (token) {
92
+ var out = '',
93
+ macroName;
94
+ if (!token || token.name !== 'macro' || !token.compile) {
95
+ return;
96
+ }
97
+ macroName = token.args[0];
98
+ // Phase 2 (#T15): macro.compile now returns an IRMacro node
99
+ // rather than a JS source string. Render it through the shared
100
+ // backend so import.js still gets the JS source it performs
101
+ // regex-surgery on for namespace-prefixing. The +'\n' trailing
102
+ // newline matches the pre-Phase-2 compile output exactly.
103
+ out += backend.compile([token.compile(compiler, token.args, token.content, [], compileOpts)], [], compileOpts) + '\n';
104
+ self.out.push({compiled: out, name: macroName});
105
+ });
85
106
  });
86
107
 
87
108
  parser.on(types.VAR, function (token) {
88
109
  var self = this;
89
- if (!tokens || ctx) {
110
+ if (importPath === undefined || ctx) {
90
111
  throw new Error('Unexpected variable "' + token.match + '" on line ' + line + '.');
91
112
  }
92
113
 
@@ -48,6 +48,10 @@ exports.compile = function (compiler, args, content, parents, options, blockName
48
48
  }
49
49
  }
50
50
 
51
+ if (options && options.codegenMode === 'async') {
52
+ return ir.includeDeferred(file, w || undefined, !!onlyCtx, !!ignoreMissing, parentFile);
53
+ }
54
+
51
55
  return ir.include(file, w || undefined, !!onlyCtx, !!ignoreMissing, parentFile);
52
56
  };
53
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "A simple, powerful, and extendable templating engine for node.js and browsers, similar to Django, Jinja2, and Twig.",
5
5
  "keywords": [
6
6
  "template",
@@ -21,7 +21,7 @@
21
21
  "Rhinostone <contact@gina.io>"
22
22
  ],
23
23
  "dependencies": {
24
- "@rhinostone/swig-core": "2.0.1",
24
+ "@rhinostone/swig-core": "2.2.0",
25
25
  "terser": "^5.46.1",
26
26
  "yargs": "^17.7.2"
27
27
  },