From d1965baeb5b8dd34a8a3bae583743db9147e640a Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 6 Sep 2026 15:43:53 +0100 Subject: [PATCH 1/2] Fix YAML date coercion --- components/framework/index.js | 7 ++++--- src/configuration/read.js | 2 ++ src/utils/yaml-schema.js | 14 ++++++++++++++ test/unit/components/framework/index.test.js | 16 ++++++++++++++++ test/unit/src/configuration/read.test.js | 12 ++++++++++++ test/unit/src/utils/yaml-schema.test.js | 19 +++++++++++++++++++ 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/utils/yaml-schema.js create mode 100644 test/unit/src/utils/yaml-schema.test.js diff --git a/components/framework/index.js b/components/framework/index.js index 91d2a38..e927c70 100644 --- a/components/framework/index.js +++ b/components/framework/index.js @@ -1,6 +1,7 @@ 'use strict'; const YAML = require('js-yaml'); +const yamlSchema = require('../../src/utils/yaml-schema'); const path = require('path'); const spawn = require('../../src/utils/spawn'); const redactArgs = require('../../src/utils/redact-args'); @@ -162,7 +163,7 @@ class ServerlessFramework { async retrieveFunctions() { const { stdout: printOutput } = await this.exec('serverless', ['print']); try { - return YAML.load(printOutput.toString()).functions || {}; + return YAML.load(printOutput.toString(), { schema: yamlSchema }).functions || {}; } catch { throw new Error(`Could not retrieve functions from configuration:\n${printOutput}`); } @@ -327,7 +328,7 @@ class ServerlessFramework { async retrieveOutputs() { const { stdout: infoOutput } = await this.exec('serverless', ['info', '--verbose']); try { - return YAML.load(infoOutput.toString())['Stack Outputs']; + return YAML.load(infoOutput.toString(), { schema: yamlSchema })['Stack Outputs']; } catch { if (infoOutput.toString()) { // Try to extract the section with `Stack Outputs` and parse it @@ -336,7 +337,7 @@ class ServerlessFramework { const res = infoOutput.toString().match(/Stack Outputs:\n(( {2}[ \S]+\n)+)/); if (res) { try { - return YAML.load(res[1]); + return YAML.load(res[1], { schema: yamlSchema }); } catch { // Pass to generic error } diff --git a/src/configuration/read.js b/src/configuration/read.js index bf28a43..d020462 100644 --- a/src/configuration/read.js +++ b/src/configuration/read.js @@ -5,6 +5,7 @@ const { createRequire } = require('module'); const path = require('path'); const fsp = require('fs').promises; const yaml = require('js-yaml'); +const yamlSchema = require('../utils/yaml-schema'); const spawn = require('../utils/spawn'); const ServerlessError = require('../serverless-error'); @@ -86,6 +87,7 @@ const parseConfigurationFile = async (configurationPath) => { try { return yaml.load(content, { filename: configurationPath, + schema: yamlSchema, }); } catch (error) { throw new ServerlessError( diff --git a/src/utils/yaml-schema.js b/src/utils/yaml-schema.js new file mode 100644 index 0000000..990ff23 --- /dev/null +++ b/src/utils/yaml-schema.js @@ -0,0 +1,14 @@ +'use strict'; + +const yaml = require('js-yaml'); + +// Drop implicit timestamps so date-shaped plain scalars and mapping keys stay strings; an +// explicit `!!timestamp` tag still constructs a Date +const implicit = yaml.DEFAULT_SCHEMA.implicit.filter( + (type) => type.tag !== 'tag:yaml.org,2002:timestamp' +); + +module.exports = new yaml.Schema({ + implicit, + explicit: [...yaml.DEFAULT_SCHEMA.explicit, yaml.types.timestamp], +}); diff --git a/test/unit/components/framework/index.test.js b/test/unit/components/framework/index.test.js index 3fb00da..5fd96d5 100644 --- a/test/unit/components/framework/index.test.js +++ b/test/unit/components/framework/index.test.js @@ -196,6 +196,22 @@ describe('test/unit/components/framework/index.test.js', () => { expect(context.outputs).to.deep.equal({ Key: 'Output' }); }); + it('correctly handles refresh-outputs with date-shaped outputs', async () => { + const spawnStub = createSpawnStub( + createClassicSpawnResult({ + stdout: 'region: us-east-1\n\nStack Outputs:\n ReleaseDate: 2026-09-06', + }) + ); + const FrameworkComponent = loadFrameworkComponent(spawnStub); + + const context = await getContext(); + const component = new FrameworkComponent('some-id', context, { path: 'path' }); + context.state.detectedFrameworkVersion = '9.9.9'; + await component.refreshOutputs(); + + expect(context.outputs).to.deep.equal({ ReleaseDate: '2026-09-06' }); + }); + it('correctly recognizes region in inputs', async () => { const spawnStub = createSpawnStub(createClassicSpawnResult({ stdout: INFO_OUTPUT })); const FrameworkComponent = loadFrameworkComponent(spawnStub); diff --git a/test/unit/src/configuration/read.test.js b/test/unit/src/configuration/read.test.js index 847120a..e8459cc 100644 --- a/test/unit/src/configuration/read.test.js +++ b/test/unit/src/configuration/read.test.js @@ -41,6 +41,18 @@ describe('test/unit/src/configuration/read.test.js', () => { }); }); + it('should keep date-shaped values as strings', async () => { + configurationPath = 'serverless-compose.yml'; + await fsp.writeFile( + configurationPath, + 'name: test-yml\nservices:\n resources:\n path: resources\n params:\n policyVersion: 2012-10-17\n' + ); + expect(await readConfiguration(configurationPath)).to.deep.equal({ + name: 'test-yml', + services: { resources: { path: 'resources', params: { policyVersion: '2012-10-17' } } }, + }); + }); + it('should read "serverless-compose.json"', async () => { configurationPath = 'serverless-compose.json'; const configuration = { diff --git a/test/unit/src/utils/yaml-schema.test.js b/test/unit/src/utils/yaml-schema.test.js new file mode 100644 index 0000000..b3dc76c --- /dev/null +++ b/test/unit/src/utils/yaml-schema.test.js @@ -0,0 +1,19 @@ +'use strict'; + +const expect = require('chai').expect; +const yaml = require('js-yaml'); +const yamlSchema = require('../../../../src/utils/yaml-schema'); + +const load = (input) => yaml.load(input, { schema: yamlSchema }); + +describe('test/unit/src/utils/yaml-schema.test.js', () => { + it('keeps date-shaped plain scalars and mapping keys as strings', () => { + expect(load('date: 2012-10-17').date).to.equal('2012-10-17'); + expect(load('dateTime: 2020-12-12T00:00:00Z').dateTime).to.equal('2020-12-12T00:00:00Z'); + expect(load('map:\n 2012-10-17: value').map).to.deep.equal({ '2012-10-17': 'value' }); + }); + + it('constructs a Date for an explicit timestamp tag', () => { + expect(load('date: !!timestamp 2020-12-12').date).to.be.instanceOf(Date); + }); +}); From 8e43b9cbcb5da1d1e4dfc66a65be15f7b8580670 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 6 Sep 2026 15:57:13 +0100 Subject: [PATCH 2/2] Cover space-separated datetimes in the schema test --- test/unit/src/utils/yaml-schema.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/src/utils/yaml-schema.test.js b/test/unit/src/utils/yaml-schema.test.js index b3dc76c..06c9c3b 100644 --- a/test/unit/src/utils/yaml-schema.test.js +++ b/test/unit/src/utils/yaml-schema.test.js @@ -10,6 +10,7 @@ describe('test/unit/src/utils/yaml-schema.test.js', () => { it('keeps date-shaped plain scalars and mapping keys as strings', () => { expect(load('date: 2012-10-17').date).to.equal('2012-10-17'); expect(load('dateTime: 2020-12-12T00:00:00Z').dateTime).to.equal('2020-12-12T00:00:00Z'); + expect(load('spaced: 2020-12-12 00:00:00').spaced).to.equal('2020-12-12 00:00:00'); expect(load('map:\n 2012-10-17: value').map).to.deep.equal({ '2012-10-17': 'value' }); });