Browse Source

WIP: implement pluginkit utilities

Yuki Takei 2 years ago
parent
commit
e8818b32a4

+ 1 - 0
packages/pluginkit/.eslintignore

@@ -0,0 +1 @@
+/dist/**

+ 5 - 0
packages/pluginkit/.eslintrc.js

@@ -0,0 +1,5 @@
+module.exports = {
+  extends: [
+    'plugin:vitest/recommended',
+  ],
+};

+ 1 - 0
packages/pluginkit/.gitignore

@@ -0,0 +1 @@
+/dist

+ 23 - 0
packages/pluginkit/package.json

@@ -0,0 +1,23 @@
+{
+  "name": "@growi/pluginkit",
+  "version": "6.1.4-RC.0",
+  "license": "MIT",
+  "main": "dist/index.js",
+  "module": "dist/index.mjs",
+  "types": "dist/index.d.ts",
+  "scripts": {
+    "build": "vite build",
+    "clean": "npx -y shx rm -rf dist",
+    "dev": "vite build --mode dev",
+    "watch": "yarn dev -w --emptyOutDir=false",
+    "lint:js": "yarn eslint **/*.{js,ts}",
+    "lint:typecheck": "tsc",
+    "lint": "npm-run-all -p lint:*",
+    "test": "vitest run --coverage"
+  },
+  "dependencies": {
+    "extensible-custom-error": "^0.0.7"
+  },
+  "devDependencies": {
+  }
+}

+ 6 - 0
packages/pluginkit/src/consts/types.ts

@@ -0,0 +1,6 @@
+export const GrowiPluginType = {
+  SCRIPT: 'script',
+  TEMPLATE: 'template',
+  THEME: 'theme',
+} as const;
+export type GrowiPluginType = typeof GrowiPluginType[keyof typeof GrowiPluginType];

+ 0 - 0
packages/pluginkit/src/index.ts


+ 12 - 0
packages/pluginkit/src/model/growi-plugin-validation-data.ts

@@ -0,0 +1,12 @@
+import { GrowiPluginType } from '../consts/types';
+
+export type GrowiPluginValidationData = {
+  projectDirRoot: string,
+  schemaVersion?: number,
+  expectedPluginType?: GrowiPluginType,
+  actualPluginTypes?: GrowiPluginType[],
+};
+
+export type GrowiTemplatePluginValidationData = GrowiPluginValidationData & {
+  supportingLocales: string[],
+}

+ 15 - 0
packages/pluginkit/src/model/growi-plugin-validation-error.ts

@@ -0,0 +1,15 @@
+import ExtensibleCustomError from 'extensible-custom-error';
+
+import type { GrowiPluginValidationData } from './growi-plugin-validation-data';
+
+
+export class GrowiPluginValidationError<E extends GrowiPluginValidationData = GrowiPluginValidationData> extends ExtensibleCustomError {
+
+  data?: E;
+
+  constructor(message: string, data?: E) {
+    super(message);
+    this.data = data;
+  }
+
+}

+ 5 - 0
packages/pluginkit/src/server/utils/v4/index.ts

@@ -0,0 +1,5 @@
+
+
+export const validate = async(projectDirRoot: string): Promise<boolean> => {
+  return false;
+};

+ 47 - 0
packages/pluginkit/src/server/utils/v4/package-json.ts

@@ -0,0 +1,47 @@
+import path from 'path';
+
+import { GrowiPluginType } from '../../../consts/types';
+import type { GrowiPluginValidationData } from '../../../model/growi-plugin-validation-data';
+import { GrowiPluginValidationError } from '../../../model/growi-plugin-validation-error';
+
+
+export const importPackageJson = async(projectDirRoot: string): Promise<any> => {
+  const packageJsonUrl = path.resolve(projectDirRoot, 'package.json');
+  return import(packageJsonUrl);
+};
+
+export const validatePackageJson = async(projectDirRoot: string, expectedPluginType?: GrowiPluginType): Promise<GrowiPluginValidationData> => {
+  const pkg = await importPackageJson(projectDirRoot);
+
+  const data: GrowiPluginValidationData = { projectDirRoot };
+
+  const { growiPlugin } = pkg;
+
+  if (growiPlugin == null) {
+    throw new GrowiPluginValidationError("The package.json does not have 'growiPlugin' directive.", data);
+  }
+
+  // schema version checking
+  const schemaVersion = Number(growiPlugin.schemaVersion);
+  data.schemaVersion = schemaVersion;
+  if (Number.isNaN(schemaVersion) || schemaVersion < 4) {
+    throw new GrowiPluginValidationError("The growiPlugin directive must have a valid 'schemaVersion' directive.", data);
+  }
+
+  const types: GrowiPluginType[] = growiPlugin.types;
+  data.actualPluginTypes = types;
+  if (types == null) {
+    throw new GrowiPluginValidationError("The growiPlugin directive does not have 'types' directive.", data);
+  }
+
+  // type checking
+  if (expectedPluginType != null) {
+    data.expectedPluginType = expectedPluginType;
+
+    if (!types.includes(expectedPluginType)) {
+      throw new GrowiPluginValidationError("The growiPlugin directive does not have 'types' directive.", data);
+    }
+  }
+
+  return data;
+};

+ 125 - 0
packages/pluginkit/src/server/utils/v4/template.ts

