Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions components/framework/index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions src/configuration/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -86,6 +87,7 @@ const parseConfigurationFile = async (configurationPath) => {
try {
return yaml.load(content, {
filename: configurationPath,
schema: yamlSchema,
});
} catch (error) {
throw new ServerlessError(
Expand Down
14 changes: 14 additions & 0 deletions src/utils/yaml-schema.js
Original file line number Diff line number Diff line change
@@ -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],
});
16 changes: 16 additions & 0 deletions test/unit/components/framework/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions test/unit/src/configuration/read.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
20 changes: 20 additions & 0 deletions test/unit/src/utils/yaml-schema.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'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('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' });
});

it('constructs a Date for an explicit timestamp tag', () => {
expect(load('date: !!timestamp 2020-12-12').date).to.be.instanceOf(Date);
});
});