@rhinostone/swig-twig 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,267 @@
1
+ var utils = require('@rhinostone/swig-core/lib/utils');
2
+
3
+ /*!
4
+ * Makes a string safe for a regular expression. Mirrors swig-twig's parser.
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 the
13
+ * Twig parser builds at parse-time so the pre-walker chunks the same way
14
+ * 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. <code>extends "x.html"</code>).
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 Twig template source for static <code>{% extends|include|import|from "..." %}</code>
52
+ * targets. Pure function; performs no I/O.
53
+ *
54
+ * The scanner mirrors the Twig parser's chunk-splitter so it agrees on
55
+ * chunk boundaries even under non-default control characters. Dynamic
56
+ * paths (<code>{% extends parent_var %}</code>) and tag bodies whose
57
+ * first token isn't a string literal are silently skipped — they remain
58
+ * on the sync path, which throws appropriately at parse time.
59
+ *
60
+ * @example
61
+ * preWalker.scan('{% from "macros.html" import foo %}', {
62
+ * varControls: ['{{', '}}'],
63
+ * tagControls: ['{%', '%}'],
64
+ * cmtControls: ['{#', '#}'],
65
+ * rawTag: 'verbatim',
66
+ * keywords: ['extends', 'include', 'import', 'from']
67
+ * });
68
+ * // => [{ kind: 'from', path: 'macros.html' }]
69
+ *
70
+ * @param {string} source
71
+ * @param {object} opts
72
+ * @param {array} opts.varControls
73
+ * @param {array} opts.tagControls
74
+ * @param {array} opts.cmtControls
75
+ * @param {string} opts.rawTag Tag name that opens verbatim regions
76
+ * (<code>verbatim</code> for Twig).
77
+ * @param {array} opts.keywords Keywords whose first quoted argument
78
+ * is a template path. Twig:
79
+ * <code>['extends', 'include', 'import',
80
+ * 'from']</code>.
81
+ * @return {array} List of <code>{ kind, path }</code> entries.
82
+ */
83
+ exports.scan = function (source, opts) {
84
+ source = source.replace(/\r\n/g, '\n');
85
+
86
+ var splitter = buildSplitter(opts),
87
+ tagOpen = opts.tagControls[0],
88
+ tagClose = opts.tagControls[1],
89
+ rawTag = opts.rawTag,
90
+ endRawTag = 'end' + rawTag,
91
+ keywordRegex = new RegExp(
92
+ '^(' + opts.keywords.join('|') + ')\\s+["\\\']([^"\\\']+)["\\\']'
93
+ ),
94
+ chunks = source.split(splitter),
95
+ results = [],
96
+ inRaw = false,
97
+ i,
98
+ chunk,
99
+ body,
100
+ name,
101
+ m;
102
+
103
+ for (i = 0; i < chunks.length; i += 1) {
104
+ chunk = chunks[i];
105
+ if (typeof chunk !== 'string' || !chunk) {
106
+ continue;
107
+ }
108
+
109
+ if (!utils.startsWith(chunk, tagOpen) || !utils.endsWith(chunk, tagClose)) {
110
+ continue;
111
+ }
112
+
113
+ body = stripTagBody(chunk, tagOpen, tagClose);
114
+ name = body.split(/\s+/)[0];
115
+
116
+ if (name === rawTag) {
117
+ inRaw = true;
118
+ continue;
119
+ }
120
+ if (name === endRawTag) {
121
+ inRaw = false;
122
+ continue;
123
+ }
124
+ if (inRaw) {
125
+ continue;
126
+ }
127
+
128
+ m = keywordRegex.exec(body);
129
+ if (m) {
130
+ results.push({ kind: m[1], path: m[2] });
131
+ }
132
+ }
133
+
134
+ return results;
135
+ };
136
+
137
+ /**
138
+ * Walk the dependency graph asynchronously starting from <var>entryPath</var>.
139
+ *
140
+ * Repeatedly loads, scans, and resolves child template paths in parallel
141
+ * via the user's async loader, until the dep graph closes. Returns a
142
+ * Promise resolving to a populated <code>{ resolvedPath: source }</code>
143
+ * map suitable for backing a memory loader.
144
+ *
145
+ * Cycles in the graph are tolerated — once a path is in the map or
146
+ * pending, subsequent enqueue requests are dropped. The synchronous
147
+ * renderer's existing circular-extends guard handles cycles at parse
148
+ * time on the second pass.
149
+ *
150
+ * @param {string} entryPath Resolved path of the entry template.
151
+ * @param {object} loader User loader. Must expose:
152
+ * <code>resolve(to, from)</code> (sync, returns
153
+ * string) and
154
+ * <code>load(id, cb)</code> (async, calls
155
+ * <code>cb(err, source)</code>).
156
+ * @param {object} scanOpts Pass-through to {@link scan}.
157
+ * @return {Promise} Resolves to the populated memory map.
158
+ */
159
+ exports.walk = function (entryPath, loader, scanOpts) {
160
+ var memMap = {};
161
+ var pending = {};
162
+
163
+ return new Promise(function (resolve, reject) {
164
+ var inFlight = 0;
165
+ var queue = [];
166
+ var hasError = false;
167
+
168
+ function enqueue(path) {
169
+ if (memMap.hasOwnProperty(path) || pending[path]) {
170
+ return;
171
+ }
172
+ pending[path] = true;
173
+ queue.push(path);
174
+ }
175
+
176
+ function drain() {
177
+ while (queue.length > 0 && !hasError) {
178
+ var path = queue.shift();
179
+ inFlight += 1;
180
+ startLoad(path);
181
+ }
182
+ if (inFlight === 0 && !hasError && queue.length === 0) {
183
+ resolve(memMap);
184
+ }
185
+ }
186
+
187
+ function startLoad(resolvedPath) {
188
+ loader.load(resolvedPath, function (err, src) {
189
+ if (hasError) {
190
+ return;
191
+ }
192
+ if (err) {
193
+ hasError = true;
194
+ reject(err);
195
+ return;
196
+ }
197
+ if (typeof src !== 'string') {
198
+ hasError = true;
199
+ reject(new Error('Async loader returned non-string source for "' + resolvedPath + '"'));
200
+ return;
201
+ }
202
+ memMap[resolvedPath] = src;
203
+
204
+ var targets;
205
+ try {
206
+ targets = exports.scan(src, scanOpts);
207
+ } catch (e) {
208
+ hasError = true;
209
+ reject(e);
210
+ return;
211
+ }
212
+
213
+ var i, resolvedChild;
214
+ for (i = 0; i < targets.length; i += 1) {
215
+ try {
216
+ resolvedChild = loader.resolve(targets[i].path, resolvedPath);
217
+ } catch (e) {
218
+ hasError = true;
219
+ reject(e);
220
+ return;
221
+ }
222
+ enqueue(resolvedChild);
223
+ }
224
+
225
+ inFlight -= 1;
226
+ drain();
227
+ });
228
+ }
229
+
230
+ enqueue(entryPath);
231
+ drain();
232
+ });
233
+ };
234
+
235
+ /**
236
+ * Build a sync memory wrapper around a pre-populated
237
+ * <code>{ resolvedPath: source }</code> map. Delegates <code>resolve</code>
238
+ * to the user loader so cache keys match what the pre-walker produced.
239
+ *
240
+ * @param {object} userLoader Original async loader (used for resolve).
241
+ * @param {object} memMap Pre-populated source map.
242
+ * @return {object} A loader exposing <code>resolve</code> and
243
+ * <code>load</code>.
244
+ */
245
+ exports.makeMemoryWrapper = function (userLoader, memMap) {
246
+ return {
247
+ resolve: function (to, from) {
248
+ return userLoader.resolve(to, from);
249
+ },
250
+ load: function (id, cb) {
251
+ var src = memMap[id];
252
+ if (typeof src !== 'string') {
253
+ var err = new Error('Pre-walked map missing path: "' + id + '"');
254
+ if (cb) {
255
+ cb(err);
256
+ return;
257
+ }
258
+ throw err;
259
+ }
260
+ if (cb) {
261
+ cb(null, src);
262
+ return;
263
+ }
264
+ return src;
265
+ }
266
+ };
267
+ };
package/lib/filters.js CHANGED
@@ -278,26 +278,33 @@ exports.raw.safe = true;
278
278
  * @param {string} [type='html'] Pass `'js'` for JavaScript-safe escaping.
