diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index b1125b794..31a9d068f 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -45,7 +45,7 @@ class LibraryBuilder extends AbstractBuilder { workspace: resourceCollections.workspace, options: { copyright: project.metadata.copyright, - pattern: "/resources/**/*.{js,json,library}" + pattern: "/resources/**/*.{js,library,less,theme}" } }); }); @@ -56,7 +56,7 @@ class LibraryBuilder extends AbstractBuilder { workspace: resourceCollections.workspace, options: { version: project.version, - pattern: "/resources/**/*.{js,json,library}" + pattern: "/resources/**/*.{js,json,library,less,theme}" } }); }); diff --git a/lib/types/themeLibrary/ThemeLibraryBuilder.js b/lib/types/themeLibrary/ThemeLibraryBuilder.js new file mode 100644 index 000000000..2af2ef49e --- /dev/null +++ b/lib/types/themeLibrary/ThemeLibraryBuilder.js @@ -0,0 +1,59 @@ +const AbstractBuilder = require("../AbstractBuilder"); +const tasks = { // can't require index.js due to circular dependency + generateComponentPreload: require("../../tasks/bundlers/generateComponentPreload"), + generateFlexChangesBundle: require("../../tasks/bundlers/generateFlexChangesBundle"), + generateBundle: require("../../tasks/bundlers/generateBundle"), + generateLibraryPreload: require("../../tasks/bundlers/generateLibraryPreload"), + generateManifestBundle: require("../../tasks/bundlers/generateManifestBundle"), + generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"), + buildThemes: require("../../tasks/buildThemes"), + createDebugFiles: require("../../tasks/createDebugFiles"), + generateJsdoc: require("../../tasks/jsdoc/generateJsdoc"), + executeJsdocSdkTransformation: require("../../tasks/jsdoc/executeJsdocSdkTransformation"), + generateLibraryManifest: require("../../tasks/generateLibraryManifest"), + generateVersionInfo: require("../../tasks/generateVersionInfo"), + replaceCopyright: require("../../tasks/replaceCopyright"), + replaceVersion: require("../../tasks/replaceVersion"), + uglify: require("../../tasks/uglify") +}; + +class ThemeLibraryBuilder extends AbstractBuilder { + addStandardTasks({resourceCollections, project, log, buildContext}) { + this.addTask("replaceCopyright", () => { + const replaceCopyright = tasks.replaceCopyright; + return replaceCopyright({ + workspace: resourceCollections.workspace, + options: { + copyright: project.metadata.copyright, + pattern: "/resources/**/*.{less,theme}" + } + }); + }); + + this.addTask("replaceVersion", () => { + const replaceVersion = tasks.replaceVersion; + return replaceVersion({ + workspace: resourceCollections.workspace, + options: { + version: project.version, + pattern: "/resources/**/*.{less,theme}" + } + }); + }); + + this.addTask("buildThemes", () => { + const buildThemes = tasks.buildThemes; + return buildThemes({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name, + librariesPattern: "/resources/**/*.library", + inputPattern: "/resources/**/themes/*/library.source.less" + } + }); + }); + } +} + +module.exports = ThemeLibraryBuilder; diff --git a/lib/types/themeLibrary/ThemeLibraryFormatter.js b/lib/types/themeLibrary/ThemeLibraryFormatter.js new file mode 100644 index 000000000..4e164a2e2 --- /dev/null +++ b/lib/types/themeLibrary/ThemeLibraryFormatter.js @@ -0,0 +1,88 @@ +const log = require("@ui5/logger").getLogger("types:themeLibrary:ThemeLibraryFormatter"); +const path = require("path"); +const AbstractUi5Formatter = require("../AbstractUi5Formatter"); + + +class ThemeLibraryFormatter extends AbstractUi5Formatter { + /** + * Formats and validates the project + * + * @returns {Promise} + */ + async format() { + const project = this._project; + await this.validate(); + + log.verbose("Formatting theme-library project %s...", project.metadata.name); + project.resources.pathMappings = { + "/resources/": project.resources.configuration.paths.src + }; + + if (project.resources.configuration.paths.test) { + // Directory 'test' is somewhat optional for theme-libraries + project.resources.pathMappings["/test-resources/"] = project.resources.configuration.paths.test; + } else { + log.verbose(`Ignoring 'test' directory for project ${project.metadata.name}.` + + "Either no setting was provided or the path not found."); + } + } + + /** + * Validates the project + * + * @returns {Promise} resolves if successfully validated + * @throws {Error} if validation fails + */ + validate() { + const project = this._project; + return Promise.resolve().then(() => { + if (!project) { + throw new Error("Project is undefined"); + } else if (project.specVersion === "0.1" || project.specVersion === "1.0") { + throw new Error(`theme-library type requires "specVersion" 1.1 or higher. Project "specVersion" is: ${project.specVersion}`); + } else if (!project.metadata || !project.metadata.name) { + throw new Error(`"metadata.name" configuration is missing for project ${project.id}`); + } else if (!project.type) { + throw new Error(`"type" configuration is missing for project ${project.id}`); + } else if (project.version === undefined) { + throw new Error(`"version" is missing for project ${project.id}`); + } + if (!project.resources) { + project.resources = {}; + } + if (!project.resources.configuration) { + project.resources.configuration = {}; + } + if (!project.resources.configuration.paths) { + project.resources.configuration.paths = {}; + } + if (!project.resources.configuration.paths.src) { + project.resources.configuration.paths.src = "src"; + } + if (!project.resources.configuration.paths.test) { + project.resources.configuration.paths.test = "test"; + } + + const absoluteSrcPath = path.join(project.path, project.resources.configuration.paths.src); + const absoluteTestPath = path.join(project.path, project.resources.configuration.paths.test); + return Promise.all([ + this.dirExists(absoluteSrcPath).then(function(bExists) { + if (!bExists) { + throw new Error(`Could not find source directory of project ${project.id}: ` + + `${absoluteSrcPath}`); + } + }), + this.dirExists(absoluteTestPath).then(function(bExists) { + if (!bExists) { + log.verbose(`Could not find (optional) test directory of project ${project.id}: ` + + `${absoluteSrcPath}`); + // Current signal to following consumers that "test" is not available is null + project.resources.configuration.paths.test = null; + } + }) + ]); + }); + } +} + +module.exports = ThemeLibraryFormatter; diff --git a/lib/types/themeLibrary/themeLibraryType.js b/lib/types/themeLibrary/themeLibraryType.js new file mode 100644 index 000000000..f1c28d903 --- /dev/null +++ b/lib/types/themeLibrary/themeLibraryType.js @@ -0,0 +1,15 @@ +const ThemeLibraryFormatter = require("./ThemeLibraryFormatter"); +const ThemeLibraryBuilder = require("./ThemeLibraryBuilder"); + +module.exports = { + format: function(project) { + return new ThemeLibraryFormatter({project}).format(); + }, + build: function({resourceCollections, tasks, project, parentLogger, buildContext}) { + return new ThemeLibraryBuilder({resourceCollections, project, parentLogger, buildContext}).build(tasks); + }, + + // Export type classes for extensibility + Builder: ThemeLibraryBuilder, + Formatter: ThemeLibraryFormatter +}; diff --git a/lib/types/typeRepository.js b/lib/types/typeRepository.js index 7299a903d..a2dcde262 100644 --- a/lib/types/typeRepository.js +++ b/lib/types/typeRepository.js @@ -1,11 +1,13 @@ const applicationType = require("./application/applicationType"); const libraryType = require("./library/libraryType"); +const themeLibraryType = require("./themeLibrary/themeLibraryType"); const moduleType = require("./module/moduleType"); const types = { - application: applicationType, - library: libraryType, - module: moduleType + "application": applicationType, + "library": libraryType, + "theme-library": themeLibraryType, + "module": moduleType }; /** diff --git a/test/fixtures/theme.library.e/package.json b/test/fixtures/theme.library.e/package.json new file mode 100644 index 000000000..d48d4d185 --- /dev/null +++ b/test/fixtures/theme.library.e/package.json @@ -0,0 +1,10 @@ +{ + "name": "theme.library.e", + "version": "1.0.0", + "description": "Simple SAPUI5 based library - test for dev dependencies", + "devDependencies": { + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theme b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theme new file mode 100644 index 000000000..4c62f2611 --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theme @@ -0,0 +1,9 @@ + + + + my_theme + me + ${copyright} + ${version} + + \ No newline at end of file diff --git a/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theming b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theming new file mode 100644 index 000000000..83b6c785a --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/.theming @@ -0,0 +1,27 @@ +{ + "sEntity": "Theme", + "sId": "sap_belize", + "oExtends": "base", + "sVendor": "SAP", + "aBundled": ["sap_belize_plus"], + "mCssScopes": { + "library": { + "sBaseFile": "library", + "sEmbeddingMethod": "APPEND", + "aScopes": [ + { + "sLabel": "Contrast", + "sSelector": "sapContrast", + "sEmbeddedFile": "sap_belize_plus.library", + "sEmbeddedCompareFile": "library", + "sThemeIdSuffix": "Contrast", + "sThemability": "PUBLIC", + "aThemabilityFilter": [ + "Color" + ], + "rExcludeSelector": "\\.sapContrastPlus\\W" + } + ] + } + } +} diff --git a/test/fixtures/theme.library.e/src/theme/library/e/my_theme/library.source.less b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/library.source.less new file mode 100644 index 000000000..ba66d46d7 --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/my_theme/library.source.less @@ -0,0 +1,18 @@ +/*! + * ${copyright} + */ + +* { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-touch-callout: none; + -webkit-text-size-adjust: none; + -ms-text-size-adjust: none; +} + +.sapUiBody { + width: 100%; + height: 100%; + margin: 0; + font-family: @sapUiFontFamily; + font-size: 1rem; +} diff --git a/test/fixtures/theme.library.e/test/theme/library/e/Test.html b/test/fixtures/theme.library.e/test/theme/library/e/Test.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/theme.library.e/ui5.yaml b/test/fixtures/theme.library.e/ui5.yaml new file mode 100644 index 000000000..cf89c2432 --- /dev/null +++ b/test/fixtures/theme.library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "1.1" +type: theme-library +metadata: + name: theme.library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/lib/types/themeLibrary/ThemeLibraryFormatter.js b/test/lib/types/themeLibrary/ThemeLibraryFormatter.js new file mode 100644 index 000000000..892c9aa9c --- /dev/null +++ b/test/lib/types/themeLibrary/ThemeLibraryFormatter.js @@ -0,0 +1,186 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +const ThemeLibraryFormatter = require("../../../../lib/types/themeLibrary/ThemeLibraryFormatter"); + +const themeLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "theme.library.e"); +const themeLibraryETree = { + id: "theme.library.e.id", + version: "1.0.0", + path: themeLibraryEPath, + dependencies: [], + _level: 0, + specVersion: "1.1", + type: "theme-library", + metadata: { + name: "theme.library.e", + copyright: "UI development toolkit for HTML5 (OpenUI5)\n * (c) Copyright 2009-xxx SAP SE or an SAP affiliate " + + "company.\n * Licensed under the Apache License, Version 2.0 - see LICENSE.txt." + }, + resources: { + configuration: { + paths: { + src: "src", + test: "test" + } + } + } +}; + +function clone(o) { + return JSON.parse(JSON.stringify(o)); +} + +test("validate: project not defined", async (t) => { + const themeLibraryFormatter = new ThemeLibraryFormatter({project: null}); + + // error is thrown because project is not defined (null) + const error = await t.throwsAsync(themeLibraryFormatter.validate()); + t.deepEqual(error.message, "Project is undefined", "Correct exception thrown"); +}); + +test("validate: wrong specVersion (0.1)", async (t) => { + const themeLibraryFormatter = new ThemeLibraryFormatter({project: { + specVersion: "0.1" + }}); + + // error is thrown because project is not defined (null) + const error = await t.throwsAsync(themeLibraryFormatter.validate()); + t.deepEqual(error.message, `theme-library type requires "specVersion" 1.1 or higher. Project "specVersion" is: 0.1`, "Correct exception thrown"); +}); + +test("validate: wrong specVersion (1.0)", async (t) => { + const themeLibraryFormatter = new ThemeLibraryFormatter({project: { + specVersion: "1.0" + }}); + + // error is thrown because project is not defined (null) + const error = await t.throwsAsync(themeLibraryFormatter.validate()); + t.deepEqual(error.message, `theme-library type requires "specVersion" 1.1 or higher. Project "specVersion" is: 1.0`, "Correct exception thrown"); +}); + +test("validate: empty version", async (t) => { + const myProject = clone(themeLibraryETree); + myProject.version = undefined; + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + + // error is thrown because project's version is not defined + const error = await t.throwsAsync(themeLibraryFormatter.validate(myProject)); + t.deepEqual(error.message, `"version" is missing for project theme.library.e.id`, "Correct exception thrown"); +}); + +test("validate: empty type", async (t) => { + const myProject = clone(themeLibraryETree); + myProject.type = undefined; + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + + // error is thrown because project's type is not defined + const error = await t.throwsAsync(themeLibraryFormatter.validate(myProject)); + t.deepEqual(error.message, `"type" configuration is missing for project theme.library.e.id`, + "Correct exception thrown"); +}); + + +test("validate: empty metadata", async (t) => { + const myProject = clone(themeLibraryETree); + myProject.metadata = undefined; + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + + // error is thrown because project's metadata is not defined + const error = await t.throwsAsync(themeLibraryFormatter.validate(myProject)); + t.deepEqual(error.message, `"metadata.name" configuration is missing for project theme.library.e.id`, + "Correct exception thrown"); +}); + +test("validate: empty resources", async (t) => { + const myProject = clone(themeLibraryETree); + myProject.resources = undefined; + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + + await themeLibraryFormatter.validate(myProject); + t.deepEqual(myProject.resources.configuration.paths.src, "src", "default src directory is set"); + t.deepEqual(myProject.resources.configuration.paths.test, "test", "default test directory is set"); +}); + +test("validate: src directory does not exist", async (t) => { + const myProject = clone(themeLibraryETree); + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + const dirExists = sinon.stub(themeLibraryFormatter, "dirExists"); + dirExists.onFirstCall().resolves(false); + dirExists.onSecondCall().resolves(true); + + const error = await await t.throwsAsync(themeLibraryFormatter.validate(myProject)); + t.regex(error.message, /^Could not find source directory of project theme\.library\.e\.id: (?!(undefined))+/, + "Missing source directory caused error"); +}); + +test("validate: test directory does not exist", async (t) => { + const myProject = clone(themeLibraryETree); + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + const dirExists = sinon.stub(themeLibraryFormatter, "dirExists"); + dirExists.onFirstCall().resolves(true); + dirExists.onSecondCall().resolves(false); + + await themeLibraryFormatter.validate(myProject); + // Missing test directory is not an error + t.deepEqual(myProject.resources.configuration.paths.test, null, "Project test path configuration is set to nul"); +}); + +test("format: copyright already configured", async (t) => { + const myProject = clone(themeLibraryETree); + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + sinon.stub(themeLibraryFormatter, "validate").resolves(); + + await themeLibraryFormatter.format(); + t.deepEqual(myProject.metadata.copyright, themeLibraryETree.metadata.copyright, "Copyright was not altered"); +}); + +test("format: formats correctly", async (t) => { + const myProject = clone(themeLibraryETree); + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + sinon.stub(themeLibraryFormatter, "validate").resolves(); + + await themeLibraryFormatter.format(); + t.deepEqual(myProject, { + id: "theme.library.e.id", + version: "1.0.0", + path: themeLibraryEPath, + dependencies: [], + _level: 0, + specVersion: "1.1", + type: "theme-library", + metadata: { + name: "theme.library.e", + copyright: + "UI development toolkit for HTML5 (OpenUI5)\n * (c) Copyright 2009-xxx SAP SE or an SAP affiliate " + + "company.\n * Licensed under the Apache License, Version 2.0 - see LICENSE.txt." + }, + resources: { + configuration: { + paths: { + src: "src", + test: "test" + } + }, + pathMappings: { + "/resources/": "src", + "/test-resources/": "test" + } + } + }, "Project got formatted correctly"); +}); + +test("format: configuration test path", async (t) => { + const myProject = clone(themeLibraryETree); + const themeLibraryFormatter = new ThemeLibraryFormatter({project: myProject}); + sinon.stub(themeLibraryFormatter, "validate").resolves(); + myProject.resources.configuration.paths.test = null; + await themeLibraryFormatter.format(); + + t.falsy(myProject.resources.pathMappings["/test-resources/"], "test-resources pathMapping is not set"); +});