@rhinostone/swig-core 2.0.1 → 2.1.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.
- package/lib/backend.js +201 -11
- package/lib/engine.js +126 -9
- package/lib/ir.js +156 -1
- package/lib/tokenparser.js +4 -6
- package/package.json +1 -1
package/lib/backend.js
CHANGED
|
@@ -273,6 +273,19 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
273
273
|
' return _output;\n' +
|
|
274
274
|
'};\n' +
|
|
275
275
|
'_ctx.' + node.name + '.safe = true;\n';
|
|
276
|
+
// Phase 3 (#T22): async-codegen mirrors the macro into _exports so
|
|
277
|
+
// {% import %} / Twig's {% from %} can bind macros across templates
|
|
278
|
+
// at runtime. The macro's closure still references _ctx (not
|
|
279
|
+
// _exports) for outer-ctx lookups — sync-semantics-preserving.
|
|
280
|
+
// Top-level filter is implicit: the existing macro body walker
|
|
281
|
+
// (above) only handles Text/Raw/LegacyJS children, so nested
|
|
282
|
+
// IRMacro never reaches this branch.
|
|
283
|
+
if (options && options.codegenMode === 'async') {
|
|
284
|
+
if (_security.dangerousProps.indexOf(node.name) !== -1) {
|
|
285
|
+
throw new Error('Macro name "' + node.name + '" is reserved.');
|
|
286
|
+
}
|
|
287
|
+
out += '_exports.' + node.name + ' = _ctx.' + node.name + ';\n';
|
|
288
|
+
}
|
|
276
289
|
return;
|
|
277
290
|
}
|
|
278
291
|
if (node.type === 'Parent') {
|
|
@@ -295,6 +308,33 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
295
308
|
// block, its body carries whichever generation's content is active.
|
|
296
309
|
// Emit the body verbatim; the block name is carried as metadata for
|
|
297
310
|
// downstream tooling (parent-chain walks happen in the parent tag).
|
|
311
|
+
//
|
|
312
|
+
// Phase 3 (#T22) async mode: block remapping happens at runtime
|
|
313
|
+
// instead of parse time. When _blocks is supplied (by an extending
|
|
314
|
+
// child via IRExtendsDeferred-emitted `_parent(_ctx, _mergedBlocks)`)
|
|
315
|
+
// and contains an override for this block name, call the override
|
|
316
|
+
// async fn and append its return string. Otherwise emit the parent's
|
|
317
|
+
// own body inline as the default (matches sync behavior). The
|
|
318
|
+
// override fn closes over the calling template's own _swig / _filters
|
|
319
|
+
// / _utils / _ext, since it was constructed inside that template's
|
|
320
|
+
// body wrapper — it does NOT see this template's local _output, which
|
|
321
|
+
// is correct (the override returns its own string and the outer
|
|
322
|
+
// _output += await ... appends it here).
|
|
323
|
+
if (options && options.codegenMode === 'async') {
|
|
324
|
+
var blockNameJS = JSON.stringify(node.name);
|
|
325
|
+
out += 'if (_blocks && _blocks[' + blockNameJS + ']) {\n';
|
|
326
|
+
out += ' _output += await _blocks[' + blockNameJS + '](_ctx);\n';
|
|
327
|
+
out += '} else {\n';
|
|
328
|
+
utils.each(node.body, function (b) {
|
|
329
|
+
if (b.type === 'LegacyJS') { out += b.js; return; }
|
|
330
|
+
if (b.type === 'Text' || b.type === 'Raw') {
|
|
331
|
+
out += '_output += "' + escapeTextValue(b.value) + '";\n';
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
out += '}\n';
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
298
338
|
utils.each(node.body, function (b) {
|
|
299
339
|
if (b.type === 'LegacyJS') { out += b.js; return; }
|
|
300
340
|
if (b.type === 'Text' || b.type === 'Raw') {
|
|
@@ -343,6 +383,157 @@ exports.compile = function (template, parents, options, blockName) {
|
|
|
343
383
|
(node.ignoreMissing ? '} catch (e) {}\n' : '');
|
|
344
384
|
return;
|
|
345
385
|
}
|
|
386
|
+
if (node.type === 'IncludeDeferred') {
|
|
387
|
+
// Phase 3 (#T22): async-codegen counterpart to Include. Resolves the
|
|
388
|
+
// path via `_swig.getTemplate(...)` at render time (Promise<TemplateFn>),
|
|
389
|
+
// then awaits the resolved template fn's call. The double-await flows
|
|
390
|
+
// correctly whether the loaded tpl is sync (returns string) or async-
|
|
391
|
+
// compiled (returns Promise<string>) — `await <non-Promise>` resolves
|
|
392
|
+
// to the value unchanged. Mirrors the Include branch's path/context/
|
|
393
|
+
// isolated/ignoreMissing dispatch; only the runtime call shape differs.
|
|
394
|
+
var incdPathJS, incdCtxJS;
|
|
395
|
+
if (node.path && typeof node.path === 'object' && typeof node.path.type === 'string') {
|
|
396
|
+
incdPathJS = exports.emitExpr(node.path);
|
|
397
|
+
} else {
|
|
398
|
+
incdPathJS = node.path;
|
|
399
|
+
}
|
|
400
|
+
if (node.context !== undefined) {
|
|
401
|
+
if (typeof node.context === 'object' && typeof node.context.type === 'string') {
|
|
402
|
+
incdCtxJS = exports.emitExpr(node.context);
|
|
403
|
+
} else {
|
|
404
|
+
incdCtxJS = node.context;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
var incdSelector;
|
|
408
|
+
if (node.isolated && incdCtxJS) {
|
|
409
|
+
incdSelector = incdCtxJS;
|
|
410
|
+
} else if (!incdCtxJS) {
|
|
411
|
+
incdSelector = '_ctx';
|
|
412
|
+
} else {
|
|
413
|
+
incdSelector = '_utils.extend({}, _ctx, ' + incdCtxJS + ')';
|
|
414
|
+
}
|
|
415
|
+
var incdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
|
|
416
|
+
out += (node.ignoreMissing ? ' try {\n' : '') +
|
|
417
|
+
'_output += (await (await _swig.getTemplate(' + incdPathJS + ', ' + incdOpts + '))(' + incdSelector + ')).output;\n' +
|
|
418
|
+
(node.ignoreMissing ? '} catch (e) {}\n' : '');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (node.type === 'ImportDeferred') {
|
|
422
|
+
// Phase 3 (#T22): async-codegen counterpart to the parse-time
|
|
423
|
+
// import flow. The imported template is loaded via _swig.getTemplate
|
|
424
|
+
// and called with the parent's _ctx so its body's compiled JS
|
|
425
|
+
// closes over outer-ctx vars (sync-semantics-preserving). Only
|
|
426
|
+
// the resolved value's `.exports` is bound; the imported body's
|
|
427
|
+
// `.output` is discarded — import doesn't render.
|
|
428
|
+
if (_security.dangerousProps.indexOf(node.alias) !== -1) {
|
|
429
|
+
throw new Error('Import alias "' + node.alias + '" is reserved.');
|
|
430
|
+
}
|
|
431
|
+
var impdPathJS;
|
|
432
|
+
if (node.path && typeof node.path === 'object' && typeof node.path.type === 'string') {
|
|
433
|
+
impdPathJS = exports.emitExpr(node.path);
|
|
434
|
+
} else {
|
|
435
|
+
impdPathJS = node.path;
|
|
436
|
+
}
|
|
437
|
+
var impdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
|
|
438
|
+
out += '_ctx.' + node.alias + ' = (await (await _swig.getTemplate(' + impdPathJS + ', ' + impdOpts + '))(_ctx)).exports;\n';
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (node.type === 'FromImportDeferred') {
|
|
442
|
+
// Phase 3 (#T22): async-codegen counterpart to Twig's `{% from
|
|
443
|
+
// "file" import a, b as c %}`. Single getTemplate call, then
|
|
444
|
+
// per-entry bind onto _ctx. Async IIFE keeps `_imp` local so
|
|
445
|
+
// multiple from-imports in the same template don't collide.
|
|
446
|
+
var fromdImports = node.imports || [];
|
|
447
|
+
utils.each(fromdImports, function (entry) {
|
|
448
|
+
if (_security.dangerousProps.indexOf(entry.name) !== -1) {
|
|
449
|
+
throw new Error('From-import name "' + entry.name + '" is reserved.');
|
|
450
|
+
}
|
|
451
|
+
var bindName = entry.alias || entry.name;
|
|
452
|
+
if (_security.dangerousProps.indexOf(bindName) !== -1) {
|
|
453
|
+
throw new Error('From-import binding "' + bindName + '" is reserved.');
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
var fromdPathJS;
|
|
457
|
+
if (node.path && typeof node.path === 'object' && typeof node.path.type === 'string') {
|
|
458
|
+
fromdPathJS = exports.emitExpr(node.path);
|
|
459
|
+
} else {
|
|
460
|
+
fromdPathJS = node.path;
|
|
461
|
+
}
|
|
462
|
+
var fromdOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
|
|
463
|
+
var fromdBindings = '';
|
|
464
|
+
utils.each(fromdImports, function (entry) {
|
|
465
|
+
var bindName = entry.alias || entry.name;
|
|
466
|
+
fromdBindings += ' _ctx.' + bindName + ' = _imp["' + entry.name + '"];\n';
|
|
467
|
+
});
|
|
468
|
+
out += 'await (async function () {\n' +
|
|
469
|
+
' var _imp = (await (await _swig.getTemplate(' + fromdPathJS + ', ' + fromdOpts + '))(_ctx)).exports;\n' +
|
|
470
|
+
fromdBindings +
|
|
471
|
+
'})();\n';
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (node.type === 'ExtendsDeferred') {
|
|
475
|
+
// Phase 3 (#T22): async-codegen counterpart to engine.getParents +
|
|
476
|
+
// engine.precompile's parse-time block remap. Inverts at runtime
|
|
477
|
+
// via the _blocks parameter contract:
|
|
478
|
+
//
|
|
479
|
+
// 1. childIRs preludes (set / import / macros) emit via recursive
|
|
480
|
+
// backend.compile so they populate _ctx and _exports with the
|
|
481
|
+
// child's own bindings before the parent runs.
|
|
482
|
+
// 2. Build _localChildBlocks — each block in node.childBlocks
|
|
483
|
+
// becomes an `async function (_ctx) -> Promise<string>` that
|
|
484
|
+
// shadows _output to a local accumulator and returns it. The
|
|
485
|
+
// block body uses the same shallow Text/Raw/LegacyJS walker as
|
|
486
|
+
// the IRBlock branch above.
|
|
487
|
+
// 3. Merge: _mergedBlocks = _utils.extend({}, _localChildBlocks,
|
|
488
|
+
// _blocks || {}). Inherited overrides from a deeper child win
|
|
489
|
+
// per "child blocks override parent's wholesale per name"
|
|
490
|
+
// (matches sync's remapBlocks). Multi-level chain (child →
|
|
491
|
+
// middle → grandparent) propagates correctly: middle merges
|
|
492
|
+
// its own block bodies with the inherited child overrides,
|
|
493
|
+
// and grandparent sees the merged map.
|
|
494
|
+
// 4. Resolve parent via _swig.getTemplate (Promise<TemplateFn>),
|
|
495
|
+
// then await _parent(_ctx, _mergedBlocks). Parent's body's
|
|
496
|
+
// IRBlock branch consults _mergedBlocks and uses overrides
|
|
497
|
+
// where present, defaults otherwise.
|
|
498
|
+
// 5. _output = _parentResult.output. Parent's _exports is
|
|
499
|
+
// DISCARDED — _exports stays as the child's own preludes-
|
|
500
|
+
// populated map. Matches sync where {% import "child" %}
|
|
501
|
+
// exposes child's own top-level macros only and never
|
|
502
|
+
// traverses extends. One semantic across sync and async.
|
|
503
|
+
if (node.childIRs && node.childIRs.length) {
|
|
504
|
+
out += exports.compile(node.childIRs, parents, options);
|
|
505
|
+
}
|
|
506
|
+
out += 'var _localChildBlocks = {};\n';
|
|
507
|
+
if (node.childBlocks) {
|
|
508
|
+
utils.each(node.childBlocks, function (block, name) {
|
|
509
|
+
var blockBodyJS = '';
|
|
510
|
+
utils.each(block.body, function (b) {
|
|
511
|
+
if (b.type === 'LegacyJS') { blockBodyJS += b.js; return; }
|
|
512
|
+
if (b.type === 'Text' || b.type === 'Raw') {
|
|
513
|
+
blockBodyJS += '_output += "' + escapeTextValue(b.value) + '";\n';
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
out += '_localChildBlocks[' + JSON.stringify(name) + '] = async function (_ctx) {\n' +
|
|
518
|
+
' var _output = "";\n' +
|
|
519
|
+
blockBodyJS +
|
|
520
|
+
' return _output;\n' +
|
|
521
|
+
'};\n';
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
out += 'var _mergedBlocks = _utils.extend({}, _localChildBlocks, _blocks || {});\n';
|
|
525
|
+
var extPathJS;
|
|
526
|
+
if (node.path && typeof node.path === 'object' && typeof node.path.type === 'string') {
|
|
527
|
+
extPathJS = exports.emitExpr(node.path);
|
|
528
|
+
} else {
|
|
529
|
+
extPathJS = node.path;
|
|
530
|
+
}
|
|
531
|
+
var extOpts = '{resolveFrom: "' + (node.resolveFrom || '') + '"}';
|
|
532
|
+
out += 'var _parentTpl = await _swig.getTemplate(' + extPathJS + ', ' + extOpts + ');\n';
|
|
533
|
+
out += 'var _parentResult = await _parentTpl(_ctx, _mergedBlocks);\n';
|
|
534
|
+
out += '_output = _parentResult.output;\n';
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
346
537
|
if (node.type === 'With') {
|
|
347
538
|
// Phase 3 Session 12: scoped-context region (Twig's `{% with %}`).
|
|
348
539
|
// Emits an IIFE that shadows `_ctx` for the body's lexical scope;
|
|
@@ -606,20 +797,19 @@ function checkDotExpr(path, ctxPrefix) {
|
|
|
606
797
|
}
|
|
607
798
|
|
|
608
799
|
/*!
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
800
|
+
* Build the dot-path emit. `(checkDot_ctx ? _ctx.<path> : (checkDot_closure
|
|
801
|
+
* ? <path> : ""))` — checks the `_ctx.<path>` walk first, falls back to a
|
|
802
|
+
* bare-closure walk (covers macro params, host-globals like `Math`), and
|
|
803
|
+
* coerces a missing path to `""` for safe interpolation. Kept as a local
|
|
804
|
+
* private helper rather than imported from tokenparser.js because (a) it
|
|
805
|
+
* is a pure function of its argument and (b) the backend must not acquire
|
|
806
|
+
* a runtime dependency on the TokenParser module (which is a specific
|
|
613
807
|
* frontend concern, not a shared-backend one). @private
|
|
614
808
|
*/
|
|
615
809
|
function checkMatchExpr(match) {
|
|
616
|
-
var
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return '(' + checkDotExpr(match, ctx) + ' ? ' + ctx + match.join('.') + ' : "")';
|
|
620
|
-
}
|
|
621
|
-
result = '(' + checkDotExpr(match, '_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
|
|
622
|
-
return '(' + result + ' !== null ? ' + result + ' : ' + '"" )';
|
|
810
|
+
var leaf = match.join('.');
|
|
811
|
+
return '(' + checkDotExpr(match, '_ctx.') + ' ? _ctx.' + leaf
|
|
812
|
+
+ ' : (' + checkDotExpr(match, '') + ' ? ' + leaf + ' : ""))';
|
|
623
813
|
}
|
|
624
814
|
|
|
625
815
|
/*!
|
package/lib/engine.js
CHANGED
|
@@ -2,6 +2,17 @@ var utils = require('./utils'),
|
|
|
2
2
|
backend = require('./backend'),
|
|
3
3
|
cache = require('./cache');
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Constructor for AsyncFunction. Not a global; resolved once at module
|
|
7
|
+
* load via the prototype-chain walk of an async function expression.
|
|
8
|
+
* Used by {@link buildTemplateFunction} to wrap async-codegen template
|
|
9
|
+
* bodies so they return a Promise. Available in any engine that supports
|
|
10
|
+
* the `async function` syntax — Node ≥ 7.6 / all modern browsers; the
|
|
11
|
+
* package's engine floor (>=12) guarantees it.
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
var AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
15
|
+
|
|
5
16
|
/**
|
|
6
17
|
* Empty function used as a fallback in compiled template code.
|
|
7
18
|
* @return {string} Empty string.
|
|
@@ -132,24 +143,46 @@ exports.getParents = function (tokens, options, deps) {
|
|
|
132
143
|
* contract every tag's compile() output depends on — `_output`, `_ext`,
|
|
133
144
|
* `_ctx`, etc. are referenced by name in emitted code.
|
|
134
145
|
*
|
|
146
|
+
* When `options.codegenMode === 'async'` the body is wrapped with
|
|
147
|
+
* AsyncFunction instead so the function returns a
|
|
148
|
+
* `Promise<{output: string, exports: object}>`. The exports map collects
|
|
149
|
+
* top-level macros so async importers (`IRImportDeferred` /
|
|
150
|
+
* `IRFromImportDeferred`) can bind them under their alias at runtime
|
|
151
|
+
* without parse-time pre-resolution. Async mode is also the consumer
|
|
152
|
+
* of the deferred-resolution IR shapes (IRIncludeDeferred,
|
|
153
|
+
* IRImportDeferred, IRFromImportDeferred, IRExtendsDeferred) which emit
|
|
154
|
+
* `await` calls into the body. The async wrapper takes a 6th positional
|
|
155
|
+
* `_blocks` parameter — an optional `{name: async fn(_ctx) -> string}`
|
|
156
|
+
* map of block overrides supplied by an extending child template. The
|
|
157
|
+
* IRBlock async-emit consults `_blocks[name]` first and falls back to
|
|
158
|
+
* the parent's inline body when no override is registered. Sync mode
|
|
159
|
+
* argument list is unchanged.
|
|
160
|
+
*
|
|
135
161
|
* Filename attribution on compile-time failures lives on the frontend
|
|
136
162
|
* per the seam rule (the caller knows which template the body came from
|
|
137
163
|
* and can attach that via options.filename in its own try/catch). This
|
|
138
|
-
* helper only throws whatever
|
|
139
|
-
* can catch and rewrap.
|
|
164
|
+
* helper only throws whatever the wrapper constructor throws — the
|
|
165
|
+
* caller can catch and rewrap.
|
|
140
166
|
*
|
|
141
167
|
* @param {object|array} tokens Parsed token tree.
|
|
142
168
|
* @param {array} [parents] Parent tokens from getParents().
|
|
143
169
|
* @param {object} [options] Swig options object.
|
|
144
|
-
* @return {Function} Template function.
|
|
170
|
+
* @return {Function} Template function (sync) or async template function (Promise-returning).
|
|
145
171
|
*/
|
|
146
172
|
exports.buildTemplateFunction = function (tokens, parents, options) {
|
|
147
|
-
|
|
148
|
-
' var _ext = _swig.extensions,\n' +
|
|
173
|
+
if (options && options.codegenMode === 'async') {
|
|
174
|
+
var asyncBody = ' var _ext = _swig.extensions,\n' +
|
|
175
|
+
' _output = "",\n' +
|
|
176
|
+
' _exports = {};\n' +
|
|
177
|
+
backend.compile(tokens, parents, options) + '\n' +
|
|
178
|
+
' return { output: _output, exports: _exports };\n';
|
|
179
|
+
return new AsyncFunction('_swig', '_ctx', '_filters', '_utils', '_fn', '_blocks', asyncBody);
|
|
180
|
+
}
|
|
181
|
+
var body = ' var _ext = _swig.extensions,\n' +
|
|
149
182
|
' _output = "";\n' +
|
|
150
183
|
backend.compile(tokens, parents, options) + '\n' +
|
|
151
|
-
' return _output;\n'
|
|
152
|
-
|
|
184
|
+
' return _output;\n';
|
|
185
|
+
return new Function('_swig', '_ctx', '_filters', '_utils', '_fn', body);
|
|
153
186
|
};
|
|
154
187
|
|
|
155
188
|
/**
|
|
@@ -306,7 +339,7 @@ exports.install = function (self, frontend) {
|
|
|
306
339
|
contextLength = utils.keys(context).length;
|
|
307
340
|
pre = self.precompile(source, options);
|
|
308
341
|
|
|
309
|
-
function compiled(locals) {
|
|
342
|
+
function compiled(locals, blocks) {
|
|
310
343
|
var lcls;
|
|
311
344
|
if (locals && contextLength) {
|
|
312
345
|
lcls = utils.extend({}, context, locals);
|
|
@@ -317,7 +350,13 @@ exports.install = function (self, frontend) {
|
|
|
317
350
|
} else {
|
|
318
351
|
lcls = {};
|
|
319
352
|
}
|
|
320
|
-
|
|
353
|
+
// `blocks` is the optional async-mode block-override map supplied
|
|
354
|
+
// by an extending child template's IRExtendsDeferred-emitted
|
|
355
|
+
// `_parent(_ctx, _mergedBlocks)` call. Sync templates ignore the
|
|
356
|
+
// extra positional arg (regular Function discards unread args);
|
|
357
|
+
// async wrapper picks it up as `_blocks` and consults it from
|
|
358
|
+
// IRBlock emits.
|
|
359
|
+
return pre.tpl(self, lcls, filters, utils, efn, blocks);
|
|
321
360
|
}
|
|
322
361
|
|
|
323
362
|
utils.extend(compiled, pre.tokens);
|
|
@@ -374,6 +413,84 @@ exports.install = function (self, frontend) {
|
|
|
374
413
|
return self.compile(src, options);
|
|
375
414
|
};
|
|
376
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Async-codegen runtime helper. Resolves a template path via the active
|
|
418
|
+
* loader (cb-shape preferred when supported, sync fallback otherwise),
|
|
419
|
+
* compiles the source in async-codegen mode, and returns a Promise that
|
|
420
|
+
* resolves to the compiled async template function. The compiled
|
|
421
|
+
* function, when called, returns a `Promise<{output: string, exports:
|
|
422
|
+
* object}>` — `output` is the rendered text, `exports` is the
|
|
423
|
+
* top-level-macro map used by `IRImportDeferred` /
|
|
424
|
+
* `IRFromImportDeferred` callers.
|
|
425
|
+
*
|
|
426
|
+
* Called from compiled bodies emitted for `IRIncludeDeferred`,
|
|
427
|
+
* `IRImportDeferred`, `IRFromImportDeferred` (and, in a subsequent
|
|
428
|
+
* slice, `IRExtendsDeferred`) when an async-mode template needs to
|
|
429
|
+
* load a child template at render time.
|
|
430
|
+
*
|
|
431
|
+
* Cache semantics — bypasses the cache via `options.cache = false` on
|
|
432
|
+
* the inner compile call. The sync compile cache and the async compile
|
|
433
|
+
* cache would otherwise share a key (the resolved filename) and serve
|
|
434
|
+
* a mode-mismatched compiled fn to the wrong caller. Cleaner
|
|
435
|
+
* namespacing (separate sync/async cache buckets, or a key suffix) is
|
|
436
|
+
* deferred to a follow-up slice.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} pathname Template path; resolved via the active loader.
|
|
439
|
+
* @param {object} [options] Per-call options. Honored: `resolveFrom`,
|
|
440
|
+
* `filename`. Inherits `self.options` for the rest.
|
|
441
|
+
* @return {Promise<Function>} Resolves with the compiled async template fn.
|
|
442
|
+
*/
|
|
443
|
+
self.getTemplate = function (pathname, options) {
|
|
444
|
+
options = options || {};
|
|
445
|
+
|
|
446
|
+
var resolved;
|
|
447
|
+
try {
|
|
448
|
+
resolved = self.options.loader.resolve(pathname, options.resolveFrom);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return Promise.reject(e);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
var compileOpts = utils.extend({}, options, {
|
|
454
|
+
filename: options.filename || resolved,
|
|
455
|
+
codegenMode: 'async',
|
|
456
|
+
cache: false
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
return new Promise(function (resolve, reject) {
|
|
460
|
+
function onLoaded(err, src) {
|
|
461
|
+
if (err) {
|
|
462
|
+
reject(err);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
var compiled;
|
|
466
|
+
try {
|
|
467
|
+
compiled = self.compile(src, compileOpts);
|
|
468
|
+
} catch (err2) {
|
|
469
|
+
reject(err2);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
resolve(compiled);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (self.options.loader.load.length >= 2) {
|
|
476
|
+
try {
|
|
477
|
+
self.options.loader.load(resolved, onLoaded);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
onLoaded(e);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
var src;
|
|
483
|
+
try {
|
|
484
|
+
src = self.options.loader.load(resolved);
|
|
485
|
+
} catch (e) {
|
|
486
|
+
onLoaded(e);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
onLoaded(null, src);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
|
|
377
494
|
self.render = function (source, options) {
|
|
378
495
|
return self.compile(source, options)();
|
|
379
496
|
};
|
package/lib/ir.js
CHANGED
|
@@ -273,13 +273,93 @@
|
|
|
273
273
|
* @property {IRLoc} [loc]
|
|
274
274
|
*/
|
|
275
275
|
|
|
276
|
+
/* ------------------------------------------------------------------ *
|
|
277
|
+
* Deferred-resolution shapes — async codegen path.
|
|
278
|
+
*
|
|
279
|
+
* In sync codegen mode the parser pre-resolves `extends` / `include` /
|
|
280
|
+
* `import` / Twig `from` paths via `swig.parseFile(...)` and inlines
|
|
281
|
+
* the resolved tokens at parse-finalization time. That model can't run
|
|
282
|
+
* against an async-only loader (S3 / Redis / fetch-backed), and it
|
|
283
|
+
* can't handle dynamic paths (`{% extends parent_var %}`) since the
|
|
284
|
+
* value isn't known until render.
|
|
285
|
+
*
|
|
286
|
+
* The deferred shapes carry the unresolved path expression to render
|
|
287
|
+
* time. The async backend (`compileAsync`) emits cb-shaped or
|
|
288
|
+
* AsyncFunction-shaped JS that hits a runtime `_swig.getTemplate(...)`
|
|
289
|
+
* call to resolve and apply the parent / included / imported template.
|
|
290
|
+
*
|
|
291
|
+
* Sync codegen uses the existing {@link IRInclude} / {@link IRImport}
|
|
292
|
+
* shapes and the parser's pre-resolution path. Async codegen uses
|
|
293
|
+
* these. The frontend tag handlers branch on the codegen mode.
|
|
294
|
+
* ------------------------------------------------------------------ */
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Deferred extends marker — replaces the parse-finalization
|
|
298
|
+
* `getParents()` walk for async-mode templates. The runtime backend
|
|
299
|
+
* loads the parent via `_swig.getTemplate(<path>)`, applies the
|
|
300
|
+
* child's block overrides via `remapBlocks`, prepends the child's
|
|
301
|
+
* non-block IR via `importNonBlocks`, then renders.
|
|
302
|
+
*
|
|
303
|
+
* @typedef {Object} IRExtendsDeferred
|
|
304
|
+
* @property {'ExtendsDeferred'} type
|
|
305
|
+
* @property {IRExpr} path Path expression. Resolved at render time.
|
|
306
|
+
* @property {Object<string, IRBlock>} [childBlocks] Child template's block overrides.
|
|
307
|
+
* @property {IRStatement[]} [childIRs] Child template's non-block IR (prepended at runtime).
|
|
308
|
+
* @property {string} [resolveFrom] Including template's filename for loader-relative resolution.
|
|
309
|
+
* @property {IRLoc} [loc]
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Deferred include — async codegen counterpart to {@link IRInclude}.
|
|
314
|
+
* Same field shape; the `'IncludeDeferred'` type tag is the dispatch
|
|
315
|
+
* signal that tells the backend to emit an async resolution call
|
|
316
|
+
* instead of an inlined sync include.
|
|
317
|
+
*
|
|
318
|
+
* @typedef {Object} IRIncludeDeferred
|
|
319
|
+
* @property {'IncludeDeferred'} type
|
|
320
|
+
* @property {IRExpr} path
|
|
321
|
+
* @property {IRExpr} [context]
|
|
322
|
+
* @property {boolean} [isolated]
|
|
323
|
+
* @property {boolean} [ignoreMissing]
|
|
324
|
+
* @property {string} [resolveFrom]
|
|
325
|
+
* @property {IRLoc} [loc]
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Deferred import — async codegen counterpart to {@link IRImport}.
|
|
330
|
+
* `alias` MUST pass the dangerousProps guard at backend emit time.
|
|
331
|
+
*
|
|
332
|
+
* @typedef {Object} IRImportDeferred
|
|
333
|
+
* @property {'ImportDeferred'} type
|
|
334
|
+
* @property {IRExpr} path
|
|
335
|
+
* @property {string} alias
|
|
336
|
+
* @property {string} [resolveFrom]
|
|
337
|
+
* @property {IRLoc} [loc]
|
|
338
|
+
*/
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Deferred Twig `{% from "file" import macroA, macroB as alias %}` —
|
|
342
|
+
* async codegen path. Each `imports[]` entry binds the imported macro
|
|
343
|
+
* `name` as a callable in `_ctx` under `alias` (or under `name` when
|
|
344
|
+
* `alias` is null). Every `name` and every non-null `alias` MUST pass
|
|
345
|
+
* the dangerousProps guard at backend emit time.
|
|
346
|
+
*
|
|
347
|
+
* @typedef {Object} IRFromImportDeferred
|
|
348
|
+
* @property {'FromImportDeferred'} type
|
|
349
|
+
* @property {IRExpr} path
|
|
350
|
+
* @property {Array<{name: string, alias: (string|null)}>} imports
|
|
351
|
+
* @property {string} [resolveFrom]
|
|
352
|
+
* @property {IRLoc} [loc]
|
|
353
|
+
*/
|
|
354
|
+
|
|
276
355
|
/**
|
|
277
356
|
* Any body-level IR node.
|
|
278
357
|
*
|
|
279
358
|
* @typedef {(
|
|
280
359
|
* IRText | IROutput | IRIf | IRFor | IRBlock | IRInclude | IRImport |
|
|
281
360
|
* IRMacro | IRCall | IRSet | IRRaw | IRParent | IRAutoescape | IRFilter |
|
|
282
|
-
* IRWith | IRLegacyJS
|
|
361
|
+
* IRWith | IRLegacyJS |
|
|
362
|
+
* IRExtendsDeferred | IRIncludeDeferred | IRImportDeferred | IRFromImportDeferred
|
|
283
363
|
* )} IRStatement
|
|
284
364
|
*/
|
|
285
365
|
|
|
@@ -754,6 +834,81 @@ exports.legacyJS = function (js, loc) {
|
|
|
754
834
|
return withLoc({ type: 'LegacyJS', js: js }, loc);
|
|
755
835
|
};
|
|
756
836
|
|
|
837
|
+
/* -- Deferred-resolution factories --------------------------------- */
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Build an {@link IRExtendsDeferred} node. The factory stores `path`,
|
|
841
|
+
* `childBlocks`, and `childIRs` opaquely.
|
|
842
|
+
*
|
|
843
|
+
* @param {IRExpr} path
|
|
844
|
+
* @param {Object<string, IRBlock>} [childBlocks]
|
|
845
|
+
* @param {IRStatement[]} [childIRs]
|
|
846
|
+
* @param {string} [resolveFrom]
|
|
847
|
+
* @param {IRLoc} [loc]
|
|
848
|
+
* @return {IRExtendsDeferred}
|
|
849
|
+
*/
|
|
850
|
+
exports.extendsDeferred = function (path, childBlocks, childIRs, resolveFrom, loc) {
|
|
851
|
+
var node = { type: 'ExtendsDeferred', path: path };
|
|
852
|
+
if (childBlocks !== undefined) { node.childBlocks = childBlocks; }
|
|
853
|
+
if (childIRs !== undefined) { node.childIRs = childIRs; }
|
|
854
|
+
if (resolveFrom !== undefined) { node.resolveFrom = resolveFrom; }
|
|
855
|
+
return withLoc(node, loc);
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Build an {@link IRIncludeDeferred} node. Async-codegen counterpart
|
|
860
|
+
* to {@link exports.include}.
|
|
861
|
+
*
|
|
862
|
+
* @param {IRExpr} path
|
|
863
|
+
* @param {IRExpr} [context]
|
|
864
|
+
* @param {boolean} [isolated]
|
|
865
|
+
* @param {boolean} [ignoreMissing]
|
|
866
|
+
* @param {string} [resolveFrom]
|
|
867
|
+
* @param {IRLoc} [loc]
|
|
868
|
+
* @return {IRIncludeDeferred}
|
|
869
|
+
*/
|
|
870
|
+
exports.includeDeferred = function (path, context, isolated, ignoreMissing, resolveFrom, loc) {
|
|
871
|
+
var node = { type: 'IncludeDeferred', path: path };
|
|
872
|
+
if (context !== undefined) { node.context = context; }
|
|
873
|
+
if (isolated !== undefined) { node.isolated = isolated; }
|
|
874
|
+
if (ignoreMissing !== undefined) { node.ignoreMissing = ignoreMissing; }
|
|
875
|
+
if (resolveFrom !== undefined) { node.resolveFrom = resolveFrom; }
|
|
876
|
+
return withLoc(node, loc);
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Build an {@link IRImportDeferred} node. `alias` MUST pass the
|
|
881
|
+
* dangerousProps guard at backend emit time.
|
|
882
|
+
*
|
|
883
|
+
* @param {IRExpr} path
|
|
884
|
+
* @param {string} alias
|
|
885
|
+
* @param {string} [resolveFrom]
|
|
886
|
+
* @param {IRLoc} [loc]
|
|
887
|
+
* @return {IRImportDeferred}
|
|
888
|
+
*/
|
|
889
|
+
exports.importDeferred = function (path, alias, resolveFrom, loc) {
|
|
890
|
+
var node = { type: 'ImportDeferred', path: path, alias: alias };
|
|
891
|
+
if (resolveFrom !== undefined) { node.resolveFrom = resolveFrom; }
|
|
892
|
+
return withLoc(node, loc);
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Build an {@link IRFromImportDeferred} node. Each entry's `name` and
|
|
897
|
+
* non-null `alias` MUST pass the dangerousProps guard at backend emit
|
|
898
|
+
* time.
|
|
899
|
+
*
|
|
900
|
+
* @param {IRExpr} path
|
|
901
|
+
* @param {Array<{name: string, alias: (string|null)}>} imports
|
|
902
|
+
* @param {string} [resolveFrom]
|
|
903
|
+
* @param {IRLoc} [loc]
|
|
904
|
+
* @return {IRFromImportDeferred}
|
|
905
|
+
*/
|
|
906
|
+
exports.fromImportDeferred = function (path, imports, resolveFrom, loc) {
|
|
907
|
+
var node = { type: 'FromImportDeferred', path: path, imports: imports };
|
|
908
|
+
if (resolveFrom !== undefined) { node.resolveFrom = resolveFrom; }
|
|
909
|
+
return withLoc(node, loc);
|
|
910
|
+
};
|
|
911
|
+
|
|
757
912
|
/* -- Expression factories ------------------------------------------ */
|
|
758
913
|
|
|
759
914
|
/**
|
package/lib/tokenparser.js
CHANGED
|
@@ -884,7 +884,8 @@ TokenParser.prototype = {
|
|
|
884
884
|
* @private
|
|
885
885
|
*/
|
|
886
886
|
checkMatch: function (match) {
|
|
887
|
-
var temp = match[0],
|
|
887
|
+
var temp = match[0],
|
|
888
|
+
leaf = match.join('.');
|
|
888
889
|
|
|
889
890
|
function checkDot(ctx) {
|
|
890
891
|
var c = ctx + temp,
|
|
@@ -904,11 +905,8 @@ TokenParser.prototype = {
|
|
|
904
905
|
return build;
|
|
905
906
|
}
|
|
906
907
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
result = '(' + checkDot('_ctx.') + ' ? ' + buildDot('_ctx.') + ' : ' + buildDot('') + ')';
|
|
911
|
-
return '(' + result + ' !== null ? ' + result + ' : ' + '"" )';
|
|
908
|
+
return '(' + checkDot('_ctx.') + ' ? _ctx.' + leaf
|
|
909
|
+
+ ' : (' + checkDot('') + ' ? ' + leaf + ' : ""))';
|
|
912
910
|
}
|
|
913
911
|
};
|
|
914
912
|
|