@rhinostone/swig-core 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.
- package/lib/backend.js +201 -11
- package/lib/engine.js +231 -13
- 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
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
var utils = require('./utils'),
|
|
2
|
+
ir = require('./ir'),
|
|
2
3
|
backend = require('./backend'),
|
|
3
4
|
cache = require('./cache');
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Constructor for AsyncFunction. Not a global; resolved once at module
|
|
8
|
+
* load via the prototype-chain walk of an async function expression.
|
|
9
|
+
* Used by {@link buildTemplateFunction} to wrap async-codegen template
|
|
10
|
+
* bodies so they return a Promise. Available in any engine that supports
|
|
11
|
+
* the `async function` syntax — Node ≥ 7.6 / all modern browsers; the
|
|
12
|
+
* package's engine floor (>=12) guarantees it.
|
|
13
|
+
* @private
|
|
14
|
+
*/
|
|
15
|
+
var AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Empty function used as a fallback in compiled template code.
|
|
7
19
|
* @return {string} Empty string.
|
|
@@ -68,6 +80,82 @@ exports.importNonBlocks = function (blocks, tokens) {
|
|
|
68
80
|
});
|
|
69
81
|
};
|
|
70
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Build an `IRExtendsDeferred` node from a parsed child template.
|
|
85
|
+
*
|
|
86
|
+
* Walks `tokens.blocks` (the parser's map of every block-level tag that
|
|
87
|
+
* appeared at top level — both `{% block %}` overrides and non-block
|
|
88
|
+
* preludes like `{% set %}`, `{% import %}`, `{% macro %}`) and:
|
|
89
|
+
*
|
|
90
|
+
* - For each `{% block %}` token: invokes its `.compile(...)` with an
|
|
91
|
+
* empty parents list to obtain an `IRBlock` (body is one `IRLegacyJS`
|
|
92
|
+
* wrapping the recursively-compiled JS source — same shape sync mode
|
|
93
|
+
* produces). The IRBlock is keyed under the block name in `childBlocks`.
|
|
94
|
+
* - For each non-`block` block-level token: invokes its `.compile(...)`
|
|
95
|
+
* and wraps any JS-string return in `IRLegacyJS`. The resulting IR
|
|
96
|
+
* node is pushed onto `childIRs` (the prelude list).
|
|
97
|
+
*
|
|
98
|
+
* The backend's `IRExtendsDeferred` emit branch handles runtime parent
|
|
99
|
+
* resolution via `_swig.getTemplate` and the `_blocks` parameter contract
|
|
100
|
+
* (see `packages/swig-core/lib/backend.js`).
|
|
101
|
+
*
|
|
102
|
+
* `tokens.parent` is lifted into an `IRLiteral('string', …)` so the
|
|
103
|
+
* backend's `emitExpr` path produces a quoted JS string literal. The
|
|
104
|
+
* native parser stashes `tokens.parent` as the literal text of the
|
|
105
|
+
* extends argument after stripping enclosing quotes (lib/parser.js:273),
|
|
106
|
+
* which works for `{% extends "layout.html" %}` but for a bare
|
|
107
|
+
* identifier (`{% extends parent_var %}`) yields a pre-lowered JS
|
|
108
|
+
* source fragment such as `((typeof _ctx.parent_var !== "undefined")
|
|
109
|
+
* ? _ctx.parent_var : …)`. Embedded here as a string literal it
|
|
110
|
+
* becomes a garbage template lookup at runtime
|
|
111
|
+
* (`Template not found: /((typeof _ctx.parent_var …`). Closing the
|
|
112
|
+
* dynamic-extends gap requires the parser to stash an IRExpr on
|
|
113
|
+
* `tokens.parent` and this helper to lower it through
|
|
114
|
+
* `ir.extendsDeferred`'s `parentExpr` slot — already designed as
|
|
115
|
+
* `<IRExpr>` per the deferred IR contract.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} tokens Parsed child template (must have `.parent`).
|
|
118
|
+
* @param {object} options Per-call Swig options; `options.filename` is
|
|
119
|
+
* used as the deferred resolveFrom.
|
|
120
|
+
* @return {object} IRExtendsDeferred node.
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
function buildExtendsDeferred(tokens, options) {
|
|
124
|
+
var childBlocks = {};
|
|
125
|
+
var childIRs = [];
|
|
126
|
+
utils.each(tokens.blocks, function (blockToken) {
|
|
127
|
+
var args = blockToken.args ? blockToken.args.slice(0) : [];
|
|
128
|
+
var content = blockToken.content ? blockToken.content.slice(0) : [];
|
|
129
|
+
if (blockToken.name === 'block') {
|
|
130
|
+
var blockName = args.join('');
|
|
131
|
+
var blockIR = blockToken.compile(backend.compile, args, content, [], options, blockName, blockToken);
|
|
132
|
+
if (blockIR && typeof blockIR === 'object' && typeof blockIR.type === 'string') {
|
|
133
|
+
childBlocks[blockName] = blockIR;
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
var result = blockToken.compile(backend.compile, args, content, [], options, undefined, blockToken);
|
|
138
|
+
if (result === undefined || result === null || result === '') { return; }
|
|
139
|
+
if (typeof result === 'string') {
|
|
140
|
+
childIRs.push(ir.legacyJS(result));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (utils.isArray(result)) {
|
|
144
|
+
utils.each(result, function (n) { childIRs.push(n); });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof result === 'object' && typeof result.type === 'string') {
|
|
148
|
+
childIRs.push(result);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return ir.extendsDeferred(
|
|
152
|
+
ir.literal('string', tokens.parent),
|
|
153
|
+
childBlocks,
|
|
154
|
+
childIRs,
|
|
155
|
+
options.filename || ''
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
71
159
|
/**
|
|
72
160
|
* Walk a template's `extends` chain and build the parent token tree.
|
|
73
161
|
* Detects circular inheritance.
|
|
@@ -132,24 +220,46 @@ exports.getParents = function (tokens, options, deps) {
|
|
|
132
220
|
* contract every tag's compile() output depends on — `_output`, `_ext`,
|
|
133
221
|
* `_ctx`, etc. are referenced by name in emitted code.
|
|
134
222
|
*
|
|
223
|
+
* When `options.codegenMode === 'async'` the body is wrapped with
|
|
224
|
+
* AsyncFunction instead so the function returns a
|
|
225
|
+
* `Promise<{output: string, exports: object}>`. The exports map collects
|
|
226
|
+
* top-level macros so async importers (`IRImportDeferred` /
|
|
227
|
+
* `IRFromImportDeferred`) can bind them under their alias at runtime
|
|
228
|
+
* without parse-time pre-resolution. Async mode is also the consumer
|
|
229
|
+
* of the deferred-resolution IR shapes (IRIncludeDeferred,
|
|
230
|
+
* IRImportDeferred, IRFromImportDeferred, IRExtendsDeferred) which emit
|
|
231
|
+
* `await` calls into the body. The async wrapper takes a 6th positional
|
|
232
|
+
* `_blocks` parameter — an optional `{name: async fn(_ctx) -> string}`
|
|
233
|
+
* map of block overrides supplied by an extending child template. The
|
|
234
|
+
* IRBlock async-emit consults `_blocks[name]` first and falls back to
|
|
235
|
+
* the parent's inline body when no override is registered. Sync mode
|
|
236
|
+
* argument list is unchanged.
|
|
237
|
+
*
|
|
135
238
|
* Filename attribution on compile-time failures lives on the frontend
|
|
136
239
|
* per the seam rule (the caller knows which template the body came from
|
|
137
240
|
* and can attach that via options.filename in its own try/catch). This
|
|
138
|
-
* helper only throws whatever
|
|
139
|
-
* can catch and rewrap.
|
|
241
|
+
* helper only throws whatever the wrapper constructor throws — the
|
|
242
|
+
* caller can catch and rewrap.
|
|
140
243
|
*
|
|
141
244
|
* @param {object|array} tokens Parsed token tree.
|
|
142
245
|
* @param {array} [parents] Parent tokens from getParents().
|
|
143
246
|
* @param {object} [options] Swig options object.
|
|
144
|
-
* @return {Function} Template function.
|
|
247
|
+
* @return {Function} Template function (sync) or async template function (Promise-returning).
|
|
145
248
|
*/
|
|
146
249
|
exports.buildTemplateFunction = function (tokens, parents, options) {
|
|
147
|
-
|
|
148
|
-
' var _ext = _swig.extensions,\n' +
|
|
250
|
+
if (options && options.codegenMode === 'async') {
|
|
251
|
+
var asyncBody = ' var _ext = _swig.extensions,\n' +
|
|
252
|
+
' _output = "",\n' +
|
|
253
|
+
' _exports = {};\n' +
|
|
254
|
+
backend.compile(tokens, parents, options) + '\n' +
|
|
255
|
+
' return { output: _output, exports: _exports };\n';
|
|
256
|
+
return new AsyncFunction('_swig', '_ctx', '_filters', '_utils', '_fn', '_blocks', asyncBody);
|
|
257
|
+
}
|
|
258
|
+
var body = ' var _ext = _swig.extensions,\n' +
|
|
149
259
|
' _output = "";\n' +
|
|
150
260
|
backend.compile(tokens, parents, options) + '\n' +
|
|
151
|
-
' return _output;\n'
|
|
152
|
-
|
|
261
|
+
' return _output;\n';
|
|
262
|
+
return new Function('_swig', '_ctx', '_filters', '_utils', '_fn', body);
|
|
153
263
|
};
|
|
154
264
|
|
|
155
265
|
/**
|
|
@@ -274,12 +384,24 @@ exports.install = function (self, frontend) {
|
|
|
274
384
|
|
|
275
385
|
self.precompile = function (source, options) {
|
|
276
386
|
var tokens = self.parse(source, options),
|
|
277
|
-
parents
|
|
387
|
+
parents,
|
|
278
388
|
tpl;
|
|
279
389
|
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
390
|
+
if (options && options.codegenMode === 'async' && tokens.parent) {
|
|
391
|
+
// Async extends — defer parent walking and block remapping to
|
|
392
|
+
// runtime via the IRExtendsDeferred emit branch in backend.js.
|
|
393
|
+
// The deferred node carries the child's blocks + non-block
|
|
394
|
+
// preludes; the backend resolves the parent via _swig.getTemplate
|
|
395
|
+
// and threads block overrides through the _blocks parameter
|
|
396
|
+
// contract.
|
|
397
|
+
parents = [];
|
|
398
|
+
tokens.tokens = [buildExtendsDeferred(tokens, options)];
|
|
399
|
+
} else {
|
|
400
|
+
parents = getParentsInternal(tokens, options);
|
|
401
|
+
if (parents.length) {
|
|
402
|
+
tokens.tokens = exports.remapBlocks(tokens.blocks, parents[0].tokens);
|
|
403
|
+
exports.importNonBlocks(tokens.blocks, tokens.tokens);
|
|
404
|
+
}
|
|
283
405
|
}
|
|
284
406
|
|
|
285
407
|
try {
|
|
@@ -306,7 +428,7 @@ exports.install = function (self, frontend) {
|
|
|
306
428
|
contextLength = utils.keys(context).length;
|
|
307
429
|
pre = self.precompile(source, options);
|
|
308
430
|
|
|
309
|
-
function compiled(locals) {
|
|
431
|
+
function compiled(locals, blocks) {
|
|
310
432
|
var lcls;
|
|
311
433
|
if (locals && contextLength) {
|
|
312
434
|
lcls = utils.extend({}, context, locals);
|
|
@@ -317,7 +439,13 @@ exports.install = function (self, frontend) {
|
|
|
317
439
|
} else {
|
|
318
440
|
lcls = {};
|
|
319
441
|
}
|
|
320
|
-
|
|
442
|
+
// `blocks` is the optional async-mode block-override map supplied
|
|
443
|
+
// by an extending child template's IRExtendsDeferred-emitted
|
|
444
|
+
// `_parent(_ctx, _mergedBlocks)` call. Sync templates ignore the
|
|
445
|
+
// extra positional arg (regular Function discards unread args);
|
|
446
|
+
// async wrapper picks it up as `_blocks` and consults it from
|
|
447
|
+
// IRBlock emits.
|
|
448
|
+
return pre.tpl(self, lcls, filters, utils, efn, blocks);
|
|
321
449
|
}
|
|
322
450
|
|
|
323
451
|
utils.extend(compiled, pre.tokens);
|
|
@@ -374,12 +502,102 @@ exports.install = function (self, frontend) {
|
|
|
374
502
|
return self.compile(src, options);
|
|
375
503
|
};
|
|
376
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Async-codegen runtime helper. Resolves a template path via the active
|
|
507
|
+
* loader (cb-shape preferred when supported, sync fallback otherwise),
|
|
508
|
+
* compiles the source in async-codegen mode, and returns a Promise that
|
|
509
|
+
* resolves to the compiled async template function. The compiled
|
|
510
|
+
* function, when called, returns a `Promise<{output: string, exports:
|
|
511
|
+
* object}>` — `output` is the rendered text, `exports` is the
|
|
512
|
+
* top-level-macro map used by `IRImportDeferred` /
|
|
513
|
+
* `IRFromImportDeferred` callers.
|
|
514
|
+
*
|
|
515
|
+
* Called from compiled bodies emitted for `IRIncludeDeferred`,
|
|
516
|
+
* `IRImportDeferred`, `IRFromImportDeferred` (and, in a subsequent
|
|
517
|
+
* slice, `IRExtendsDeferred`) when an async-mode template needs to
|
|
518
|
+
* load a child template at render time.
|
|
519
|
+
*
|
|
520
|
+
* Cache semantics — bypasses the cache via `options.cache = false` on
|
|
521
|
+
* the inner compile call. The sync compile cache and the async compile
|
|
522
|
+
* cache would otherwise share a key (the resolved filename) and serve
|
|
523
|
+
* a mode-mismatched compiled fn to the wrong caller. Cleaner
|
|
524
|
+
* namespacing (separate sync/async cache buckets, or a key suffix) is
|
|
525
|
+
* deferred to a follow-up slice.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} pathname Template path; resolved via the active loader.
|
|
528
|
+
* @param {object} [options] Per-call options. Honored: `resolveFrom`,
|
|
529
|
+
* `filename`. Inherits `self.options` for the rest.
|
|
530
|
+
* @return {Promise<Function>} Resolves with the compiled async template fn.
|
|
531
|
+
*/
|
|
532
|
+
self.getTemplate = function (pathname, options) {
|
|
533
|
+
options = options || {};
|
|
534
|
+
|
|
535
|
+
var resolved;
|
|
536
|
+
try {
|
|
537
|
+
resolved = self.options.loader.resolve(pathname, options.resolveFrom);
|
|
538
|
+
} catch (e) {
|
|
539
|
+
return Promise.reject(e);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
var compileOpts = utils.extend({}, options, {
|
|
543
|
+
filename: options.filename || resolved,
|
|
544
|
+
codegenMode: 'async',
|
|
545
|
+
cache: false
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
return new Promise(function (resolve, reject) {
|
|
549
|
+
function onLoaded(err, src) {
|
|
550
|
+
if (err) {
|
|
551
|
+
reject(err);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
var compiled;
|
|
555
|
+
try {
|
|
556
|
+
compiled = self.compile(src, compileOpts);
|
|
557
|
+
} catch (err2) {
|
|
558
|
+
reject(err2);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
resolve(compiled);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (self.options.loader.load.length >= 2) {
|
|
565
|
+
try {
|
|
566
|
+
self.options.loader.load(resolved, onLoaded);
|
|
567
|
+
} catch (e) {
|
|
568
|
+
onLoaded(e);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
var src;
|
|
572
|
+
try {
|
|
573
|
+
src = self.options.loader.load(resolved);
|
|
574
|
+
} catch (e) {
|
|
575
|
+
onLoaded(e);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
onLoaded(null, src);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
|
|
377
583
|
self.render = function (source, options) {
|
|
378
584
|
return self.compile(source, options)();
|
|
379
585
|
};
|
|
380
586
|
|
|
381
587
|
self.renderFile = function (pathName, locals, cb) {
|
|
382
588
|
if (cb) {
|
|
589
|
+
// Async loader opt-in: route through getTemplate when
|
|
590
|
+
// loader.async === true. Explicit flag only — load.length is
|
|
591
|
+
// not a dispatch signal (the built-in fs + memory loaders are
|
|
592
|
+
// dual-mode with length 2 and must keep the sync-cb path).
|
|
593
|
+
if (self.options.loader && self.options.loader.async === true) {
|
|
594
|
+
self.getTemplate(pathName)
|
|
595
|
+
.then(function (fn) { return fn(locals); })
|
|
596
|
+
.then(function (result) { cb(null, result.output); })
|
|
597
|
+
.catch(cb);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
383
601
|
self.compileFile(pathName, {}, function (err, fn) {
|
|
384
602
|
var result;
|
|
385
603
|
|
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
|
|