diff --git a/.changeset/nine-keys-hide.md b/.changeset/nine-keys-hide.md new file mode 100644 index 00000000000..3ab12efe839 --- /dev/null +++ b/.changeset/nine-keys-hide.md @@ -0,0 +1,6 @@ +--- +'@growi/pluginkit': minor +'@growi/core': minor +--- + +Add vaidator for GROWI theme plugins diff --git a/packages/core/src/interfaces/growi-theme-metadata.ts b/packages/core/src/interfaces/growi-theme-metadata.ts index 54a0c1d5d79..fa1c2ece688 100644 --- a/packages/core/src/interfaces/growi-theme-metadata.ts +++ b/packages/core/src/interfaces/growi-theme-metadata.ts @@ -19,3 +19,21 @@ export type GrowiThemeMetadata = { createBtn: string, isPresetTheme?: boolean, }; + +export const isGrowiThemeMetadata = (obj: unknown): obj is GrowiThemeMetadata => { + const objAny = obj as any; + + return objAny != null + && typeof objAny === 'object' + && Array.isArray(objAny) === false + && 'name' in objAny && typeof objAny.name === 'string' + && 'manifestKey' in objAny && typeof objAny.manifestKey === 'string' + && 'schemeType' in objAny && typeof objAny.schemeType === 'string' + && 'lightBg' in objAny && typeof objAny.lightBg === 'string' + && 'darkBg' in objAny && typeof objAny.darkBg === 'string' + && 'lightSidebar' in objAny && typeof objAny.lightSidebar === 'string' + && 'darkSidebar' in objAny && typeof objAny.darkSidebar === 'string' + && 'lightIcon' in objAny && typeof objAny.lightIcon === 'string' + && 'darkIcon' in objAny && typeof objAny.darkIcon === 'string' + && 'createBtn' in objAny && typeof objAny.createBtn === 'string'; +}; diff --git a/packages/pluginkit/src/model/growi-plugin-validation-data.ts b/packages/pluginkit/src/model/growi-plugin-validation-data.ts index 420c6d15ebe..e69282156c5 100644 --- a/packages/pluginkit/src/model/growi-plugin-validation-data.ts +++ b/packages/pluginkit/src/model/growi-plugin-validation-data.ts @@ -1,6 +1,6 @@ -import { GrowiPluginType } from '@growi/core'; +import type { GrowiPluginType, GrowiThemeMetadata } from '@growi/core'; -import { GrowiPluginDirective } from './growi-plugin-package-data'; +import type { GrowiPluginDirective } from './growi-plugin-package-data'; export type GrowiPluginValidationData = { projectDirRoot: string, @@ -13,3 +13,7 @@ export type GrowiPluginValidationData = { export type GrowiTemplatePluginValidationData = GrowiPluginValidationData & { supportingLocales: string[], } + +export type GrowiThemePluginValidationData = GrowiPluginValidationData & { + themes: GrowiThemeMetadata[], +} diff --git a/packages/pluginkit/src/v4/server/utils/theme/index.ts b/packages/pluginkit/src/v4/server/utils/theme/index.ts new file mode 100644 index 00000000000..6146b891de4 --- /dev/null +++ b/packages/pluginkit/src/v4/server/utils/theme/index.ts @@ -0,0 +1 @@ +export * from './validate-growi-plugin-directive'; diff --git a/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.spec.ts b/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.spec.ts new file mode 100644 index 00000000000..d348b0ff88d --- /dev/null +++ b/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.spec.ts @@ -0,0 +1,50 @@ +import path from 'path'; + +import { isGrowiThemeMetadata } from '@growi/core'; + +import { validateThemePluginGrowiDirective } from './validate-growi-plugin-directive'; + + +describe('validateThemePluginGrowiDirective()', () => { + + it('returns a data object', async() => { + // setup + const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/theme1'); + + // when + const data = validateThemePluginGrowiDirective(projectDirRoot); + + // then + expect(data).not.toBeNull(); + expect(data.growiPlugin).not.toBeNull(); + expect(data.themes).not.toBeNull(); + expect(isGrowiThemeMetadata(data.themes[0])).toBeTruthy(); + }); + + describe('should throw an GrowiPluginValidationError', () => { + + it("when the pkg does not have 'growiPlugin.themes' directive", () => { + // setup + const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/invalid-theme1'); + + // when + const caller = () => { validateThemePluginGrowiDirective(projectDirRoot) }; + + // then + expect(caller).toThrow("Theme plugin must have 'themes' array and that must have one or more theme metadata"); + }); + + it("when the pkg does not have 'growiPlugin.locale' directive", () => { + // setup + const projectDirRoot = path.resolve(__dirname, '../../../../../test/fixtures/example-package/invalid-theme2'); + + // when + const caller = () => { validateThemePluginGrowiDirective(projectDirRoot) }; + + // then + expect(caller).toThrow(/^Some of theme metadata are invalid:/); + }); + + }); + +}); diff --git a/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.ts b/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.ts new file mode 100644 index 00000000000..d3240a6b3ca --- /dev/null +++ b/packages/pluginkit/src/v4/server/utils/theme/validate-growi-plugin-directive.ts @@ -0,0 +1,46 @@ +import type { GrowiThemeMetadata } from '@growi/core'; +import { GrowiPluginType, isGrowiThemeMetadata } from '@growi/core'; + +import type { GrowiPluginValidationData, GrowiThemePluginValidationData } from '../../../../model'; +import { GrowiPluginValidationError } from '../../../../model'; +import { validateGrowiDirective } from '../common'; + + +/** + * An utility for theme plugin which wrap 'validateGrowiDirective' of './common' module + * @param projectDirRoot + */ +export const validateThemePluginGrowiDirective = (projectDirRoot: string): GrowiThemePluginValidationData => { + const data = validateGrowiDirective(projectDirRoot, GrowiPluginType.Theme); + + const { growiPlugin } = data; + + // check themes + if (growiPlugin.themes == null || !Array.isArray(growiPlugin.themes) || growiPlugin.themes.length === 0) { + throw new GrowiPluginValidationError( + "Theme plugin must have 'themes' array and that must have one or more theme metadata", + ); + } + + const validMetadatas: GrowiThemeMetadata[] = []; + const invalidObjects: unknown[] = []; + growiPlugin.themes.forEach((theme: unknown) => { + if (isGrowiThemeMetadata(theme)) { + validMetadatas.push(theme); + } + else { + invalidObjects.push(theme); + } + }); + + if (invalidObjects.length > 0) { + throw new GrowiPluginValidationError( + `Some of theme metadata are invalid: ${invalidObjects.toString()}`, + ); + } + + return { + ...data, + themes: validMetadatas, + }; +}; diff --git a/packages/pluginkit/test/fixtures/example-package/invalid-theme1/index.js b/packages/pluginkit/test/fixtures/example-package/invalid-theme1/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/pluginkit/test/fixtures/example-package/invalid-theme1/package.json b/packages/pluginkit/test/fixtures/example-package/invalid-theme1/package.json new file mode 100644 index 00000000000..b854d32f632 --- /dev/null +++ b/packages/pluginkit/test/fixtures/example-package/invalid-theme1/package.json @@ -0,0 +1,9 @@ +{ + "name": "example-package-invalid-theme1", + "version": "1.0.0", + "main": "index.js", + "growiPlugin": { + "schemaVersion": "4", + "types": ["theme"] + } +} diff --git a/packages/pluginkit/test/fixtures/example-package/invalid-theme2/index.js b/packages/pluginkit/test/fixtures/example-package/invalid-theme2/index.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/pluginkit/test/fixtures/example-package/invalid-theme2/package.json b/packages/pluginkit/test/fixtures/example-package/invalid-theme2/package.json new file mode 100644 index 00000000000..35951d360be --- /dev/null +++ b/packages/pluginkit/test/fixtures/example-package/invalid-theme2/package.json @@ -0,0 +1,29 @@ +{ + "name": "example-package-invalid-theme2", + "version": "1.0.0", + "main": "index.js", + "growiPlugin": { + "schemaVersion": "4", + "types": ["theme"], + "themes": [ + { + "name": "theme2-1", + "manifestKey": "src/styles/style.scss", + "schemeType": "light", + "lightBg": "#ff0000", + "darkBg": "#0000ff", + "lightSidebar": "#ffff00", + "darkSidebar": "#ff8800", + "lightIcon": "#ff0000", + "darkIcon": "#000000", + "createBtn": "#00ff00" + }, + { + "name": "theme2-2", + "manifestKey": "src/styles/style.scss", + "schemeType": "light", + "// missing some attributes": "" + } + ] + } +}