@@ -0,0 +1,125 @@
+import fs from 'fs';
+import path from 'path';
+import { promisify } from 'util';
+
+import { GrowiPluginType } from '../../../consts/types';
+import type { GrowiPluginValidationData, GrowiTemplatePluginValidationData } from '../../../model/growi-plugin-validation-data';
+import { GrowiPluginValidationError } from '../../../model/growi-plugin-validation-error';
+
+import { importPackageJson, validatePackageJson } from './package-json';
+
+
+const statAsync = promisify(fs.stat);
+
+
+/**
+ * An utility for template plugin which wrap 'validatePackageJson' of './package-json.ts' module
+ * @param projectDirRoot
+ */
+export const validateTemplatePluginPackageJson = async(projectDirRoot: string): Promise<GrowiTemplatePluginValidationData> => {
+  const data = await validatePackageJson(projectDirRoot, GrowiPluginType.TEMPLATE);
+
+  const pkg = await importPackageJson(projectDirRoot);
+
+  // check supporting locales
+  const supportingLocales: string[] | undefined = pkg.supportingLocales;
+  if (supportingLocales == null || supportingLocales.length === 0) {
+    throw new GrowiPluginValidationError<GrowiPluginValidationData & { supportingLocales?: string[] }>(
+      "Template plugin must have 'supportingLocales' and that must have one or more locales",
+      {
+        ...data,
+        supportingLocales,
+      },
+    );
+  }
+
+  return {
+    ...data,
+    supportingLocales,
+  };
+};
+
+export type TemplateStatus = {
+  id: string,
+  locale: string,
+  isValid: boolean,
+  invalidReason?: string,
+}
+
+type TemplateDirStatus = {
+  isTemplateExists: boolean,
+  isMetaDataFileExists: boolean,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  meta?: any,
+}
+
+async function getStats(tplDir: string): Promise<TemplateDirStatus> {
+  const markdownPath = path.resolve(tplDir, 'template.md');
+  const statForMarkdown = await statAsync(markdownPath);
+  const isTemplateExists = statForMarkdown.isFile();
+
+  const metaDataPath = path.resolve(tplDir, 'meta.json');
+  const statForMetaDataFile = await statAsync(metaDataPath);
+  const isMetaDataFileExists = statForMetaDataFile.isFile();
+
+  const result: TemplateDirStatus = {
+    isTemplateExists,
+    isMetaDataFileExists,
+  };
+
+  if (isMetaDataFileExists) {
+    result.meta = await import(metaDataPath);
+  }
+
+  return result;
+}
+
+export const scanTemplateStatus = async(projectDirRoot: string, templateId: string, data: GrowiTemplatePluginValidationData): Promise<TemplateStatus[]> => {
+  const status: TemplateStatus[] = [];
+
+  const tplRootDirPath = path.resolve(projectDirRoot, 'dist', templateId);
+
+  for await (const locale of data.supportingLocales) {
+    const tplDir = path.resolve(tplRootDirPath, locale);
+
+    try {
+      const {
+        isTemplateExists, isMetaDataFileExists, meta,
+      } = await getStats(tplDir);
+
+      if (!isTemplateExists) throw new Error("'template.md does not exist.");
+      if (!isMetaDataFileExists) throw new Error("'meta.md does not exist.");
+      if (meta?.title == null) throw new Error("'meta.md does not contain the title.");
+
+      status.push({ id: templateId, locale, isValid: true });
+    }
+    catch (err) {
+      status.push({
+        id: templateId,
+        locale,
+        isValid: false,
+        invalidReason: err.message,
+      });
+    }
+  }
+
+  // eslint-disable-next-line no-console
+  console.debug({ status });
+
+  return status;
+};
+
+export const scanAllTemplateStatus = async(projectDirRoot: string): Promise<TemplateStatus[]> => {
+  const data = await validateTemplatePluginPackageJson(projectDirRoot);
+
+  const status: TemplateStatus[] = [];
+
+  const distDirPath = path.resolve(projectDirRoot, 'dist');
+  const distDirFiles = fs.readdirSync(distDirPath);
+
+  for await (const templateId of distDirFiles) {
+    status.push(...await scanTemplateStatus(projectDirRoot, templateId, data));
+  }
+
+  return status;
+};

+ 14 - 0
packages/pluginkit/tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json.schemastore.org/tsconfig",
+  "extends": "../../tsconfig.base.json",
+  "compilerOptions": {
+    "module": "CommonJS",
+    "types": [
+      "node",
+      "vitest/globals"
+    ]
+  },
+  "include": [
+    "src"
+  ]
+}

+ 29 - 0
packages/pluginkit/vite.config.ts

@@ -0,0 +1,29 @@
+import path from 'path';
+
+import glob from 'glob';
+import { defineConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [
+    dts(),
+  ],
+  build: {
+    outDir: 'dist',
+    sourcemap: true,
+    lib: {
+      entry: glob.sync(path.resolve(__dirname, 'src/**/*.ts')),
+      name: 'pluginkit-libs',
+      formats: ['es', 'cjs'],
+    },
+    rollupOptions: {
+      output: {
+        preserveModules: true,
+        preserveModulesRoot: 'src',
+      },
+      external: [
+      ],
+    },
+  },
+});

+ 13 - 0
packages/pluginkit/vitest.config.ts

@@ -0,0 +1,13 @@
+import tsconfigPaths from 'vite-tsconfig-paths';
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  plugins: [
+    tsconfigPaths(),
+  ],
+  test: {
+    environment: 'node',
+    clearMocks: true,
+    globals: true,
+  },
+});