@rhinostone/swig-twig 2.4.0 → 2.4.2

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/tags/from.js CHANGED
@@ -50,9 +50,12 @@ exports.block = true;
50
50
  * for each requested macro, invokes its `compile` to get the IRMacro
51
51
  * node and renders that node to JS through `backend.compile`. A
52
52
  * macro requested by name but not found in the imported template
53
- * raises a filename-aware throw. The resulting
54
- * `[{compiled, origName, aliasName}, ...]` list is stashed on
55
- * `token.args` for the compile step to rewrite.
53
+ * raises a filename-aware throw. The imported file's own nested
54
+ * `{% import %}` / `{% from %}` tokens are carried through (flagged
55
+ * `isImport`, with `boundNames` + a synthesized private `slot`) so the
56
+ * requested macros can reference them at call time. The resulting list
57
+ * (nested entries + `[{compiled, origName, aliasName}, ...]`) is stashed
58
+ * on `token.args` for the compile step to rewrite.
56
59
  *
57
60
  * @param {string} str Tag body.
58
61
  * @param {number} line Source line of the opening `{%`.
@@ -157,9 +160,34 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
157
160
  // Index the imported template's macros by name so we can look up
158
161
  // each requested entry once. Raises a filename-aware throw if an
159
162
  // entry names a macro that doesn't exist in the imported template.
163
+ //
164
+ // The imported file may have its own `{% import %}` / `{% from %}`.
165
+ // Unlike `{% import %}`, `{% from %}` binds bare names with no namespace
166
+ // alias to hang those nested imports under, so they re-home under a
167
+ // synthesized private slot keyed off the path (compile() applies the
168
+ // rewrite). This keeps the inner names out of the parent's bare scope —
169
+ // bare `{{ name }}` in the parent stays undefined, matching Twig's rule
170
+ // that imports are local to the importing template.
171
+ var privateSlot = '__nsfrom_' + path.replace(/[^a-zA-Z0-9]/g, '_');
172
+ var nested = [];
160
173
  var macroIndex = {};
161
174
  utils.each(parsed.tokens, function (tk) {
162
- if (!tk || tk.name !== 'macro' || typeof tk.compile !== 'function') {
175
+ if (!tk || typeof tk.compile !== 'function') {
176
+ return;
177
+ }
178
+ if (tk.name === 'import' || tk.name === 'from') {
179
+ var bn = (tk.name === 'import')
180
+ ? [tk.args[tk.args.length - 1]]
181
+ : utils.map(tk.args, function (a) { return a.aliasName; });
182
+ nested.push({
183
+ compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
184
+ isImport: true,
185
+ boundNames: bn,
186
+ slot: privateSlot
187
+ });
188
+ return;
189
+ }
190
+ if (tk.name !== 'macro') {
163
191
  return;
164
192
  }
165
193
  macroIndex[tk.args[0]] = tk;
@@ -181,7 +209,7 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
181
209
  });
182
210
  }
183
211
 
184
- token.args = resolved;
212
+ token.args = nested.concat(resolved);
185
213
  return true;
186
214
  };
187
215
 
@@ -201,6 +229,11 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
201
229
  * `undefined`, matching Twig's "unimported macros are not available"
202
230
  * semantic.
203
231
  *
232
+ * The imported file's own nested imports (flagged `isImport`) are emitted
233
+ * first, re-homed under a private slot (`_ctx.<boundName>` ->
234
+ * `_ctx.<slot>.<boundName>`) so they resolve for the imported macros at
235
+ * call time without leaking into the parent's bare scope.
236
+ *
204
237
  * @return {string} JS source that assigns every imported macro into
205
238
  * `_ctx.<aliasName>`. Backend lifts it into
206
239
  * `IRLegacyJS`.
@@ -225,20 +258,43 @@ exports.compile = function (compiler, args, content, parents, options) {
225
258
  options.filename || ''
226
259
  );
227
260
  }