279
279
  * @return {string}
280
280
  */
281
+ function escapeHtmlRest(ch) {
282
+ return ch === '<' ? '&lt;' : ch === '>' ? '&gt;' : ch === '"' ? '&quot;' : '&#39;';
283
+ }
284
+
281
285
  exports.escape = function (input, type) {
282
- var out = iterateFilter.apply(exports.escape, arguments),
283
- inp = input,
284
- i = 0,
285
- code;
286
+ var t, inp, out, i, code;
286
287
 
287
- if (out !== undefined) {
288
- return out;
288
+ if (input === null || input === undefined) {
289
+ return input;
289
290
  }
290
291
 
291
- if (typeof input !== 'string') {
292
+ t = typeof input;
293
+
294
+ if (t !== 'string') {
295
+ if (t === 'object') {
296
+ out = iterateFilter.apply(exports.escape, arguments);
297
+ if (out !== undefined) {
298
+ return out;
299
+ }
300
+ }
292
301
  return input;
293
302
  }
294
303
 
295
- out = '';
296
-
297
- switch (type) {
298
- case 'js':
299
- inp = inp.replace(/\\/g, '\\u005C');
300
- for (i; i < inp.length; i += 1) {
304
+ if (type === 'js') {
305
+ inp = input.replace(/\\/g, '\\u005C');
306
+ out = '';
307
+ for (i = 0; i < inp.length; i += 1) {
301
308
  code = inp.charCodeAt(i);
302
309
  if (code < 32) {
303
310
  code = code.toString(16).toUpperCase();
@@ -315,14 +322,10 @@ exports.escape = function (input, type) {
315
322
  .replace(/\=/g, '\\u003D')
316
323
  .replace(/-/g, '\\u002D')
317
324
  .replace(/;/g, '\\u003B');
318
-
319
- default:
320
- return inp.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&amp;')
321
- .replace(/</g, '&lt;')
322
- .replace(/>/g, '&gt;')
323
- .replace(/"/g, '&quot;')
324
- .replace(/'/g, '&#39;');
325
325
  }
326
+
327
+ return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&amp;')
328
+ .replace(/[<>"']/g, escapeHtmlRest);
326
329
  };
327
330
  exports.e = exports.escape;
328
331
 
package/lib/index.js CHANGED
@@ -14,7 +14,8 @@ var utils = require('@rhinostone/swig-core/lib/utils'),
14
14
  parser = require('./parser'),
15
15
  _tags = require('./tags'),
16
16
  _filters = require('./filters'),
17
- _tests = require('./tests');
17
+ _tests = require('./tests'),
18
+ preWalker = require('./async/pre-walker');
18
19
 
19
20
  exports.name = 'twig';
20
21
 
@@ -163,6 +164,157 @@ exports.Twig = function (opts) {
163
164
  utils.each(_tests, function (fn, name) {
164
165
  self.setExtension('_test_' + name, fn);
165
166
  });
167
+
168
+ function buildScanOpts() {
169
+ return {
170
+ varControls: self.options.varControls,
171
+ tagControls: self.options.tagControls,
172
+ cmtControls: self.options.cmtControls,
173
+ rawTag: 'verbatim',
174
+ keywords: ['extends', 'include', 'import', 'from']
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Render a Twig template file asynchronously, supporting async loaders.
180
+ *
181
+ * Pre-walks <code>extends</code> / <code>include</code> /
182
+ * <code>import</code> / <code>from</code> targets in parallel via the
183
+ * user loader, populates an in-memory map, then runs the existing sync
184
+ * render pipeline against the populated map. Dynamic paths
185
+ * (<code>{% extends parent_var %}</code>) are not pre-resolved and will
186
+ * throw at render time as they would on the sync path.
187
+ *
188
+ * @deprecated since 2.2.0 — use {@link Twig#renderFile} with a loader that
189
+ * sets <code>loader.async === true</code>. The async-codegen dispatch
190
+ * handles dynamic include paths the pre-walker cannot. This method will
191
+ * be removed in 3.0.
192
+ *
193
+ * @example
194
+ * twig.setDefaults({ loader: myAsyncLoader });
195
+ * twig.renderFileAsync('page.twig', { name: 'world' }, function (err, output) {
196
+ * if (err) { return done(err); }
197
+ * res.end(output);
198
+ * });
199
+ *
200
+ * @param {string} pathName Template path; resolved via the active loader.
201
+ * @param {object} [locals] Locals to render with.
202
+ * @param {Function} cb Node-style callback <code>(err, output)</code>.
203
+ * @return {undefined}
204
+ */
205
+ this.renderFileAsync = function (pathName, locals, cb) {
206
+ if (typeof locals === 'function') {
207
+ cb = locals;
208
+ locals = undefined;
209
+ }
210
+
211
+ var loader = self.options.loader;
212
+ var entry;
213
+
214
+ try {
215
+ entry = loader.resolve(pathName);
216
+ } catch (e) {
217
+ cb(e);
218
+ return;
219
+ }
220
+
221
+ preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
222
+ var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
223
+ var origLoader = self.options.loader;
224
+ self.options.loader = memWrapper;
225
+ var output, error;
226
+ try {
227
+ output = self.renderFile(entry, locals);
228
+ } catch (e) {
229
+ error = e;
230
+ }
231
+ self.options.loader = origLoader;
232
+ if (error) {
233
+ cb(error);
234
+ return;
235
+ }
236
+ cb(null, output);
237
+ }, function (err) {
238
+ cb(err);
239
+ });
240
+ };
241
+
242
+ /**
243
+ * Compile a Twig template file asynchronously, supporting async loaders.
244
+ *
245
+ * Same pre-walk / memory-wrapper / sync-pipeline shape as
246
+ * {@link Twig#renderFileAsync}. Returns the compiled function (via
247
+ * <var>cb</var>) that takes a locals object and yields a rendered
248
+ * string. The returned function captures the pre-walked memory map and
249
+ * temporarily swaps the loader on each call, so subsequent runtime
250
+ * <code>include</code>s resolve correctly without re-running the
251
+ * pre-walk.
252
+ *
253
+ * @deprecated since 2.2.0 — use {@link Twig#compileFile} with
254
+ * <code>options.codegenMode === 'async'</code> on a loader that sets
255
+ * <code>loader.async === true</code>. The returned compiled function
256
+ * yields a <code>Promise&lt;{output, exports}&gt;</code> instead of a
257
+ * string. This method will be removed in 3.0.
258
+ *
259
+ * @example
260
+ * twig.compileFileAsync('page.twig', {}, function (err, fn) {
261
+ * if (err) { return done(err); }
262
+ * res.end(fn({ name: 'world' }));
263
+ * });
264
+ *
265
+ * @param {string} pathName Template path.
266
+ * @param {object} [options] Compilation options.
267
+ * @param {Function} cb Node-style callback <code>(err, fn)</code>.
268
+ * @return {undefined}
269
+ */
270
+ this.compileFileAsync = function (pathName, options, cb) {
271
+ if (typeof options === 'function') {
272
+ cb = options;
273
+ options = {};
274
+ }
275
+
276
+ var loader = self.options.loader;
277
+ var entry;
278
+
279
+ try {
280
+ entry = loader.resolve(pathName);
281
+ } catch (e) {
282
+ cb(e);
283
+ return;
284
+ }
285
+
286
+ preWalker.walk(entry, loader, buildScanOpts()).then(function (memMap) {
287
+ var memWrapper = preWalker.makeMemoryWrapper(loader, memMap);
288
+ var origLoader = self.options.loader;
289
+ self.options.loader = memWrapper;
290
+ var compiled, error;
291
+ try {
292
+ compiled = self.compileFile(entry, options);
293
+ } catch (e) {
294
+ error = e;
295
+ }
296
+ self.options.loader = origLoader;
297
+ if (error) {
298
+ cb(error);
299
+ return;
300
+ }
301
+ var wrapped = function (locals) {
302
+ var origInner = self.options.loader;
303
+ self.options.loader = memWrapper;
304
+ try {
305
+ var output = compiled(locals);
306
+ self.options.loader = origInner;
307
+ return output;
308
+ } catch (e) {
309
+ self.options.loader = origInner;
310
+ throw e;
311
+ }
312
+ };
313
+ cb(null, wrapped);
314
+ }, function (err) {
315
+ cb(err);
316
+ });
317
+ };
166
318
  };
167
319
 
168
320
  /*!
@@ -176,8 +328,10 @@ exports.parseFile = defaultInstance.parseFile;
176
328
  exports.precompile = defaultInstance.precompile;
177
329
  exports.compile = defaultInstance.compile;
178
330
  exports.compileFile = defaultInstance.compileFile;
331
+ exports.compileFileAsync = defaultInstance.compileFileAsync;
179
332
  exports.render = defaultInstance.render;
180
333
  exports.renderFile = defaultInstance.renderFile;
334
+ exports.renderFileAsync = defaultInstance.renderFileAsync;
181
335
  exports.run = defaultInstance.run;
182
336
  exports.invalidateCache = defaultInstance.invalidateCache;
183
337
 
package/lib/tags/from.js CHANGED
@@ -30,6 +30,7 @@
30
30
  */
31
31
 
32
32
  var utils = require('@rhinostone/swig-core/lib/utils');
33
+ var ir = require('@rhinostone/swig-core/lib/ir');
33
34
  var backend = require('@rhinostone/swig-core/lib/backend');
34
35
  var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
35
36
 
@@ -135,11 +136,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
135
136
  consume();
136
137
  }
137
138
 
139
+ var path = pathTok.match.replace(/^['"]|['"]$/g, '');
140
+
141
+ if (opts && opts.codegenMode === 'async') {
142
+ // Phase 2 (#T22): async mode skips parse-time parseFile + macro
143
+ // pre-render. compile() emits IRFromImportDeferred; runtime resolves
144
+ // the template via _swig.getTemplate and binds each entry on _ctx.
145
+ token.args = [{ path: path, entries: entries }];
146
+ return true;
147
+ }
148
+
138
149
  if (!swig || typeof swig.parseFile !== 'function') {
139
150
  utils.throwError('"from" tag requires an engine context with a loader', line, opts.filename);
140
151
  }
141
152
 
142
- var path = pathTok.match.replace(/^['"]|['"]$/g, '');
143
153
  var parseOpts = { resolveFrom: opts.filename };
144
154
  var compileOpts = utils.extend({}, opts, parseOpts);
145
155
  var parsed = swig.parseFile(path, parseOpts);
@@ -195,7 +205,26 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
195
205
  * `_ctx.<aliasName>`. Backend lifts it into
196
206
  * `IRLegacyJS`.
197
207
  */
198
- exports.compile = function (compiler, args) {
208
+ exports.compile = function (compiler, args, content, parents, options) {
209
+ // Phase 2 (#T22): async-codegen branch. Parse stashed a single bundle
210
+ // `[{path, entries: [{origName, aliasName}, ...]}]` in async mode (no
211
+ // macro pre-render); emit IRFromImportDeferred so the backend's
212
+ // `_swig.getTemplate` + per-entry `_ctx.<bind>` assignment happens at
213
+ // runtime.
214
+ if (options && options.codegenMode === 'async') {
215
+ var bundle = args[0];
216
+ var imports = utils.map(bundle.entries, function (e) {
217
+ return {
218
+ name: e.origName,
219
+ alias: e.aliasName === e.origName ? null : e.aliasName
220
+ };
221
+ });
222
+ return ir.fromImportDeferred(
223
+ ir.literal('string', bundle.path),
224
+ imports,
225
+ options.filename || ''
226
+ );
227
+ }
199
228
  var allOrigNames = utils.map(args, function (arg) { return arg.origName; }).join('|');
200
229
  var replacements = utils.map(args, function (arg) {
201
230
  return {
@@ -31,6 +31,7 @@
31
31
  */
32
32
 
33
33
  var utils = require('@rhinostone/swig-core/lib/utils');
34
+ var ir = require('@rhinostone/swig-core/lib/ir');
34
35
  var backend = require('@rhinostone/swig-core/lib/backend');
35
36
  var _dangerousProps = require('@rhinostone/swig-core/lib/security').dangerousProps;
36
37
 
@@ -109,11 +110,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
109
110
  utils.throwError('Unexpected token "' + peek().match + '" after alias in "import" tag', line, opts.filename);
110
111
  }
111
112
 
113
+ var path = pathTok.match.replace(/^['"]|['"]$/g, '');
114
+
115
+ if (opts && opts.codegenMode === 'async') {
116
+ // Phase 2 (#T22): async mode skips the parse-time parseFile + macro
117
+ // pre-render. compile() emits IRImportDeferred; runtime resolves the
118
+ // template via _swig.getTemplate and binds .exports under the alias.
119
+ token.args = [path, aliasTok.match];
120
+ return true;
121
+ }
122
+
112
123
  if (!swig || typeof swig.parseFile !== 'function') {
113
124
  utils.throwError('"import" tag requires an engine context with a loader', line, opts.filename);
114
125
  }
115
126
 
116
- var path = pathTok.match.replace(/^['"]|['"]$/g, '');
117
127
  var parseOpts = { resolveFrom: opts.filename };
118
128
  var compileOpts = utils.extend({}, opts, parseOpts);
119
129
  var parsed = swig.parseFile(path, parseOpts);
@@ -145,7 +155,17 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
145
155
  * @return {string} JS source that initialises `_ctx.<alias>` and
146
156
  * assigns every imported macro into it.
147
157
  */
148
- exports.compile = function (compiler, args) {
158
+ exports.compile = function (compiler, args, content, parents, options) {
159
+ // Phase 2 (#T22): async-codegen branch. Parse stashed `[path, alias]`
160
+ // in async mode (no macro pre-render); emit IRImportDeferred so the
161
+ // backend's `_swig.getTemplate` + `.exports` bind happens at runtime.
162
+ if (options && options.codegenMode === 'async') {
163
+ return ir.importDeferred(
164
+ ir.literal('string', args[0]),
165
+ args[args.length - 1],
166
+ options.filename || ''
167
+ );
168
+ }
149
169
  var ctx = args.pop();
150
170
  var allMacros = utils.map(args, function (arg) { return arg.name; }).join('|');
151
171
  var out = '_ctx.' + ctx + ' = {};\n var _output = "";\n';
@@ -163,8 +163,17 @@ function sliceTrim(tokens, start, end, types) {
163
163
  * context emission, isolated-vs-merged selector, resolveFrom, optional
164
164
  * try/catch for ignoreMissing).
165
165
  *
166
- * @return {object} IRInclude node from `token.irExpr`.
166
+ * In async codegen mode (`options.codegenMode === 'async'`), derive an
167
+ * `IRIncludeDeferred` from the same fields so the backend routes through
168
+ * the `_swig.getTemplate` + `await` deferred-resolution path instead of
169
+ * the sync `_swig.compileFile` call.
170
+ *
171
+ * @return {object} IRInclude or IRIncludeDeferred node.
167
172
  */
168
173
  exports.compile = function (compiler, args, content, parents, options, blockName, token) {
174
+ if (options && options.codegenMode === 'async') {
175
+ var i = token.irExpr;
176
+ return ir.includeDeferred(i.path, i.context, i.isolated, i.ignoreMissing, i.resolveFrom);
177
+ }
169
178
  return token.irExpr;
170
179
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-twig",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "Twig-syntax frontend for the @rhinostone/swig-core template engine. Part of the @rhinostone/swig multi-flavor family.",
5
5
  "keywords": [
6
6
  "template",
@@ -22,7 +22,7 @@
22
22
  "node": ">=12"
23
23
  },
24
24
  "peerDependencies": {
25
- "@rhinostone/swig-core": "2.0.1"
25
+ "@rhinostone/swig-core": "2.2.0"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"