Browse Source

Merge pull request #6456 from weseek/imprv/refactor-lsx-plugin

imprv: refactor lsx plugin
Yuki Takei 3 years ago
parent
commit
25916af819
46 changed files with 142 additions and 475 deletions
  1. 1 0
      package.json
  2. 0 56
      packages/app/bin/generate-plugin-definitions-source.ts
  3. 2 1
      packages/app/jest.config.js
  4. 6 5
      packages/app/src/server/crowi/index.js
  5. 0 40
      packages/app/src/server/plugins/plugin-utils-v2.js
  6. 0 38
      packages/app/src/server/plugins/plugin-utils-v4.ts
  7. 1 44
      packages/app/src/server/plugins/plugin-utils.js
  8. 0 72
      packages/app/src/server/plugins/plugin.service.js
  9. 7 5
      packages/app/src/server/routes/ogp.ts
  10. 2 1
      packages/app/src/server/service/s2s-messaging/nchan.ts
  11. 2 2
      packages/app/src/services/renderer/renderer.tsx
  12. 5 2
      packages/app/src/utils/download.ts
  13. 1 0
      packages/app/tsconfig.build.client.json
  14. 1 0
      packages/app/tsconfig.json
  15. 2 3
      packages/core/src/index.ts
  16. 4 0
      packages/core/src/plugin/interfaces/option-parser.ts
  17. 0 11
      packages/core/src/plugin/interfaces/plugin-definition-v4.ts
  18. 8 4
      packages/core/src/plugin/model/tag-context.ts
  19. 1 3
      packages/core/src/plugin/util/args-parser.js
  20. 0 88
      packages/core/src/plugin/util/custom-tag-utils.js
  21. 5 0
      packages/core/src/plugin/util/custom-tag-utils.ts
  22. 4 10
      packages/core/src/plugin/util/option-parser.ts
  23. 2 2
      packages/core/src/test/plugin/service/tag-cache-manager.test.js
  24. 1 1
      packages/core/src/test/plugin/util/args-parser.test.js
  25. 50 48
      packages/core/src/test/plugin/util/custom-tag-utils.test.js
  26. 1 1
      packages/core/src/test/plugin/util/option-parser.test.js
  27. 1 1
      packages/core/src/test/service/localstorage-manager.test.js
  28. 5 0
      packages/plugin-lsx/package.json
  29. 0 2
      packages/plugin-lsx/src/client-entry.js
  30. 0 0
      packages/plugin-lsx/src/components/Lsx.module.scss
  31. 8 9
      packages/plugin-lsx/src/components/Lsx.tsx
  32. 0 0
      packages/plugin-lsx/src/components/LsxPageList/LsxListView.jsx
  33. 0 0
      packages/plugin-lsx/src/components/LsxPageList/LsxPage.jsx
  34. 0 0
      packages/plugin-lsx/src/components/LsxPageList/PagePathWrapper.jsx
  35. 0 0
      packages/plugin-lsx/src/components/PageNode.js
  36. 1 0
      packages/plugin-lsx/src/components/index.ts
  37. 3 3
      packages/plugin-lsx/src/components/lsx-context.ts
  38. 0 0
      packages/plugin-lsx/src/components/tag-cache-manager.ts
  39. 0 11
      packages/plugin-lsx/src/index.js
  40. 6 0
      packages/plugin-lsx/src/index.ts
  41. 0 4
      packages/plugin-lsx/src/server-entry.js
  42. 4 5
      packages/plugin-lsx/src/server/routes/lsx.js
  43. 1 0
      packages/plugin-lsx/src/services/renderer/index.ts
  44. 1 3
      packages/plugin-lsx/src/services/renderer/lsx.ts
  45. 1 0
      packages/plugin-lsx/tsconfig.base.json
  46. 5 0
      yarn.lock

+ 1 - 0
package.json

@@ -53,6 +53,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@testing-library/cypress": "^8.0.2",
     "@testing-library/cypress": "^8.0.2",
+    "@types/css-modules": "^1.0.2",
     "@types/jest": "^26.0.22",
     "@types/jest": "^26.0.22",
     "@types/node": "^17.0.43",
     "@types/node": "^17.0.43",
     "@types/rewire": "^2.5.28",
     "@types/rewire": "^2.5.28",

+ 0 - 56
packages/app/bin/generate-plugin-definitions-source.ts

@@ -1,56 +0,0 @@
-/**
- * the tool for genetion of plugin definitions source code
- *
- * @author Yuki Takei <yuki@weseek.co.jp>
- */
-import fs from 'graceful-fs';
-import normalize from 'normalize-path';
-import swig from 'swig-templates';
-
-import { PluginDefinitionV4 } from '@growi/core';
-
-import PluginUtils from '../src/server/plugins/plugin-utils';
-import loggerFactory from '../src/utils/logger';
-import { resolveFromRoot } from '../src/utils/project-dir-utils';
-
-const logger = loggerFactory('growi:bin:generate-plugin-definitions-source');
-
-
-const pluginUtils = new PluginUtils();
-
-const TEMPLATE = resolveFromRoot('bin/templates/plugin-definitions.js.swig');
-const OUT = resolveFromRoot('tmp/plugins/plugin-definitions.js');
-
-// list plugin names
-const pluginNames: string[] = pluginUtils.listPluginNames();
-logger.info('Detected plugins: ', pluginNames);
-
-async function main(): Promise<void> {
-
-  // get definitions
-  const definitions: PluginDefinitionV4[] = [];
-  for (const pluginName of pluginNames) {
-    // eslint-disable-next-line no-await-in-loop
-    const definition = await pluginUtils.generatePluginDefinition(pluginName, true);
-    if (definition != null) {
-      definitions.push(definition);
-    }
-  }
-
-  definitions.map((definition) => {
-    // convert backslash to slash
-    definition.entries = definition.entries.map((entryPath) => {
-      return normalize(entryPath);
-    });
-    return definition;
-  });
-
-  const compiledTemplate = swig.compileFile(TEMPLATE);
-  const code = compiledTemplate({ definitions });
-
-  // write
-  fs.writeFileSync(OUT, code);
-
-}
-
-main();