228
- var allOrigNames = utils.map(args, function (arg) { return arg.origName; }).join('|');
229
- var replacements = utils.map(args, function (arg) {
261
+ var macros = [];
262
+ var nested = [];
263
+ utils.each(args, function (a) { (a.isImport ? nested : macros).push(a); });
264
+ var allOrigNames = utils.map(macros, function (arg) { return arg.origName; }).join('|');
265
+ var replacements = utils.map(macros, function (arg) {
230
266
  return {
231
267
  ex: new RegExp('_ctx\\.' + arg.origName + '(\\W)(?!' + allOrigNames + ')', 'g'),
232
268
  re: '_ctx.' + arg.aliasName + '$1'
233
269
  };
234
270
  });
271
+ // The imported file's own nested imports re-home under a private slot so
272
+ // they never leak into the parent's bare scope: `_ctx.<boundName>` ->
273
+ // `_ctx.<slot>.<boundName>`, applied to the nested setup JS and to every
274
+ // imported macro body that references a bound name.
275
+ var innerReplacements = [];
276
+ utils.each(nested, function (a) {
277
+ utils.each(a.boundNames, function (nm) {
278
+ innerReplacements.push({
279
+ ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
280
+ re: '_ctx.' + a.slot + '.' + nm + '$1'
281
+ });
282
+ });
283
+ });
235
284
 
236
285
  var out = ' var _output = "";\n';
237
- utils.each(args, function (arg) {
286
+ // Nested imports first (under the private slot), so the imported macros
287
+ // below resolve them at call time.
288
+ utils.each(nested, function (a) {
289
+ out += '_ctx.' + a.slot + ' = _ctx.' + a.slot + ' || {};\n';
290
+ var c = a.compiled;
291
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
292
+ out += c;
293
+ });
294
+ utils.each(macros, function (arg) {
238
295
  var c = arg.compiled;
239
- utils.each(replacements, function (re) {
240
- c = c.replace(re.ex, re.re);
241
- });
296
+ utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
297
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
242
298
  out += c;
243
299
  });
244
300
 
@@ -47,13 +47,16 @@ exports.block = true;
47
47
  * against the bare-identifier rule and the CVE-2023-25345
48
48
  * `_dangerousProps` blocklist.
49
49
  *
50
- * Walks the imported template's token list (via `swig.parseFile`) for
51
- * `{% macro %}` tokens; for each macro, invokes its `compile` to get
52
- * the IRMacro node and renders that node to JS through
53
- * `backend.compile`. The resulting `{compiled, name}` objects + the
54
- * alias string are stashed on `token.args` `exports.compile` pops
55
- * the alias off the tail and performs the namespace-prefix rewrite on
56
- * each macro's compiled JS.
50
+ * Walks the imported template's token list (via `swig.parseFile`). For
51
+ * each `{% macro %}` token, invokes its `compile` to get the IRMacro node
52
+ * and renders it to JS through `backend.compile`. For each nested
53
+ * `{% import %}` / `{% from %}` token (the imported file's own imports),
54
+ * carries its compiled setup through flagged `isImport` (with `boundNames`)
55
+ * so macros defined here can reference it at call time. The resulting
56
+ * entries + the alias string are stashed on `token.args` —
57
+ * `exports.compile` pops the alias off the tail, re-homes any nested
58
+ * imports under it, and performs the namespace-prefix rewrite on each
59
+ * macro's compiled JS.
57
60
  *
58
61
  * @param {string} str Tag body.
59
62
  * @param {number} line Source line of the opening `{%`.
@@ -130,7 +133,37 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
130
133
  var macros = [];
131
134
 
132
135
  utils.each(parsed.tokens, function (tk) {
133
- if (!tk || tk.name !== 'macro' || typeof tk.compile !== 'function') {
136
+ if (!tk || typeof tk.compile !== 'function') {
137
+ return;
138
+ }
139
+ // The imported file may itself import macros, via `{% import "x" as y %}`
140
+ // or `{% from "x" import a, b %}`. Carry those nested imports through so
141
+ // a macro defined here that references a name they bind resolves at call
142
+ // time — without them the call compiles against a namespace/binding that
143
+ // was never set up, and silently renders empty.
144
+ //
145
+ // Unlike native `lib/tags/import.js` (which emits the nested bindings
146
+ // straight into the caller's `_ctx` and thereby leaks them into the
147
+ // parent scope), swig-twig keeps imports local to their template per
148
+ // Twig's scoping rule: compile() re-homes every bound name under THIS
149
+ // import's alias (`_ctx.<alias>.<boundName>`), so none is visible bare
150
+ // in the parent. `tk.args` is already parsed; slice() avoids the pop()
151
+ // in compile() mutating the cached token.
152
+ if (tk.name === 'import' || tk.name === 'from') {
153
+ // Names the nested import binds into _ctx: `{% import %}` binds one
154
+ // namespace alias (the tail of args); `{% from %}` binds one per
155
+ // requested entry (its alias name).
156
+ var boundNames = (tk.name === 'import')
157
+ ? [tk.args[tk.args.length - 1]]
158
+ : utils.map(tk.args, function (a) { return a.aliasName; });
159
+ macros.push({
160
+ compiled: tk.compile(null, tk.args.slice(), tk.content, [], compileOpts) + '\n',
161
+ isImport: true,
162
+ boundNames: boundNames
163
+ });
164
+ return;
165
+ }
166
+ if (tk.name !== 'macro') {
134
167
  return;
135
168
  }
136
169
  var macroName = tk.args[0];
@@ -145,15 +178,20 @@ exports.parse = function (str, line, parser, types, stack, opts, swig, token) {
145
178
 
146
179
  /**
147
180
  * Emit the namespace-prefix rewrite. Pops the alias off the tail of
148
- * `args`, builds a `_ctx.<name>(\\W)(?!<allMacros>)` regex for each
149
- * imported macro, and rewrites every occurrence in each macro's
150
- * compiled JS to `_ctx.<alias>.<name>`. Concatenates the rewritten
151
- * bodies after the `_ctx.<alias> = {};` namespace-init line and
152
- * returns the result as a JS source string (the backend lifts it into
153
- * `IRLegacyJS`).
181
+ * `args` and splits the rest into macros and nested imports (flagged
182
+ * `isImport` by parse()). Builds a `_ctx.<name>(\\W)(?!<allMacros>)`
183
+ * regex for each imported macro and rewrites every occurrence in each
184
+ * macro's compiled JS to `_ctx.<alias>.<name>`. Nested imports are
185
+ * emitted first, re-homed under the alias (`_ctx.<boundName>` ->
186
+ * `_ctx.<alias>.<boundName>`) so a file's own imports stay local and
187
+ * never leak bare into the parent scope; the same re-homing is applied
188
+ * to macro bodies that reference any bound name. Concatenates after the
189
+ * `_ctx.<alias> = {};` namespace-init line and returns a JS source string
190
+ * (the backend lifts it into `IRLegacyJS`).
154
191
  *
155
- * @return {string} JS source that initialises `_ctx.<alias>` and
156
- * assigns every imported macro into it.
192
+ * @return {string} JS source that initialises `_ctx.<alias>`, re-homes
193
+ * any nested imports under it, and assigns every
194
+ * imported macro into it.
157
195
  */
158
196
  exports.compile = function (compiler, args, content, parents, options) {
159
197
  // Phase 2 (#T22): async-codegen branch. Parse stashed `[path, alias]`
@@ -167,20 +205,47 @@ exports.compile = function (compiler, args, content, parents, options) {
167
205
  );
168
206
  }
169
207
  var ctx = args.pop();
170
- var allMacros = utils.map(args, function (arg) { return arg.name; }).join('|');
208
+ var macros = [];
209
+ var nested = [];
210
+ utils.each(args, function (a) {
211
+ (a.isImport ? nested : macros).push(a);
212
+ });
213
+ var allMacros = utils.map(macros, function (arg) { return arg.name; }).join('|');
171
214
  var out = '_ctx.' + ctx + ' = {};\n var _output = "";\n';
172
- var replacements = utils.map(args, function (arg) {
215
+ var replacements = utils.map(macros, function (arg) {
173
216
  return {
174
217
  ex: new RegExp('_ctx\\.' + arg.name + '(\\W)(?!' + allMacros + ')', 'g'),
175
218
  re: '_ctx.' + ctx + '.' + arg.name + '$1'
176
219
  };
177
220
  });
221
+ // Re-home every name a nested import binds under THIS alias so it stays
222
+ // local to the imported file (Twig scoping) and never leaks bare into the
223
+ // parent: `_ctx.<boundName>` -> `_ctx.<alias>.<boundName>`, applied to both
224
+ // the nested setup JS and every macro body below that references it.
225
+ // Compounds across import depth (a 3-level chain re-homes at each layer
226
+ // with no special-casing).
227
+ var innerReplacements = [];
228
+ utils.each(nested, function (a) {
229
+ utils.each(a.boundNames, function (nm) {
230
+ innerReplacements.push({
231
+ ex: new RegExp('_ctx\\.' + nm + '(\\W)', 'g'),
232
+ re: '_ctx.' + ctx + '.' + nm + '$1'
233
+ });
234
+ });
235
+ });
236
+
237
+ // Nested imports first, re-homed under the alias, so the macros defined
238
+ // below resolve them at call time.
239
+ utils.each(nested, function (a) {
240
+ var c = a.compiled;
241
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
242
+ out += c;
243
+ });
178
244
 
179
- utils.each(args, function (arg) {
245
+ utils.each(macros, function (arg) {
180
246
  var c = arg.compiled;
181
- utils.each(replacements, function (re) {
182
- c = c.replace(re.ex, re.re);
183
- });
247
+ utils.each(replacements, function (re) { c = c.replace(re.ex, re.re); });
248
+ utils.each(innerReplacements, function (re) { c = c.replace(re.ex, re.re); });
184
249
  out += c;
185
250
  });
186
251
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinostone/swig-twig",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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.4.0"
25
+ "@rhinostone/swig-core": "2.4.2"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"