From cc4a9ce0a78b0eb1e3eee30c2f91344917a79fdd Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Thu, 9 Jul 2026 00:05:27 +0800 Subject: [PATCH] feat!: drop TagImplOptions in favor of Tag classes (#839) (#927) * feat!: drop TagImplOptions in favor of Tag classes (#839) Remove tag-options-adapter and the registerTag object-literal overload. Custom tags must extend Tag. Co-authored-by: Cursor * test: drop TagImplOptions-specific e2e coverage (#839) Remove #570 v9 object-literal registration test and unused metadata_file setup in #573. Co-authored-by: Cursor * test: use inline Tag classes in register-tags spec Co-authored-by: Cursor * test: remove dead throwingTag setup and duplicate throw stub for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag. Co-authored-by: Cursor * test: remove dead throwingTag setup and duplicate throw stub for.spec kept throwingTag registration after #713 removed its test. Reuse ThrowingTag in liquid.spec instead of IntendedRenderErrorTag. Co-authored-by: Cursor * fix(demo): ignore killall exit when express server already stopped Co-authored-by: Cursor * fix(demo): revert unrelated return->exit change in express test Co-authored-by: Cursor * fix(demo): use exit in express test script (no enclosing function) Co-authored-by: Cursor --------- Co-authored-by: Cursor --- demo/express/test.sh | 4 +- src/liquid.ts | 8 ++-- src/template/index.ts | 1 - src/template/tag-options-adapter.ts | 28 ------------ test/demo/test.sh | 4 +- test/e2e/issues.spec.ts | 27 +---------- test/integration/liquid/liquid.spec.ts | 9 ++-- test/integration/liquid/register-tags.spec.ts | 45 +++++++++++++------ test/integration/misc/error.spec.ts | 22 ++++----- test/integration/tags/for.spec.ts | 3 -- test/stub/tags.ts | 26 +++++++++++ 11 files changed, 78 insertions(+), 99 deletions(-) delete mode 100644 src/template/tag-options-adapter.ts create mode 100644 test/stub/tags.ts diff --git a/demo/express/test.sh b/demo/express/test.sh index ebf5f976d..b39c812f4 100755 --- a/demo/express/test.sh +++ b/demo/express/test.sh @@ -7,13 +7,13 @@ while ! grep -q "Express running" "$LOG_FILE"; do if ! kill -0 $SERVER_PID; then echo "Server exited unexpectedly." cat $LOG_FILE - return 1 + exit 1 fi sleep 1 done curl http://127.0.0.1:3000 | grep -q 'Welcome to LiquidJS' RESULT=$? -killall node +killall node || true rm $LOG_FILE if [ $RESULT != 0 ]; then exit 1 diff --git a/src/liquid.ts b/src/liquid.ts index 57ff27361..6eab36d60 100644 --- a/src/liquid.ts +++ b/src/liquid.ts @@ -1,6 +1,6 @@ import { Context } from './context' -import { toPromise, toValueSync, isFunction, forOwn, isString, strictUniq } from './util' -import { TagClass, createTagClass, TagImplOptions, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, SegmentArray } from './template' +import { toPromise, toValueSync, forOwn, isString, strictUniq } from './util' +import { TagClass, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, SegmentArray } from './template' import { LookupType } from './fs/loader' import { Render } from './render' import { Parser } from './parser' @@ -101,8 +101,8 @@ export class Liquid { public registerFilter (name: string, filter: FilterImplOptions) { this.filters[name] = filter } - public registerTag (name: string, tag: TagClass | TagImplOptions) { - this.tags[name] = isFunction(tag) ? tag : createTagClass(tag) + public registerTag (name: string, tag: TagClass) { + this.tags[name] = tag } public plugin (plugin: (this: Liquid, L: typeof Liquid) => void) { return plugin.call(this, Liquid) diff --git a/src/template/index.ts b/src/template/index.ts index 5f4f62d22..b47e1a237 100644 --- a/src/template/index.ts +++ b/src/template/index.ts @@ -1,7 +1,6 @@ export * from './template' export * from './template-impl' export * from './tag' -export * from './tag-options-adapter' export * from './filter' export * from './filter-impl-options' export * from './hash' diff --git a/src/template/tag-options-adapter.ts b/src/template/tag-options-adapter.ts deleted file mode 100644 index a8a4931ae..000000000 --- a/src/template/tag-options-adapter.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { isFunction } from '../util' -import { Hash } from './hash' -import { Tag, TagClass, TagRenderReturn } from './tag' -import { TagToken, TopLevelToken } from '../tokens' -import { Emitter } from '../emitters' -import { Context } from '../context' -import type { Liquid } from '../liquid' - -export interface TagImplOptions { - [key: string]: any - parse?: (this: Tag & TagImplOptions, token: TagToken, remainingTokens: TopLevelToken[]) => void; - render: (this: Tag & TagImplOptions, ctx: Context, emitter: Emitter, hash: Record) => TagRenderReturn; -} - -export function createTagClass (options: TagImplOptions): TagClass { - return class extends Tag { - constructor (token: TagToken, tokens: TopLevelToken[], liquid: Liquid) { - super(token, tokens, liquid) - if (isFunction(options.parse)) { - options.parse.call(this, token, tokens) - } - } - * render (ctx: Context, emitter: Emitter): TagRenderReturn { - const hash = (yield new Hash(this.token.args, ctx.opts.keyValueSeparator).render(ctx)) as Record - return yield options.render.call(this, ctx, emitter, hash) - } - } -} diff --git a/test/demo/test.sh b/test/demo/test.sh index 3ff87ac49..11a506f52 100755 --- a/test/demo/test.sh +++ b/test/demo/test.sh @@ -14,9 +14,9 @@ for demo in $(ls demo); do npm link liquidjs if npm test; then - echo [success] demo/webpack + echo "[success] demo/$demo" else - echo [fail] demo/webpack + echo "[fail] demo/$demo" exit 1 fi cd - diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index c5243fe66..cc2f36e72 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -1,4 +1,4 @@ -import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..' +import { Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..' import { spawnSync } from 'child_process' import { resolve as resolvePath } from 'path' const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid @@ -362,33 +362,8 @@ describe('Issues', function () { const html = await liquid.parseAndRender(tpl) expect(html).toMatch(/^\s*This is a love or luck potion.\s+This is a strength or health or love potion.\s*$/) }) - it('tag registration compatible to v9 #570', async () => { - const liquid = new Liquid() - liquid.registerTag('metadata_file', { - parse (tagToken: TagToken, remainTokens: TopLevelToken[]) { - this.str = tagToken.args - }, - async render (ctx: Context) { - const content = await Promise.resolve(`{{${this.str}}}`) - return this.liquid.parseAndRender(content.toString(), ctx) - } - }) - const tpl = '{% metadata_file foo %}' - const ctx = { foo: 'FOO' } - const html = await liquid.parseAndRender(tpl, ctx) - expect(html).toBe('FOO') - }) it('date filter should return parsed input when no format is provided #573', async () => { const liquid = new Liquid() - liquid.registerTag('metadata_file', { - parse (tagToken: TagToken, remainTokens: TopLevelToken[]) { - this.str = tagToken.args - }, - async render (ctx: Context) { - const content = await Promise.resolve(`{{${this.str}}}`) - return this.liquid.parseAndRender(content.toString(), ctx) - } - }) const tpl = `{{ 'now' | date }}` const html = await liquid.parseAndRender(tpl) // sample: Thursday, February 2, 2023 at 6:25 pm +0000 diff --git a/test/integration/liquid/liquid.spec.ts b/test/integration/liquid/liquid.spec.ts index e229940a2..360d1b923 100644 --- a/test/integration/liquid/liquid.spec.ts +++ b/test/integration/liquid/liquid.spec.ts @@ -1,6 +1,7 @@ import { Liquid, Context, isFalsy } from '../../../src' import { mock, restore } from '../../stub/mockfs' import { drainStream } from '../../stub/stream' +import { ThrowingTag } from '../../stub/tags' import { resolve } from 'path' describe('Liquid', function () { @@ -231,11 +232,7 @@ describe('Liquid', function () { '/root/error.html': 'A{%throwingTag%}B' }) engine = new Liquid({ root: ['/root/'] }) - engine.registerTag('throwingTag', { - render: function () { - throw new Error('intended render error') - } - }) + engine.registerTag('throwingTag', ThrowingTag) }) afterEach(restore) it('should render a simple value', async () => { @@ -244,7 +241,7 @@ describe('Liquid', function () { }) it('should throw RenderError when tag throws', async () => { const stream = await engine.renderFileToNodeStream('error.html') - expect(drainStream(stream)).rejects.toThrow(/intended render error/) + expect(drainStream(stream)).rejects.toThrow(/intended error/) }) }) describe('#analyze', () => { diff --git a/test/integration/liquid/register-tags.spec.ts b/test/integration/liquid/register-tags.spec.ts index 76d6cdf4e..2dcc2d78f 100644 --- a/test/integration/liquid/register-tags.spec.ts +++ b/test/integration/liquid/register-tags.spec.ts @@ -1,38 +1,57 @@ import { Liquid } from '../../../src/liquid' +import { Tag } from '../../../src/template/tag' +import type { Context } from '../../../src/context' +import type { TagToken, TopLevelToken } from '../../../src/tokens' describe('liquid#registerTag()', function () { it('should support render to simple string', async () => { + class SimpleStringTag extends Tag { + render () { + return 'B' + } + } const liquid = new Liquid() - liquid.registerTag('simple-string', { - render: () => 'B' - }) + liquid.registerTag('simple-string', SimpleStringTag) const html = await liquid.parseAndRender(`A{% simple-string %}C`) return expect(html).toBe('ABC') }) it('should support async tag render', async () => { + class AsyncStringTag extends Tag { + async render () { + return 'B' + } + } const liquid = new Liquid() - liquid.registerTag('async-string', { - render: async () => 'B' - }) + liquid.registerTag('async-string', AsyncStringTag) const html = await liquid.parseAndRender(`A{% async-string %}C`) return expect(html).toBe('ABC') }) it('should have access to ctx in render()', async () => { + class DynamicStringTag extends Tag { + async render (ctx: Context) { + return ctx.get(['c']) + } + } const liquid = new Liquid() - liquid.registerTag('dynamic-string', { - render: async (ctx) => ctx.get(['c']) - }) + liquid.registerTag('dynamic-string', DynamicStringTag) const html = await liquid.parseAndRender(`A{% dynamic-string %}C`, { c: 'B' }) return expect(html).toBe('ABC') }) it('should have access to tag arguments', async () => { + class ArgumentReflectorTag extends Tag { + variable: string + constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) { + super(token, remainTokens, liquid) + this.variable = token.args.split('=')[1] + } + async render (ctx: Context) { + return ctx.get([this.variable]) + } + } const liquid = new Liquid() - liquid.registerTag('argument-reflector', { - parse: function (token) { this.variable = token.args.split('=')[1] }, - render: async function (ctx) { return ctx.get(this.variable) } - }) + liquid.registerTag('argument-reflector', ArgumentReflectorTag) const html = await liquid.parseAndRender(`A{% argument-reflector variable=c %}C`, { c: 'B' }) diff --git a/test/integration/misc/error.spec.ts b/test/integration/misc/error.spec.ts index ce89cf86a..4f17181c8 100644 --- a/test/integration/misc/error.spec.ts +++ b/test/integration/misc/error.spec.ts @@ -2,7 +2,8 @@ import { RenderError } from '../../../src/util/error' import { Liquid } from '../../../src/liquid' import { resolve } from 'path' import { mock, restore } from '../../stub/mockfs' -import { throwIntendedError, rejectIntendedError } from '../../stub/util' +import { throwIntendedError } from '../../stub/util' +import { ThrowingTag, RejectingTag, ThrowsOnParseTag } from '../../stub/tags' const strictEngine = new Liquid({ strictVariables: true, @@ -13,9 +14,9 @@ const strictCatchingEngine = new Liquid({ strictVariables: true, strictFilters: true }) -strictEngine.registerTag('throwingTag', { render: throwIntendedError }) +strictEngine.registerTag('throwingTag', ThrowingTag) strictEngine.registerFilter('throwingFilter', throwIntendedError) -strictCatchingEngine.registerTag('throwingTag', { render: throwIntendedError }) +strictCatchingEngine.registerTag('throwingTag', ThrowingTag) strictCatchingEngine.registerFilter('throwingFilter', throwIntendedError) describe('error', function () { @@ -83,8 +84,8 @@ describe('error', function () { engine = new Liquid({ root: '/' }) - engine.registerTag('throwingTag', { render: throwIntendedError }) - engine.registerTag('rejectingTag', { render: rejectIntendedError }) + engine.registerTag('throwingTag', ThrowingTag) + engine.registerTag('rejectingTag', RejectingTag) engine.registerFilter('throwingFilter', throwIntendedError) }) it('should throw RenderError when tag throws', async function () { @@ -244,10 +245,7 @@ describe('error', function () { let engine: Liquid beforeEach(function () { engine = new Liquid() - engine.registerTag('throwsOnParse', { - parse: throwIntendedError, - render: () => '' - }) + engine.registerTag('throwsOnParse', ThrowsOnParseTag) }) it('should throw ParseError when filter not defined', async function () { await expect(strictEngine.parseAndRender('{{1 | a}}')).rejects.toMatchObject({ @@ -337,11 +335,7 @@ describe('error', function () { engine = new Liquid({ root: '/' }) - engine.registerTag('throwingTag', { - render: function () { - throw new Error('intended error') - } - }) + engine.registerTag('throwingTag', ThrowingTag) }) it('should throw RenderError when tag throws', function () { const src = '{%throwingTag%}' diff --git a/test/integration/tags/for.spec.ts b/test/integration/tags/for.spec.ts index 034800d56..1c8c3d7ef 100644 --- a/test/integration/tags/for.spec.ts +++ b/test/integration/tags/for.spec.ts @@ -7,9 +7,6 @@ describe('tags/for', function () { let liquid: Liquid, scope: Scope beforeEach(function () { liquid = new Liquid() - liquid.registerTag('throwingTag', { - render: function () { throw new Error('intended render error') } - }) scope = { one: 1, // eslint-disable-next-line diff --git a/test/stub/tags.ts b/test/stub/tags.ts new file mode 100644 index 000000000..6484885c0 --- /dev/null +++ b/test/stub/tags.ts @@ -0,0 +1,26 @@ +import { throwIntendedError, rejectIntendedError } from './util' +import { Tag } from '../../src/template/tag' +import type { TagToken, TopLevelToken } from '../../src/tokens' +import type { Liquid } from '../../src/liquid' + +export class ThrowingTag extends Tag { + render () { + throwIntendedError() + } +} + +export class RejectingTag extends Tag { + async render () { + await rejectIntendedError() + } +} + +export class ThrowsOnParseTag extends Tag { + constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) { + super(token, remainTokens, liquid) + throwIntendedError() + } + render () { + return '' + } +}