diff --git a/.changeset/ninety-shirts-float.md b/.changeset/ninety-shirts-float.md new file mode 100644 index 0000000000..4ff9a39560 --- /dev/null +++ b/.changeset/ninety-shirts-float.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Align with type declaration of template filter/global function by supporting undefined as return value. diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts index 051096db57..53bf67fd87 100644 --- a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.test.ts @@ -107,8 +107,9 @@ describe('SecureTemplater', () => { const mockFilter1 = jest.fn(() => 'filtered text'); const mockFilter2 = jest.fn((var1, var2) => `${var1} ${var2}`); const mockFilter3 = jest.fn((var1, var2) => ({ var1, var2 })); + const mockFilter4 = jest.fn(() => undefined); const renderWith = await SecureTemplater.loadRenderer({ - templateFilters: { mockFilter1, mockFilter2, mockFilter3 }, + templateFilters: { mockFilter1, mockFilter2, mockFilter3, mockFilter4 }, }); const renderWithout = await SecureTemplater.loadRenderer(); @@ -131,6 +132,7 @@ describe('SecureTemplater', () => { var2: 'another extra arg', }), ); + expect(renderWith('${{ inputValue | mockFilter4 }}', ctx)).toBe(''); expect(() => renderWithout('${{ inputValue | mockFilter1 }}', ctx)).toThrow( /Error: filter not found: mockFilter1/, @@ -152,8 +154,9 @@ describe('SecureTemplater', () => { const mockGlobal1 = jest.fn(() => 'awesome global function'); const mockGlobal2 = 'foo'; const mockGlobal3 = 123456; + const mockGlobal4 = jest.fn(() => undefined); const renderWith = await SecureTemplater.loadRenderer({ - templateGlobals: { mockGlobal1, mockGlobal2, mockGlobal3 }, + templateGlobals: { mockGlobal1, mockGlobal2, mockGlobal3, mockGlobal4 }, }); const renderWithout = await SecureTemplater.loadRenderer(); @@ -164,6 +167,7 @@ describe('SecureTemplater', () => { ); expect(renderWith('${{ mockGlobal2 }}', ctx)).toBe('foo'); expect(renderWith('${{ mockGlobal3 }}', ctx)).toBe('123456'); + expect(renderWith('${{ mockGlobal4() }}', ctx)).toBe(''); expect(() => renderWithout('${{ mockGlobal1() }}', ctx)).toThrow( /Error: Unable to call `mockGlobal1`/, diff --git a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts index 6860902c78..b24d859bb7 100644 --- a/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts +++ b/plugins/scaffolder-backend/src/lib/templating/SecureTemplater.ts @@ -52,14 +52,17 @@ const { render, renderCompat } = (() => { }); compatEnv.addFilter('jsonify', compatEnv.getFilter('dump')); + const handleFunctionResult = (value) => { + return value === '' ? undefined : JSON.parse(value); + }; for (const name of JSON.parse(availableTemplateFilters)) { - env.addFilter(name, (...args) => JSON.parse(callFilter(name, args))); + env.addFilter(name, (...args) => handleFunctionResult(callFilter(name, args))); } for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) { env.addGlobal(name, value); } for (const name of JSON.parse(availableTemplateCallbacks)) { - env.addGlobal(name, (...args) => JSON.parse(callGlobal(name, args))); + env.addGlobal(name, (...args) => handleFunctionResult(callGlobal(name, args))); } let uninstallCompat = undefined; @@ -187,7 +190,8 @@ export class SecureTemplater { if (!Object.hasOwn(templateFilters, filterName)) { return ''; } - return JSON.stringify(templateFilters[filterName](...args)); + const rz = templateFilters[filterName](...args); + return rz === undefined ? '' : JSON.stringify(rz); }, ); @@ -201,7 +205,8 @@ export class SecureTemplater { if (typeof global !== 'function') { return ''; } - return JSON.stringify(global(...args)); + const rz = global(...args); + return rz === undefined ? '' : JSON.stringify(rz); }, );