+ 2 - 1
packages/app/jest.config.js

@@ -5,7 +5,8 @@
 const MODULE_NAME_MAPPING = {
 const MODULE_NAME_MAPPING = {
   '^\\^/(.+)$': '<rootDir>/$1',
   '^\\^/(.+)$': '<rootDir>/$1',
   '^~/(.+)$': '<rootDir>/src/$1',
   '^~/(.+)$': '<rootDir>/src/$1',
-  '^@growi/(.+)$': '<rootDir>/../$1/src',
+  '^@growi/([^/]+)$': '<rootDir>/../$1/src',
+  '^@growi/([^/]+)/(.+)$': '<rootDir>/../$1/src/$2',
 };
 };
 
 
 module.exports = {
 module.exports = {

+ 6 - 5
packages/app/src/server/crowi/index.js

@@ -3,6 +3,7 @@ import http from 'http';
 import path from 'path';
 import path from 'path';
 
 
 import { createTerminus } from '@godaddy/terminus';
 import { createTerminus } from '@godaddy/terminus';
+import lsxRoutes from '@growi/plugin-lsx/server/routes';
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
 import next from 'next';
 import next from 'next';
 
 
@@ -33,7 +34,6 @@ import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '../util/m
 const logger = loggerFactory('growi:crowi');
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const httpErrorHandler = require('../middlewares/http-error-handler');
 const models = require('../models');
 const models = require('../models');
-const PluginService = require('../plugins/plugin.service');
 
 
 const sep = path.sep;
 const sep = path.sep;
 
 
@@ -434,10 +434,6 @@ Crowi.prototype.start = async function() {
 
 
   const { express, configManager } = this;
   const { express, configManager } = this;
 
 
-  // setup plugins
-  this.pluginService = new PluginService(this, express);
-  await this.pluginService.autoDetectAndLoadPlugins();
-
   const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
   const app = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
 
 
   const httpServer = http.createServer(app);
   const httpServer = http.createServer(app);
@@ -465,6 +461,7 @@ Crowi.prototype.start = async function() {
   }
   }
 
 
   // setup Express Routes
   // setup Express Routes
+  this.setupRoutesForPlugins();
   this.setupRoutesAtLast();
   this.setupRoutesAtLast();
 
 
   // setup Global Error Handlers
   // setup Global Error Handlers
@@ -515,6 +512,10 @@ Crowi.prototype.setupTerminus = function(server) {
   });
   });
 };
 };
 
 
+Crowi.prototype.setupRoutesForPlugins = function() {
+  lsxRoutes(this, this.express);
+};
+
 /**
 /**
  * setup Express Routes
  * setup Express Routes
  * !! this must be at last because it includes '/*' route !!
  * !! this must be at last because it includes '/*' route !!

+ 0 - 40
packages/app/src/server/plugins/plugin-utils-v2.js

@@ -1,40 +0,0 @@
-const path = require('path');
-
-class PluginUtilsV2 {
-
-  /**
-   * return a definition objects that has following structure:
-   *
-   * {
-   *   name: 'crowi-plugin-X',
-   *   meta: require('crowi-plugin-X'),
-   *   entries: [
-   *     'crowi-plugin-X/lib/client-entry'
-   *   ]
-   * }
-   *
-   *
-   * @param {string} pluginName
-   * @return
-   * @memberOf PluginService
-   */
-  generatePluginDefinition(name, isForClient = false) {
-    const meta = require(name);
-    let entries = (isForClient) ? meta.clientEntries : meta.serverEntries;
-
-    entries = entries.map((entryPath) => {
-      const moduleRoot = path.resolve(require.resolve(`${name}/package.json`), '..');
-      const entryRelativePath = path.relative(moduleRoot, entryPath);
-      return path.join(name, entryRelativePath);
-    });
-
-    return {
-      name,
-      meta,
-      entries,
-    };
-  }
-
-}
-
-module.exports = PluginUtilsV2;

+ 0 - 38
packages/app/src/server/plugins/plugin-utils-v4.ts

@@ -1,38 +0,0 @@
-import path from 'path';
-
-import { PluginMetaV4, PluginDefinitionV4 } from '@growi/core';
-
-export class PluginUtilsV4 {
-
-  /**
-   * return a definition objects that has following structure:
-   *
-   * {
-   *   name: 'crowi-plugin-X',
-   *   meta: require('crowi-plugin-X'),
-   *   entries: [
-   *     'crowi-plugin-X/lib/client-entry'
-   *   ]
-   * }
-   *
-   *
-   * @param {string} pluginName
-   * @return
-   * @memberOf PluginService
-   */
-  async generatePluginDefinition(name: string, isForClient = false): Promise<PluginDefinitionV4> {
-    const meta: PluginMetaV4 = await import(name);
-    let entries = (isForClient) ? meta.clientEntries : meta.serverEntries;
-
-    entries = entries.map((entryPath) => {
-      return path.join(name, entryPath);
-    });
-
-    return {
-      name,
-      meta,
-      entries,
-    };
-  }
-
-}

