mikser-io-render-liquid 4.2.2 → 4.2.3

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/index.js CHANGED
@@ -319,10 +319,23 @@ export function parseReferences(source) {
319
319
  const raw = [...found][0] ?? null
320
320
  const from = raw ? record(raw, scope, guarded) : null
321
321
  if (node.key) {
322
- assigns.push({ key: node.key, from })
322
+ // `derived` travels with it: the closure walker applies
323
+ // these assigns as bindings too, and it has no other way
324
+ // to know the value went through a filter.
325
+ const derived = !!(node.value?.filters ?? []).length
326
+ assigns.push({ key: node.key, from, ...(derived ? { derived } : {}) })
323
327
  // Bound for the REST of this template, which is what
324
328
  // `assign` means — everything after it sees the alias.
325
- if (from) scope[node.key] = from
329
+ //
330
+ // NOT bound when the value passed through a filter: the
331
+ // result is DERIVED, so a read on it says nothing about
332
+ // the source. `{% assign rows = c.specs | split: '|' %}`
333
+ // turns a string into a list, and binding it would let a
334
+ // loop over `rows` report `specs[]` — a key the document
335
+ // does not have and cannot be given, since specs is the
336
+ // string being split. The source itself is still
337
+ // recorded above, which is the true dependency.
338
+ if (from && !derived) scope[node.key] = from
326
339
  }
327
340
  break
328
341
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io-render-liquid",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -213,3 +213,47 @@ describe('liquid parseReferences: default: as a guard', () => {
213
213
  `optional: ${r.optional.join(', ')}`)
214
214
  })
215
215
  })
216
+
217
+ // A filtered value is DERIVED, not an alias.
218
+ //
219
+ // `{% assign rows = c.specs | split: '|' %}` turns a string into a list, so a
220
+ // read of `rows[0]` says nothing about `specs` having members. Treating it as an
221
+ // alias reports `specs[]` as a required key of a document whose `specs` IS the
222
+ // string being split — a key no document can ever satisfy, on a page that
223
+ // renders correctly.
224
+ describe('liquid parseReferences: derived values are not aliases', () => {
225
+ it('records the source but does not extend paths under it', () => {
226
+ const r = parseReferences(
227
+ "{% assign rows = data.meta.specs | split: '|' %}"
228
+ + '{% for row in rows %}{{ row.name }}{% endfor %}')
229
+ assert.ok(r.variables.includes('data.meta.specs'), 'the real dependency is still recorded')
230
+ assert.ok(!r.variables.some(v => v.startsWith('data.meta.specs[')),
231
+ `fabricated a member of a string: ${r.variables.join(', ')}`)
232
+ })
233
+
234
+ it('marks the assign derived so a closure walker knows too', () => {
235
+ // The walker applies these as bindings as well, and has no other way to
236
+ // know the value went through a filter.
237
+ const r = parseReferences("{% assign rows = data.meta.specs | split: '|' %}")
238
+ assert.equal(r.assigns.find(a => a.key === 'rows')?.derived, true)
239
+ })
240
+
241
+ it('leaves an UNFILTERED alias working', () => {
242
+ const r = parseReferences('{% assign hero = data.meta.hero %}{{ hero.title }}')
243
+ assert.ok(r.variables.includes('data.meta.hero.title'))
244
+ assert.ok(!r.assigns.find(a => a.key === 'hero')?.derived)
245
+ })
246
+
247
+ it('handles the real shape: split, loop, split again', () => {
248
+ // Verbatim from the project that surfaced this.
249
+ const r = parseReferences(
250
+ "{% assign rows = data.meta.results.cases | split: '|' %}"
251
+ + '{% for row in rows %}'
252
+ + "{% assign kv = row | split: ':' %}"
253
+ + '<dt>{{ kv[0] }}</dt>'
254
+ + '{% endfor %}')
255
+ assert.ok(r.variables.includes('data.meta.results.cases'))
256
+ assert.ok(!r.variables.some(v => /results\.cases\[/.test(v)),
257
+ `fabricated: ${r.variables.filter(v => v.includes('cases')).join(', ')}`)
258
+ })
259
+ })