diff --git a/internal/benchmark/lib/ConfigurationLoader.js b/internal/benchmark/lib/ConfigurationLoader.js index 609245b483d..c41ba0c228a 100644 --- a/internal/benchmark/lib/ConfigurationLoader.js +++ b/internal/benchmark/lib/ConfigurationLoader.js @@ -1,4 +1,4 @@ -import yaml from "js-yaml"; +import {load as yamlLoad} from "js-yaml"; import Configuration from "./benchmark/Configuration.js"; /** @@ -37,7 +37,7 @@ export default class ConfigurationLoader { let parsedYaml; try { - parsedYaml = yaml.load(fileContents); + parsedYaml = yamlLoad(fileContents); } catch (error) { throw new Error(`Failed to parse YAML configuration: ${error.message}`); } diff --git a/internal/benchmark/package.json b/internal/benchmark/package.json index 7c7d134cd79..2a53a47789f 100644 --- a/internal/benchmark/package.json +++ b/internal/benchmark/package.json @@ -19,6 +19,6 @@ }, "devDependencies": { "eslint": "^10.11.0", - "js-yaml": "^4.3.1" + "js-yaml": "^5.4.2" } } diff --git a/package-lock.json b/package-lock.json index 21257c7565f..e80d3065e74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,13 +40,36 @@ }, "devDependencies": { "eslint": "^10.11.0", - "js-yaml": "^4.3.1" + "js-yaml": "^5.4.2" }, "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0", "npm": ">= 8" } }, + "internal/benchmark/node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "internal/documentation": { "name": "@ui5/documentation", "version": "0.0.1", @@ -11648,6 +11671,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, "funding": [ { "type": "github", @@ -18693,7 +18717,7 @@ "chalk": "^6.0.0", "data-with-position": "^0.5.0", "import-local": "^3.2.0", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.2", "open": "^11.0.4", "pretty-hrtime": "^1.0.3", "semver": "^7.8.5", @@ -18748,6 +18772,28 @@ "node": ">=20" } }, + "packages/cli/node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "packages/cli/node_modules/yargs": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", @@ -18902,7 +18948,7 @@ "escape-string-regexp": "^5.0.0", "globby": "^16.2.4", "graceful-fs": "^4.2.11", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.2", "lockfile": "^1.0.4", "make-fetch-happen": "^16.0.1", "micromatch": "^4.0.8", @@ -19273,6 +19319,28 @@ "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, + "packages/project/node_modules/js-yaml": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.2.tgz", + "integrity": "sha512-m+aqu+LwO1O6sIopafj8HUVl5aawITwZQe/yHpMCKjaWBaA/d07B/QdMb3529REftiU+RMMHL3Vlsw3hON7vWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, "packages/project/node_modules/json-parse-even-better-errors": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-6.0.0.tgz", diff --git a/packages/cli/lib/cli/commands/init.js b/packages/cli/lib/cli/commands/init.js index 5985c4f07c0..b4e88219268 100644 --- a/packages/cli/lib/cli/commands/init.js +++ b/packages/cli/lib/cli/commands/init.js @@ -12,7 +12,7 @@ initCommand.handler = async function() { const {default: init} = await import("../../init/init.js"); const {default: path} = await import("node:path"); const {writeFile} = await import("node:fs/promises"); - const {default: jsYaml} = await import("js-yaml"); + const {dump: jsYamlDump} = await import("js-yaml"); const yamlPath = path.resolve("./ui5.yaml"); if (await exists(yamlPath)) { @@ -20,7 +20,7 @@ initCommand.handler = async function() { } const projectConfig = await init(); - const yaml = jsYaml.dump(projectConfig, {quotingType: `"`}); + const yaml = jsYamlDump(projectConfig, {quoteStyle: "double"}); await writeFile(yamlPath, yaml); process.stdout.write(`Wrote ui5.yaml to ${yamlPath}:`); diff --git a/packages/cli/lib/framework/ui5YamlSchema.js b/packages/cli/lib/framework/ui5YamlSchema.js new file mode 100644 index 00000000000..05b78d30a08 --- /dev/null +++ b/packages/cli/lib/framework/ui5YamlSchema.js @@ -0,0 +1,8 @@ +import {CORE_SCHEMA, mergeTag, timestampTag} from "js-yaml"; + +// js-yaml v5 defaults to CORE_SCHEMA (YAML 1.2), which drops merge key ("<<:") +// and implicit timestamp support that were present in v4's DEFAULT_SCHEMA. +// Restore both by extending CORE_SCHEMA with the two tags. +// YAML11_SCHEMA is deliberately avoided: it additionally changes scalar resolution +// (yes/no/on/off → boolean, 0-prefixed numbers → octal). +export default CORE_SCHEMA.withTags([mergeTag, timestampTag]); diff --git a/packages/cli/lib/framework/updateYaml.js b/packages/cli/lib/framework/updateYaml.js index 12063245f91..b70e1368d03 100644 --- a/packages/cli/lib/framework/updateYaml.js +++ b/packages/cli/lib/framework/updateYaml.js @@ -1,6 +1,7 @@ import path from "node:path"; import {readFile, writeFile} from "node:fs/promises"; import {loadAll, dump} from "js-yaml"; +import ui5YamlSchema from "./ui5YamlSchema.js"; import {fromYaml, getPosition, getValue, getKind} from "data-with-position"; import {getLogger} from "@ui5/logger"; @@ -8,7 +9,8 @@ const log = getLogger("cli:framework:updateYaml"); function getProjectYamlDocument({project, configFile, configPath}) { const configs = loadAll(configFile, undefined, { - filename: configPath + filename: configPath, + schema: ui5YamlSchema }); const projectDocumentIndex = configs.findIndex((config) => { @@ -277,7 +279,7 @@ export default async function({project, configPathOverride, data}) { // Validate content before writing try { - loadAll(adoptedYaml); + loadAll(adoptedYaml, undefined, {schema: ui5YamlSchema}); } catch (err) { const error = new Error("Failed to update YAML file: " + err.message); error.name = "FrameworkUpdateYamlFailed"; diff --git a/packages/cli/package.json b/packages/cli/package.json index 34b20781c4d..455b5eec4c9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -58,7 +58,7 @@ "chalk": "^6.0.0", "data-with-position": "^0.5.0", "import-local": "^3.2.0", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.2", "open": "^11.0.4", "pretty-hrtime": "^1.0.3", "semver": "^7.8.5", diff --git a/packages/cli/test/lib/cli/commands/init.js b/packages/cli/test/lib/cli/commands/init.js index 33785f14367..24554ea6e7e 100644 --- a/packages/cli/test/lib/cli/commands/init.js +++ b/packages/cli/test/lib/cli/commands/init.js @@ -38,7 +38,8 @@ test.serial("Writes ui5.yaml to fs", async (t) => { t.is(fsWriteFileStub.getCall(0).args[0], ui5YamlPath, "Passes yaml path to write the yaml file to"); t.is(fsWriteFileStub.getCall(0).args[1], ui5Yaml, "Passes yaml content to write to fs"); - t.deepEqual(jsyamlDumpStub.getCall(0).args[1], {quotingType: `"`}, "Enforce usage of double quotes in yaml files"); + t.deepEqual(jsyamlDumpStub.getCall(0).args[1], + {quoteStyle: "double"}, "Enforce usage of double quotes in yaml files"); }); test.serial("Error: throws if ui5.yaml already exists", async (t) => { diff --git a/packages/project/lib/graph/Module.js b/packages/project/lib/graph/Module.js index 9609a49d456..5130ef2acf0 100644 --- a/packages/project/lib/graph/Module.js +++ b/packages/project/lib/graph/Module.js @@ -2,7 +2,8 @@ import fs from "graceful-fs"; import path from "node:path"; import {promisify} from "node:util"; const readFile = promisify(fs.readFile); -import jsyaml from "js-yaml"; +import {loadAll as jsyamlLoadAll} from "js-yaml"; +import ui5YamlSchema from "./helpers/ui5YamlSchema.js"; import {createReader} from "@ui5/fs/resourceFactory"; import Specification from "../specifications/Specification.js"; import {validate} from "../validation/validator.js"; @@ -318,12 +319,11 @@ class Module { let configs; try { - // Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename". - // safeLoadAll doesn't handle its parameters properly. - // See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381 - configs = jsyaml.loadAll(configFile, undefined, { + // Use ui5YamlSchema (CORE_SCHEMA + mergeTag) to preserve v4 DEFAULT_SCHEMA + // behaviour. + configs = jsyamlLoadAll(configFile, undefined, { filename: configPath, - schema: jsyaml.DEFAULT_SAFE_SCHEMA + schema: ui5YamlSchema }); } catch (err) { if (err.name === "YAMLException") { diff --git a/packages/project/lib/graph/graph.js b/packages/project/lib/graph/graph.js index f54f2e21bce..09b006aba6d 100644 --- a/packages/project/lib/graph/graph.js +++ b/packages/project/lib/graph/graph.js @@ -199,6 +199,7 @@ const utils = { const {promisify} = await import("util"); const readFile = promisify(fs.readFile); const parseYaml =(await import("js-yaml")).load; + const {default: ui5YamlSchema} = await import("./helpers/ui5YamlSchema.js"); filePath = utils.resolveConfigPath(cwd, filePath); @@ -206,7 +207,8 @@ const utils = { try { const contents = await readFile(filePath, {encoding: "utf-8"}); dependencyTree = parseYaml(contents, { - filename: filePath + filename: filePath, + schema: ui5YamlSchema }); utils.resolveProjectPaths(cwd, dependencyTree); } catch (err) { diff --git a/packages/project/lib/graph/helpers/createWorkspace.js b/packages/project/lib/graph/helpers/createWorkspace.js index 4b8a41d39dc..c166dadf12e 100644 --- a/packages/project/lib/graph/helpers/createWorkspace.js +++ b/packages/project/lib/graph/helpers/createWorkspace.js @@ -74,7 +74,8 @@ async function readWorkspaceConfigFile(filePath) { } = await import("graceful-fs"); const {promisify} = await import("node:util"); const readFile = promisify(fs.readFile); - const jsyaml = await import("js-yaml"); + const {loadAll: jsyamlLoadAll} = await import("js-yaml"); + const {default: ui5YamlSchema} = await import("./ui5YamlSchema.js"); let fileContent; try { @@ -87,8 +88,9 @@ async function readWorkspaceConfigFile(filePath) { } let configs; try { - configs = jsyaml.loadAll(fileContent, undefined, { + configs = jsyamlLoadAll(fileContent, undefined, { filename: filePath, + schema: ui5YamlSchema }); } catch (err) { throw new Error(`Failed to parse workspace configuration at ${filePath}\nError: ${err.message}`); diff --git a/packages/project/lib/graph/helpers/ui5YamlSchema.js b/packages/project/lib/graph/helpers/ui5YamlSchema.js new file mode 100644 index 00000000000..05b78d30a08 --- /dev/null +++ b/packages/project/lib/graph/helpers/ui5YamlSchema.js @@ -0,0 +1,8 @@ +import {CORE_SCHEMA, mergeTag, timestampTag} from "js-yaml"; + +// js-yaml v5 defaults to CORE_SCHEMA (YAML 1.2), which drops merge key ("<<:") +// and implicit timestamp support that were present in v4's DEFAULT_SCHEMA. +// Restore both by extending CORE_SCHEMA with the two tags. +// YAML11_SCHEMA is deliberately avoided: it additionally changes scalar resolution +// (yes/no/on/off → boolean, 0-prefixed numbers → octal). +export default CORE_SCHEMA.withTags([mergeTag, timestampTag]); diff --git a/packages/project/package.json b/packages/project/package.json index 3c6b0c842bb..932409cf32b 100644 --- a/packages/project/package.json +++ b/packages/project/package.json @@ -71,7 +71,7 @@ "escape-string-regexp": "^5.0.0", "globby": "^16.2.4", "graceful-fs": "^4.2.11", - "js-yaml": "^4.3.1", + "js-yaml": "^5.4.2", "lockfile": "^1.0.4", "make-fetch-happen": "^16.0.1", "micromatch": "^4.0.8", diff --git a/packages/project/test/fixtures/application.a/ui5-merge-keys.yaml b/packages/project/test/fixtures/application.a/ui5-merge-keys.yaml new file mode 100644 index 00000000000..c8709bc4b43 --- /dev/null +++ b/packages/project/test/fixtures/application.a/ui5-merge-keys.yaml @@ -0,0 +1,33 @@ +--- +specVersion: "4.0" +metadata: + name: application.a +type: application +# Shared configuration anchor — used to verify that merge keys (<<:) are parsed +# correctly by js-yaml v5 with the ui5YamlSchema (CORE_SCHEMA + mergeTag). +server: + customMiddleware: + - name: middleware-base + afterMiddleware: compression + configuration: &sharedCfg + debug: true + port: 3000 + timeout: 30 + - name: middleware-extended + afterMiddleware: middleware-base + configuration: + <<: *sharedCfg + timeout: 60 +builder: + customTasks: + - name: task-base + afterTask: replaceVersion + configuration: &taskCfg + debug: true + minify: false + - name: task-extended + afterTask: task-base + configuration: + <<: *taskCfg + minify: true + extraKey: added-by-local-block diff --git a/packages/project/test/lib/graph/Module.js b/packages/project/test/lib/graph/Module.js index c87aac18542..abd57d90860 100644 --- a/packages/project/test/lib/graph/Module.js +++ b/packages/project/test/lib/graph/Module.js @@ -438,6 +438,40 @@ test("Corrupt configuration in file", async (t) => { "Threw with parsing error"); }); +test("Merge keys (<<:) in ui5.yaml are resolved and not left as literal keys", async (t) => { + // Regression test: js-yaml v5 CORE_SCHEMA drops merge key ("<<:") and timestamp + // support from v4's DEFAULT_SCHEMA. ui5YamlSchema restores both with mergeTag and + // timestampTag on top of CORE_SCHEMA. + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-merge-keys.yaml" + }); + const {project} = await ui5Module.getSpecifications(); + const cfg = project.getConfig(); + + // --- middleware: anchor defined on middleware-base, merged into middleware-extended --- + const mwExtended = cfg.server.customMiddleware[1].configuration; + // Keys from anchor must be merged in + t.is(mwExtended.debug, true, "Anchor key 'debug' merged into middleware configuration"); + t.is(mwExtended.port, 3000, "Anchor key 'port' merged into middleware configuration"); + // Local key overrides anchor value + t.is(mwExtended.timeout, 60, "Local 'timeout' overrides merged anchor value"); + // No literal "<<" key must remain in the parsed object + t.false("<<" in mwExtended, "Merge key '<<' is resolved, not left as a literal mapping key"); + + // --- tasks: anchor defined on task-base, merged into task-extended --- + const taskExtended = cfg.builder.customTasks[1].configuration; + t.is(taskExtended.debug, true, "Anchor key 'debug' merged into task configuration"); + // Local key overrides anchor value + t.true(taskExtended.minify, "Local 'minify: true' overrides merged anchor value 'false'"); + // Extra key added in the local block must also be present + t.is(taskExtended.extraKey, "added-by-local-block", "Additional local key is preserved"); + t.false("<<" in taskExtended, "Merge key '<<' is resolved, not left as a literal mapping key"); +}); + + test("Empty configuration in file", async (t) => { const ui5Module = new Module({ id: "application.a.id",