+ 1 - 44
packages/app/src/server/plugins/plugin-utils.js

@@ -1,57 +1,14 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 
-import { PluginUtilsV4 } from './plugin-utils-v4';
+// import { PluginUtilsV4 } from './plugin-utils-v4';
 
 
 const fs = require('graceful-fs');
 const fs = require('graceful-fs');
 
 
 const logger = loggerFactory('growi:plugins:plugin-utils');
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
 
-const pluginUtilsV4 = new PluginUtilsV4();
-
 class PluginUtils {
 class PluginUtils {
 
 
-  /**
-   * return a definition objects that has following structure:
-   *
-   * {
-   *   name: 'growi-plugin-X',
-   *   meta: require('growi-plugin-X'),
-   *   entries: [
-   *     'growi-plugin-X/lib/client-entry'
-   *   ]
-   * }
-   *
-   * @param {string} pluginName
-   * @return
-   * @memberOf PluginService
-   */
-  async generatePluginDefinition(name, isForClient = false) {
-    const meta = require(name);
-    let definition;
-
-    switch (meta.pluginSchemaVersion) {
-      // v1, v2 and v3 is deprecated
-      case 1:
-        logger.debug('pluginSchemaVersion 1 is deprecated');
-        break;
-      case 2:
-        logger.debug('pluginSchemaVersion 2 is deprecated');
-        break;
-      case 3:
-        logger.debug('pluginSchemaVersion 3 is deprecated');
-        break;
-      // v4 or above
-      case 4:
-        definition = await pluginUtilsV4.generatePluginDefinition(name, isForClient);
-        break;
-      default:
-        logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
-    }
-
-    return definition;
-  }
-
   /**
   /**
    * list plugin module objects
    * list plugin module objects
    *  that starts with 'growi-plugin-' or 'crowi-plugin-'
    *  that starts with 'growi-plugin-' or 'crowi-plugin-'

+ 0 - 72
packages/app/src/server/plugins/plugin.service.js

@@ -1,72 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const PluginUtils = require('./plugin-utils');
-
-const logger = loggerFactory('growi:plugins:PluginService');
-
-class PluginService {
-
-  constructor(crowi, app) {
-    this.crowi = crowi;
-    this.app = app;
-    this.pluginUtils = new PluginUtils();
-  }
-
-  async autoDetectAndLoadPlugins() {
-    const isEnabledPlugins = this.crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
-
-    // import plugins
-    if (isEnabledPlugins) {
-      logger.debug('Plugins are enabled');
-      return this.loadPlugins(this.pluginUtils.listPluginNames(this.crowi.rootDir));
-    }
-
-  }
-
-  /**
-   * load plugins
-   *
-   * @memberOf PluginService
-   */
-  async loadPlugins(pluginNames) {
-    // get definitions
-    const definitions = [];
-    for (const pluginName of pluginNames) {
-      // eslint-disable-next-line no-await-in-loop
-      const definition = await this.pluginUtils.generatePluginDefinition(pluginName);
-      if (definition != null) {
-        this.loadPlugin(definition);
-      }
-    }
-  }
-
-  loadPlugin(definition) {
-    const meta = definition.meta;
-
-    switch (meta.pluginSchemaVersion) {
-      // v1, v2 and v3 is deprecated
-      case 1:
-        logger.warn('pluginSchemaVersion 1 is deprecated', definition);
-        break;
-      case 2:
-        logger.warn('pluginSchemaVersion 2 is deprecated', definition);
-        break;
-      case 3:
-        logger.warn('pluginSchemaVersion 3 is deprecated', definition);
-        break;
-      // v4 or above
-      case 4:
-        logger.info(`load plugin '${definition.name}'`);
-        definition.entries.forEach((entryPath) => {
-          const entry = require(entryPath);
-          entry(this.crowi, this.app);
-        });
-        break;
-      default:
-        logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
-    }
-  }
-
-}
-
-module.exports = PluginService;

+ 7 - 5
packages/app/src/server/routes/ogp.ts

@@ -1,15 +1,17 @@
+import * as fs from 'fs';
+import path from 'path';
+
+import { DevidedPagePath } from '@growi/core';
+// eslint-disable-next-line no-restricted-imports
+import axios from 'axios';
 import {
 import {
   Request, Response, NextFunction,
   Request, Response, NextFunction,
 } from 'express';
 } from 'express';
 import { param, validationResult, ValidationError } from 'express-validator';
 import { param, validationResult, ValidationError } from 'express-validator';
 
 
-import path from 'path';
-import * as fs from 'fs';
-
-import { DevidedPagePath } from '@growi/core';
-import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 import { projectRoot } from '~/utils/project-dir-utils';
+
 import { convertStreamToBuffer } from '../util/stream';
 import { convertStreamToBuffer } from '../util/stream';
 
 
 const logger = loggerFactory('growi:routes:ogp');
 const logger = loggerFactory('growi:routes:ogp');

+ 2 - 1
packages/app/src/server/service/s2s-messaging/nchan.ts

@@ -1,9 +1,10 @@
 import path from 'path';
 import path from 'path';
 
 
+// eslint-disable-next-line no-restricted-imports
+import axios from 'axios';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import ReconnectingWebSocket from 'reconnecting-websocket';
 import WebSocket from 'ws';
 import WebSocket from 'ws';
 
 
-import axios from '~/utils/axios';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import S2sMessage from '../../models/vo/s2s-message';
 import S2sMessage from '../../models/vo/s2s-message';

+ 2 - 2
packages/app/src/services/renderer/renderer.tsx

@@ -1,6 +1,8 @@
 // allow only types to import from react
 // allow only types to import from react
 import { ComponentType } from 'react';
 import { ComponentType } from 'react';
 
 
+import { Lsx } from '@growi/plugin-lsx/components';
+import * as lsxGrowiPlugin from '@growi/plugin-lsx/services/renderer';
 import growiPlugin from '@growi/remark-growi-plugin';
 import growiPlugin from '@growi/remark-growi-plugin';
 import { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { Schema as SanitizeOption } from 'hast-util-sanitize';
 import { SpecialComponents } from 'react-markdown/lib/ast-to-react';
 import { SpecialComponents } from 'react-markdown/lib/ast-to-react';
@@ -21,12 +23,10 @@ import { PluggableList, Pluggable, PluginTuple } from 'unified';
 
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
 import { Header } from '~/components/ReactMarkdownComponents/Header';
-import { Lsx } from '~/components/ReactMarkdownComponents/Lsx/Lsx';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import * as lsxGrowiPlugin from './growi-plugins/lsx';
 import { addClass } from './rehype-plugins/add-class';
 import { addClass } from './rehype-plugins/add-class';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinks } from './rehype-plugins/relative-links';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';
 import { relativeLinksByPukiwikiLikeLinker } from './rehype-plugins/relative-links-by-pukiwiki-like-linker';

+ 5 - 2
packages/app/src/utils/download.ts

@@ -1,9 +1,12 @@
 import path from 'path';
 import path from 'path';
+import { Readable, Writable, Transform } from 'stream';
+
+// eslint-disable-next-line no-restricted-imports
+import axios from 'axios';
 import fs from 'graceful-fs';
 import fs from 'graceful-fs';
 import mkdirp from 'mkdirp';
 import mkdirp from 'mkdirp';
 import streamToPromise from 'stream-to-promise';
 import streamToPromise from 'stream-to-promise';
-import { Readable, Writable, Transform } from 'stream';
-import axios from '~/utils/axios';
+
 
 
 export async function downloadTo(url: string, outDir: string, fileName: string, transform: Transform|null = null): Promise<void> {
 export async function downloadTo(url: string, outDir: string, fileName: string, transform: Transform|null = null): Promise<void> {
   // get
   // get

+ 1 - 0
packages/app/tsconfig.build.client.json

@@ -8,6 +8,7 @@
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
       "^/*": ["./*"],
       "^/*": ["./*"],
+      "@growi/plugin-lsx/*": ["../plugin-lsx/src/*"],
       "@growi/*": ["../*/src"],
       "@growi/*": ["../*/src"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
       "debug": ["./src/server/utils/logger/alias-for-debug"]
     }
     }

+ 1 - 0
packages/app/tsconfig.json

@@ -5,6 +5,7 @@
     "paths": {
     "paths": {
       "~/*": ["./src/*"],
       "~/*": ["./src/*"],
       "^/*": ["./*"],
       "^/*": ["./*"],
+      "@growi/plugin-lsx/*": ["../plugin-lsx/src/*"],
       "@growi/*": ["../*/src"],
       "@growi/*": ["../*/src"],
       "debug": ["./src/server/utils/logger/alias-for-debug"]
       "debug": ["./src/server/utils/logger/alias-for-debug"]
     }
     }

+ 2 - 3
packages/core/src/index.ts

@@ -1,11 +1,10 @@
-import * as _customTagUtils from './plugin/util/custom-tag-utils';
 import * as _envUtils from './utils/env-utils';
 import * as _envUtils from './utils/env-utils';
 
 
 // export utils by *.js
 // export utils by *.js
 export const envUtils = _envUtils;
 export const envUtils = _envUtils;
-export const customTagUtils = _customTagUtils;
 
 
 // export utils with namespace
 // export utils with namespace
+export * as customTagUtils from './plugin/util/custom-tag-utils';
 export * as templateChecker from './utils/template-checker';
 export * as templateChecker from './utils/template-checker';
 export * as objectIdUtils from './utils/objectid-utils';
 export * as objectIdUtils from './utils/objectid-utils';
 export * as pagePathUtils from './utils/page-path-utils';
 export * as pagePathUtils from './utils/page-path-utils';
@@ -13,6 +12,7 @@ export * as pathUtils from './utils/path-utils';
 export * as pageUtils from './utils/page-utils';
 export * as pageUtils from './utils/page-utils';
 
 
 // export all
 // export all
+export * from './plugin/interfaces/option-parser';
 export * from './interfaces/attachment';
 export * from './interfaces/attachment';
 export * from './interfaces/common';
 export * from './interfaces/common';
 export * from './interfaces/has-object-id';
 export * from './interfaces/has-object-id';
@@ -22,7 +22,6 @@ export * from './interfaces/revision';
 export * from './interfaces/subscription';
 export * from './interfaces/subscription';
 export * from './interfaces/tag';
 export * from './interfaces/tag';
 export * from './interfaces/user';
 export * from './interfaces/user';
-export * from './plugin/interfaces/plugin-definition-v4';
 export * from './plugin/service/tag-cache-manager';
 export * from './plugin/service/tag-cache-manager';
 export * from './models/devided-page-path';
 export * from './models/devided-page-path';
 export * from './service/localstorage-manager';
 export * from './service/localstorage-manager';

+ 4 - 0
packages/core/src/plugin/interfaces/option-parser.ts

@@ -0,0 +1,4 @@
+export type ParseRangeResult = {
+  start: number,
+  end: number,
+}

+ 0 - 11
packages/core/src/plugin/interfaces/plugin-definition-v4.ts

@@ -1,11 +0,0 @@
-export type PluginMetaV4 = {
-  pluginSchemaVersion: number,
-  serverEntries: string[],
-  clientEntries: string[],
-};
-
-export type PluginDefinitionV4 = {
-  name: string,
-  meta: PluginMetaV4,
-  entries: string[],
-};

+ 8 - 4
packages/core/src/plugin/model/tag-context.js → packages/core/src/plugin/model/tag-context.ts

@@ -1,14 +1,18 @@
 /**
 /**
  * Context class for custom-tag-utils#findTagAndReplace
  * Context class for custom-tag-utils#findTagAndReplace
  */
  */
-class TagContext {
+export class TagContext {
 
 
-  constructor(initArgs = {}) {
+  tagExpression: string | null;
+
+  method: string | null;
+
+  args: any;
+
+  constructor(initArgs: any = {}) {
     this.tagExpression = initArgs.tagExpression || null;
     this.tagExpression = initArgs.tagExpression || null;
     this.method = initArgs.method || null;
     this.method = initArgs.method || null;
     this.args = initArgs.args || null;
     this.args = initArgs.args || null;
   }
   }
 
 
 }
 }
-
-module.exports = TagContext;

+ 1 - 3
packages/core/src/plugin/util/args-parser.js

@@ -1,7 +1,7 @@
 /**
 /**
  * Arguments parser for custom tag
  * Arguments parser for custom tag
  */
  */
-class ArgsParser {
+export class ArgsParser {
 
 
   /**
   /**
    * @typedef ParseArgsResult
    * @typedef ParseArgsResult
@@ -55,5 +55,3 @@ class ArgsParser {
   }
   }
 
 
 }
 }
-
-module.exports = ArgsParser;

+ 0 - 88
packages/core/src/plugin/util/custom-tag-utils.js

@@ -1,88 +0,0 @@
-const TagContext = require('../model/tag-context');
-
-/**
- * @private
- *
- * create random strings
- * @see http://qiita.com/ryounagaoka/items/4736c225bdd86a74d59c
- *
- * @param {number} length
- * @return {string} random strings
- */
-function createRandomStr(length) {
-  const bag = 'abcdefghijklmnopqrstuvwxyz0123456789';
-  let generated = '';
-  for (let i = 0; i < length; i++) {
-    generated += bag[Math.floor(Math.random() * bag.length)];
-  }
-  return generated;
-}
-
-/**
- * @typedef FindTagAndReplaceResult
- * @property {string} html - HTML string
- * @property {Object} tagContextMap - Object.<string, [TagContext]{@link ../model/tag-context.html#TagContext}>
- *
- * @memberof customTagUtils
- */
-/**
- * @param {RegExp} tagPattern
- * @param {string} html
- * @param {function} replace replace function
- * @return {FindTagAndReplaceResult}
- *
- * @memberof customTagUtils
- */
-function findTagAndReplace(tagPattern, html, replace) {
-  let replacedHtml = html;
-  const tagContextMap = {};
-
-  if (tagPattern == null || html == null) {
-    return { html: replacedHtml, tagContextMap };
-  }
-
-  // see: https://regex101.com/r/NQq3s9/9
-  const pattern = new RegExp(`\\$(${tagPattern.source})\\((.*?)\\)(?=[<\\[\\s\\$])|\\$(${tagPattern.source})\\((.*)\\)(?![<\\[\\s\\$])`, 'g');
-
-  replacedHtml = html.replace(pattern, (all, group1, group2, group3, group4) => {
-    const tagExpression = all;
-    const method = (group1 || group3).trim();
-    const args = (group2 || group4 || '').trim();
-
-    // create contexts
-    const tagContext = new TagContext({ tagExpression, method, args });
-
-    if (replace != null) {
-      return replace(tagContext);
-    }
-
-    // replace with empty dom
-    const domId = `${method}-${createRandomStr(8)}`;
-    tagContextMap[domId] = tagContext;
-    return `<div id="${domId}"></div>`;
-  });
-
-  return { html: replacedHtml, tagContextMap };
-}
-
-/**
- * @namespace customTagUtils
- */
-module.exports = {
-  findTagAndReplace,
-  /**
-   * Context class used by findTagAndReplace
-   * @memberof customTagUtils
-   */
-  TagContext,
-  /**
-   * [ArgsParser]{@link ./args-parser#ArgsParser}
-   * @memberof customTagUtils
-   */
-  ArgsParser: require('./args-parser'),
-  /**
-   * [OptionParser]{@link ./option-parser#OptionParser}
-   * @memberof customTagUtils
-   */
-  OptionParser: require('./option-parser'),
-};

+ 5 - 0
packages/core/src/plugin/util/custom-tag-utils.ts

@@ -0,0 +1,5 @@
+export * from '../model/tag-context';
+
+export * from './args-parser';
+
+export * from './option-parser';

+ 4 - 10
packages/core/src/plugin/util/option-parser.js → packages/core/src/plugin/util/option-parser.ts

@@ -1,13 +1,9 @@
+import { ParseRangeResult } from '../interfaces/option-parser';
+
 /**
 /**
  * Options parser for custom tag
  * Options parser for custom tag
  */
  */
-class OptionParser {
-
-  /**
-   * @typedef ParseRangeResult
-   * @property {number} start - start index
-   * @property {number} end - end index
-   */
+export class OptionParser {
 
 
   /**
   /**
    * Parse range expression
    * Parse range expression
@@ -27,7 +23,7 @@ class OptionParser {
    * @param {string} str
    * @param {string} str
    * @returns {ParseRangeResult}
    * @returns {ParseRangeResult}
    */
    */
-  static parseRange(str) {
+  static parseRange(str: string): ParseRangeResult | null {
     if (str == null) {
     if (str == null) {
       return null;
       return null;
     }
     }
@@ -66,5 +62,3 @@ class OptionParser {
   }
   }
 
 
 }
 }
-
-module.exports = OptionParser;

+ 2 - 2
packages/core/src/test/plugin/service/tag-cache-manager.test.js

@@ -3,8 +3,8 @@
 // import each from 'jest-each';
 // import each from 'jest-each';
 jest.mock('~/service/localstorage-manager');
 jest.mock('~/service/localstorage-manager');
 
 
-import * as TagCacheManager from '~/plugin/service/tag-cache-manager';
-import * as LocalStorageManager from '~/service/localstorage-manager';
+import { TagCacheManager } from '~/plugin/service/tag-cache-manager';
+import { LocalStorageManager } from '~/service/localstorage-manager';
 /* eslint-enable import/first */
 /* eslint-enable import/first */
 
 
 describe('TagCacheManager.constructor', () => {
 describe('TagCacheManager.constructor', () => {

+ 1 - 1
packages/core/src/test/plugin/util/args-parser.test.js

@@ -1,4 +1,4 @@
-import ArgsParser from '~/plugin/util/args-parser';
+import { ArgsParser } from '~/plugin/util/args-parser';
 
 
 describe('args-parser', () => {
 describe('args-parser', () => {
 
 

+ 50 - 48
packages/core/src/test/plugin/util/custom-tag-utils.test.js

@@ -1,8 +1,9 @@
 import rewire from 'rewire';
 import rewire from 'rewire';
 
 
-import customTagUtils from '~/plugin/util/custom-tag-utils';
+import * as customTagUtils from '~/plugin/util/custom-tag-utils';
 
 
-const rewiredCustomTagUtils = rewire('../../../plugin/util/custom-tag-utils');
+// leave it commented out for rewire example -- 2022.08.18 Yuki Takei
+// const rewiredCustomTagUtils = rewire('../../../plugin/util/custom-tag-utils');
 
 
 describe('customTagUtils', () => {
 describe('customTagUtils', () => {
 
 
@@ -21,52 +22,53 @@ describe('customTagUtils', () => {
     expect(typeof customTagUtils.OptionParser).toBe('function');
     expect(typeof customTagUtils.OptionParser).toBe('function');
   });
   });
 
 
-  test('.createRandomStr(10) returns random string', () => {
-    // get private resource
-    const createRandomStr = rewiredCustomTagUtils.__get__('createRandomStr');
-    expect(createRandomStr(10)).toMatch(/^[a-z0-9]{10}$/);
-  });
-
-  test('.findTagAndReplace() returns default object when tagPattern is null', () => {
-    const htmlMock = jest.fn();
-    htmlMock.replace = jest.fn();
-
-    const result = customTagUtils.findTagAndReplace(null, '');
-
-    expect(result).toEqual({ html: '', tagContextMap: {} });
-    expect(htmlMock.replace).not.toHaveBeenCalled();
-  });
-
-  test('.findTagAndReplace() returns default object when html is null', () => {
-    const tagPatternMock = jest.fn();
-    tagPatternMock.source = jest.fn();
-
-    const result = customTagUtils.findTagAndReplace(tagPatternMock, null);
-
-    expect(result).toEqual({ html: null, tagContextMap: {} });
-    expect(tagPatternMock.source).not.toHaveBeenCalled();
-  });
-
-  test('.findTagAndReplace() works correctly', () => {
-    // setup mocks for private function
-    rewiredCustomTagUtils.__set__('createRandomStr', (length) => {
-      return 'dummyDomId';
-    });
-
-    const tagPattern = /ls|lsx/;
-    const html = '<section><h1>header</h1>\n$ls(/)</section>';
-
-    const result = rewiredCustomTagUtils.findTagAndReplace(tagPattern, html);
-
-    expect(result.html).toMatch(/<section><h1>header<\/h1>\n<div id="ls-dummyDomId"><\/div>/);
-    expect(result.tagContextMap).toEqual({
-      'ls-dummyDomId': {
-        tagExpression: '$ls(/)',
-        method: 'ls',
-        args: '/',
-      },
-    });
-  });
+  // leave it commented out for rewire example -- 2022.08.18 Yuki Takei
+  // test('.createRandomStr(10) returns random string', () => {
+  //   // get private resource
+  //   const createRandomStr = rewiredCustomTagUtils.__get__('createRandomStr');
+  //   expect(createRandomStr(10)).toMatch(/^[a-z0-9]{10}$/);
+  // });
+
+  // test('.findTagAndReplace() returns default object when tagPattern is null', () => {
+  //   const htmlMock = jest.fn();
+  //   htmlMock.replace = jest.fn();
+
+  //   const result = customTagUtils.findTagAndReplace(null, '');
+
+  //   expect(result).toEqual({ html: '', tagContextMap: {} });
+  //   expect(htmlMock.replace).not.toHaveBeenCalled();
+  // });
+
+  // test('.findTagAndReplace() returns default object when html is null', () => {
+  //   const tagPatternMock = jest.fn();
+  //   tagPatternMock.source = jest.fn();
+
+  //   const result = customTagUtils.findTagAndReplace(tagPatternMock, null);
+
+  //   expect(result).toEqual({ html: null, tagContextMap: {} });
+  //   expect(tagPatternMock.source).not.toHaveBeenCalled();
+  // });
+
+  // test('.findTagAndReplace() works correctly', () => {
+  //   // setup mocks for private function
+  //   rewiredCustomTagUtils.__set__('createRandomStr', (length) => {
+  //     return 'dummyDomId';
+  //   });
+
+  //   const tagPattern = /ls|lsx/;
+  //   const html = '<section><h1>header</h1>\n$ls(/)</section>';
+
+  //   const result = rewiredCustomTagUtils.findTagAndReplace(tagPattern, html);
+
+  //   expect(result.html).toMatch(/<section><h1>header<\/h1>\n<div id="ls-dummyDomId"><\/div>/);
+  //   expect(result.tagContextMap).toEqual({
+  //     'ls-dummyDomId': {
+  //       tagExpression: '$ls(/)',
+  //       method: 'ls',
+  //       args: '/',
+  //     },
+  //   });
+  // });
 
 
 
 
 });
 });

+ 1 - 1
packages/core/src/test/plugin/util/option-parser.test.js

@@ -1,6 +1,6 @@
 import each from 'jest-each';
 import each from 'jest-each';
 
 
-import OptionParser from '~/plugin/util/option-parser';
+import { OptionParser } from '~/plugin/util/option-parser';
 
 
 describe('option-parser', () => {
 describe('option-parser', () => {
 
 

+ 1 - 1
packages/core/src/test/service/localstorage-manager.test.js

@@ -1,7 +1,7 @@
 // eslint-disable-next-line import/no-unresolved
 // eslint-disable-next-line import/no-unresolved
 import 'jest-localstorage-mock';
 import 'jest-localstorage-mock';
 
 
-import * as LocalStorageManager from '~/service/localstorage-manager';
+import { LocalStorageManager } from '~/service/localstorage-manager';
 
 
 let localStorageManager = null;
 let localStorageManager = null;
 
 

+ 5 - 0
packages/plugin-lsx/package.json

@@ -8,6 +8,11 @@
     "growi-plugin"
     "growi-plugin"
   ],
   ],
   "main": "dist/cjs/index.js",
   "main": "dist/cjs/index.js",
+  "exports": {
+    "./components": "./dist/cjs/components/index.js",
+    "./services/renderer": "./dist/cjs/services/renderer/index.js",
+    "./server/routes": "./dist/cjs/server/routes/index.js"
+  },
   "module": "dist/esm/index.js",
   "module": "dist/esm/index.js",
   "files": [
   "files": [
     "dist"
     "dist"

+ 0 - 2
packages/plugin-lsx/src/client-entry.js

@@ -1,2 +0,0 @@
-export default () => {
-};

+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/Lsx.module.scss → packages/plugin-lsx/src/components/Lsx.module.scss


+ 8 - 9
packages/app/src/components/ReactMarkdownComponents/Lsx/Lsx.tsx → packages/plugin-lsx/src/components/Lsx.tsx

@@ -5,10 +5,7 @@ import React, {
 import * as url from 'url';
 import * as url from 'url';
 
 
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
-
-import { apiGet } from '~/client/util/apiv1-client';
-
-// eslint-disable-next-line no-unused-vars
+import axios from 'axios';
 
 
 import { LsxListView } from './LsxPageList/LsxListView';
 import { LsxListView } from './LsxPageList/LsxListView';
 import { PageNode } from './PageNode';
 import { PageNode } from './PageNode';
@@ -175,14 +172,16 @@ export const Lsx = ({
 
 
     let newNodeTree: PageNode[] = [];
     let newNodeTree: PageNode[] = [];
     try {
     try {
-      const result: any = await apiGet('/plugins/lsx', {
-        pagePath,
-        options: lsxContext.options,
+      const result: any = await axios.get('/_api/plugins/lsx', {
+        params: {
+          pagePath,
+          options: lsxContext.options,
+        },
       });
       });
 
 
-      newNodeTree = generatePageNodeTree(pagePath, result.pages);
+      newNodeTree = generatePageNodeTree(pagePath, result.data.pages);
       setNodeTree(newNodeTree);
       setNodeTree(newNodeTree);
-      setBasisViewersCount(result.toppageViewersCount);
+      setBasisViewersCount(result.data.toppageViewersCount);
       setError(false);
       setError(false);
 
 
       // store to sessionStorage
       // store to sessionStorage

+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/LsxPageList/LsxListView.jsx → packages/plugin-lsx/src/components/LsxPageList/LsxListView.jsx


+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/LsxPageList/LsxPage.jsx → packages/plugin-lsx/src/components/LsxPageList/LsxPage.jsx


+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/LsxPageList/PagePathWrapper.jsx → packages/plugin-lsx/src/components/LsxPageList/PagePathWrapper.jsx


+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/PageNode.js → packages/plugin-lsx/src/components/PageNode.js


+ 1 - 0
packages/plugin-lsx/src/components/index.ts

@@ -0,0 +1 @@
+export { Lsx } from './Lsx';

+ 3 - 3
packages/app/src/components/ReactMarkdownComponents/Lsx/lsx-context.ts → packages/plugin-lsx/src/components/lsx-context.ts

@@ -1,4 +1,4 @@
-import { customTagUtils } from '@growi/core';
+import { customTagUtils, ParseRangeResult } from '@growi/core';
 
 
 const { OptionParser } = customTagUtils;
 const { OptionParser } = customTagUtils;
 
 
@@ -18,9 +18,9 @@ export class LsxContext {
     this.options = options;
     this.options = options;
   }
   }
 
 
-  getOptDepth() {
+  getOptDepth(): ParseRangeResult | null {
     if (this.options?.depth == null) {
     if (this.options?.depth == null) {
-      return undefined;
+      return null;
     }
     }
     return OptionParser.parseRange(this.options.depth);
     return OptionParser.parseRange(this.options.depth);
   }
   }

+ 0 - 0
packages/app/src/components/ReactMarkdownComponents/Lsx/tag-cache-manager.ts → packages/plugin-lsx/src/components/tag-cache-manager.ts


+ 0 - 11
packages/plugin-lsx/src/index.js

@@ -1,11 +0,0 @@
-const isProd = process.env.NODE_ENV === 'production';
-
-module.exports = {
-  pluginSchemaVersion: 4,
-  serverEntries: [
-    isProd ? 'dist/cjs/server-entry.js' : 'src/server-entry.js',
-  ],
-  clientEntries: [
-    'src/client-entry.js',
-  ],
-};

+ 6 - 0
packages/plugin-lsx/src/index.ts

@@ -0,0 +1,6 @@
+import * as _serverRoutes from './server/routes';
+
+export const serverRoutes = _serverRoutes;
+
+export * from './components';
+export * from './services/renderer';

+ 0 - 4
packages/plugin-lsx/src/server-entry.js

@@ -1,4 +0,0 @@
-module.exports = (crowi, app) => {
-  // add routes
-  require('./server/routes')(crowi, app);
-};

+ 4 - 5
packages/plugin-lsx/src/server/routes/lsx.js

@@ -153,7 +153,6 @@ class Lsx {
 
 
 module.exports = (crowi, app) => {
 module.exports = (crowi, app) => {
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
-  const ApiResponse = crowi.require('../util/apiResponse');
   const actions = {};
   const actions = {};
 
 
   /**
   /**
@@ -202,7 +201,7 @@ module.exports = (crowi, app) => {
       options = JSON.parse(req.query.options);
       options = JSON.parse(req.query.options);
     }
     }
     catch (error) {
     catch (error) {
-      return res.json(ApiResponse.error(error));
+      return res.status(400).send(error);
     }
     }
 
 
     const builder = await generateBaseQueryBuilder(pagePath, user);
     const builder = await generateBaseQueryBuilder(pagePath, user);
@@ -220,7 +219,7 @@ module.exports = (crowi, app) => {
         : 1;
         : 1;
     }
     }
     catch (error) {
     catch (error) {
-      return res.json(ApiResponse.error(error));
+      return res.status(500).send(error);
     }
     }
 
 
     let query = builder.query;
     let query = builder.query;
@@ -243,10 +242,10 @@ module.exports = (crowi, app) => {
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);
 
 
       const pages = await query.exec();
       const pages = await query.exec();
-      res.json(ApiResponse.success({ pages, toppageViewersCount }));
+      res.status(200).send({ pages, toppageViewersCount });
     }
     }
     catch (error) {
     catch (error) {
-      return res.json(ApiResponse.error(error));
+      return res.status(500).send(error);
     }
     }
   };
   };
 
 

+ 1 - 0
packages/plugin-lsx/src/services/renderer/index.ts

@@ -0,0 +1 @@
+export * from './lsx';

+ 1 - 3
packages/app/src/services/renderer/growi-plugins/lsx.ts → packages/plugin-lsx/src/services/renderer/lsx.ts

@@ -7,8 +7,6 @@ import { selectAll, HastNode } from 'hast-util-select';
 import { Plugin } from 'unified';
 import { Plugin } from 'unified';
 import { visit } from 'unist-util-visit';
 import { visit } from 'unist-util-visit';
 
 
-import { IHrefResolver } from '../rehype-plugins/relative-links';
-
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const NODE_NAME_PATTERN = new RegExp(/ls|lsx/);
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter'];
 const SUPPORTED_ATTRIBUTES = ['prefix', 'num', 'depth', 'sort', 'reverse', 'filter'];
 
 
@@ -59,7 +57,7 @@ export type LsxRehypePluginParams = {
   pagePath?: string,
   pagePath?: string,
 }
 }
 
 
-const pathResolver: IHrefResolver = (relativeHref, basePath) => {
+const pathResolver = (relativeHref: string, basePath: string): string => {
   // generate relative pathname
   // generate relative pathname
   const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
   const baseUrl = new URL(pathUtils.addTrailingSlash(basePath), 'https://example.com');
   const relativeUrl = new URL(relativeHref, baseUrl);
   const relativeUrl = new URL(relativeHref, baseUrl);

+ 1 - 0
packages/plugin-lsx/tsconfig.base.json

@@ -1,6 +1,7 @@
 {
 {
   "extends": "../../tsconfig.base.json",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
+    "jsx": "preserve",
   },
   },
   "include": [
   "include": [
     "src"
     "src"

+ 5 - 0
yarn.lock

@@ -3957,6 +3957,11 @@
   resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
   resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
   integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
   integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
 
 
+"@types/css-modules@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@types/css-modules/-/css-modules-1.0.2.tgz#8884135f9be3e204b42ef7ad7fce2474e8d74cb6"
+  integrity sha512-tyqlt2GtEBdsxJylh78zSxI/kOJK5Iz8Ta4Fxr8KLTP8mD/IgMa84D8EKPS/AWCp+MDoctgJyikrVWY28GKmcg==
+
 "@types/debug@^0.0.30":
 "@types/debug@^0.0.30":
   version "0.0.30"
   version "0.0.30"
   resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.30.tgz#dc1e40f7af3b9c815013a7860e6252f6352a84df"
   resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.30.tgz#dc1e40f7af3b9c815013a7860e6252f6352a84df"