Преглед изворни кода

Merge branch 'master' of https://github.com/weseek/growi into imprv/gw5983-update-helmet-4.6.0

* 'master' of https://github.com/weseek/growi: (358 commits)
  fix version
  write tar command with blob
  bugfix
  update workflow files
  add packages
  update docs
  fix label and encode spaces in Linker class
  remove getLinkForPreview method
  hide scrollbar of .grw-tag-labels when editing
  update docs
  hide footer on edit
  hide Fab on edit
  update README
  update ci settings
  update .env files
  ignore dist
  update ci.yml to upload artifact
  update jest settings
  fix ci.yml
  update ci settings
  ...

# Conflicts:
#	packages/app/package.json
#	yarn.lock
Luqman Grune пре 4 година
родитељ
комит
6400d7a77e
100 измењених фајлова са 1215 додато и 221 уклоњено
  1. 1 0
      .devcontainer/devcontainer.json
  2. 0 1
      .devcontainer/docker-compose.yml
  3. 4 0
      .eslintrc.js
  4. 2 2
      .github/workflows/ci-slackbot-proxy.yml
  5. 12 9
      .github/workflows/ci.yml
  6. 1 1
      .github/workflows/release-rc.yml
  7. 2 2
      .github/workflows/release.yml
  8. 4 0
      .markdownlint.yml
  9. 20 0
      .vscode/launch.json
  10. 6 0
      CHANGES.md
  11. 21 3
      package.json
  12. 2 2
      packages/app/.env.development
  13. 1 1
      packages/app/.env.production
  14. 1 0
      packages/app/.eslintignore
  15. 1 1
      packages/app/.gitignore
  16. 22 14
      packages/app/bin/generate-plugin-definitions-source.ts
  17. 2 0
      packages/app/config/ci/.env.local.for-ci
  18. 18 13
      packages/app/docker/Dockerfile
  19. 1 0
      packages/app/jest.config.js
  20. 9 14
      packages/app/package.json
  21. 20 1
      packages/app/src/client/models/Linker.js
  22. 4 2
      packages/app/src/client/plugin.js
  23. 1 1
      packages/app/src/client/services/AdminGitHubSecurityContainer.js
  24. 1 1
      packages/app/src/client/services/AdminGoogleSecurityContainer.js
  25. 1 1
      packages/app/src/client/services/AdminOidcSecurityContainer.js
  26. 1 1
      packages/app/src/client/services/AdminSamlSecurityContainer.js
  27. 1 1
      packages/app/src/client/services/AdminTwitterSecurityContainer.js
  28. 3 1
      packages/app/src/client/services/PageContainer.js
  29. 1 1
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  30. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx
  31. 1 1
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx
  32. 1 1
      packages/app/src/components/Admin/Users/UserTable.jsx
  33. 3 1
      packages/app/src/components/ComparePathsTable.jsx
  34. 3 1
      packages/app/src/components/ContentLinkButtons.jsx
  35. 1 1
      packages/app/src/components/CreateTemplateModal.jsx
  36. 3 1
      packages/app/src/components/DuplicatedPathsTable.jsx
  37. 1 1
      packages/app/src/components/Fab.jsx
  38. 4 2
      packages/app/src/components/Navbar/AuthorInfo.jsx
  39. 1 1
      packages/app/src/components/Navbar/GrowiSubNavigation.jsx
  40. 1 1
      packages/app/src/components/Navbar/PersonalDropdown.jsx
  41. 3 1
      packages/app/src/components/Page/CopyDropdown.jsx
  42. 3 1
      packages/app/src/components/Page/PageManagement.jsx
  43. 1 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  44. 1 1
      packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx
  45. 1 1
      packages/app/src/components/PageAttachment/PageAttachmentList.jsx
  46. 1 1
      packages/app/src/components/PageComment/Comment.jsx
  47. 1 1
      packages/app/src/components/PageComment/CommentEditor.jsx
  48. 1 1
      packages/app/src/components/PageComment/DeleteCommentModal.jsx
  49. 6 4
      packages/app/src/components/PageCreateModal.jsx
  50. 6 19
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  51. 1 1
      packages/app/src/components/PageHistory/Revision.jsx
  52. 2 3
      packages/app/src/components/PageList/Page.jsx
  53. 0 24
      packages/app/src/components/PageList/PagePath.jsx
  54. 1 1
      packages/app/src/components/PagePathAutoComplete.jsx
  55. 3 1
      packages/app/src/components/RevisionComparer/RevisionComparer.jsx
  56. 2 3
      packages/app/src/components/SearchTypeahead.jsx
  57. 2 2
      packages/app/src/components/Sidebar/RecentChanges.jsx
  58. 1 1
      packages/app/src/components/User/UserInfo.jsx
  59. 1 1
      packages/app/src/components/User/UserPictureList.jsx
  60. 1 1
      packages/app/src/migrations/20191126173016-adjust-pages-path.js
  61. 3 6
      packages/app/src/models/linked-page-path.js
  62. 1 1
      packages/app/src/server/crowi/index.js
  63. 3 4
      packages/app/src/server/middlewares/admin-required.js
  64. 5 3
      packages/app/src/server/models/page.js
  65. 38 0
      packages/app/src/server/plugins/plugin-utils-v4.ts
  66. 15 7
      packages/app/src/server/plugins/plugin-utils.js
  67. 16 11
      packages/app/src/server/plugins/plugin.service.js
  68. 3 2
      packages/app/src/server/routes/apiv3/page.js
  69. 7 2
      packages/app/src/server/routes/apiv3/pages.js
  70. 2 1
      packages/app/src/server/routes/page.js
  71. 2 2
      packages/app/src/server/service/app.ts
  72. 1 1
      packages/app/src/server/service/customize.ts
  73. 3 1
      packages/app/src/server/service/global-notification/global-notification-slack.js
  74. 3 1
      packages/app/src/server/service/page.js
  75. 2 13
      packages/app/src/server/service/socket-io.js
  76. 3 1
      packages/app/src/server/util/createGrowiPagesFromImports.js
  77. 1 1
      packages/app/src/server/views/layout-growi/base/layout.html
  78. 1 0
      packages/app/src/styles/_on-edit.scss
  79. 2 5
      packages/app/tsconfig.base.json
  80. 0 1
      packages/app/tsconfig.build.client.json
  81. 1 0
      packages/core/.eslintignore
  82. 1 0
      packages/core/.gitignore
  83. 27 0
      packages/core/README.md
  84. 60 0
      packages/core/jest.config.js
  85. 24 0
      packages/core/package.json
  86. 21 0
      packages/core/src/index.js
  87. 2 4
      packages/core/src/models/devided-page-path.js
  88. 11 0
      packages/core/src/plugin/interfaces/plugin-definition-v4.ts
  89. 14 0
      packages/core/src/plugin/model/tag-context.js
  90. 71 0
      packages/core/src/plugin/service/tag-cache-manager.js
  91. 59 0
      packages/core/src/plugin/util/args-parser.js
  92. 88 0
      packages/core/src/plugin/util/custom-tag-utils.js
  93. 70 0
      packages/core/src/plugin/util/option-parser.js
  94. 56 0
      packages/core/src/service/localstorage-manager.js
  95. 125 0
      packages/core/src/test/plugin/service/tag-cache-manager.test.js
  96. 42 0
      packages/core/src/test/plugin/util/args-parser.test.js
  97. 72 0
      packages/core/src/test/plugin/util/custom-tag-utils.test.js
  98. 32 0
      packages/core/src/test/plugin/util/option-parser.test.js
  99. 86 0
      packages/core/src/test/service/localstorage-manager.test.js
  100. 21 0
      packages/core/src/test/util/env-utils.test.js

+ 1 - 0
.devcontainer/devcontainer.json

@@ -16,6 +16,7 @@
 	"extensions": [
 	"extensions": [
 		"dbaeumer.vscode-eslint",
 		"dbaeumer.vscode-eslint",
 		"eamodio.gitlens",
 		"eamodio.gitlens",
+    "firsttris.vscode-jest-runner",
 		"msjsdiag.debugger-for-chrome",
 		"msjsdiag.debugger-for-chrome",
 		"firefox-devtools.vscode-firefox-debug",
 		"firefox-devtools.vscode-firefox-debug",
 		"editorconfig.editorconfig",
 		"editorconfig.editorconfig",

+ 0 - 1
.devcontainer/docker-compose.yml

@@ -25,7 +25,6 @@ services:
       - ..:/workspace/growi:delegated
       - ..:/workspace/growi:delegated
       - node_modules:/workspace/growi/node_modules
       - node_modules:/workspace/growi/node_modules
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
       - ../../growi-docker-compose:/workspace/growi-docker-compose:delegated
-      - ../../node_modules:/workspace/node_modules:delegated
 
 
     tty: true
     tty: true
 
 

+ 4 - 0
.eslintrc.js

@@ -26,5 +26,9 @@ module.exports = {
         FunctionExpression: { body: 1, parameters: 2 },
         FunctionExpression: { body: 1, parameters: 2 },
       },
       },
     ],
     ],
+    'jest/no-standalone-expect': [
+      'error',
+      { additionalTestBlockFunctions: ['each.test'] },
+    ],
   },
   },
 };
 };

+ 2 - 2
.github/workflows/ci-slackbot-proxy.yml

@@ -120,7 +120,7 @@ jobs:
     - name: yarn dev:ci
     - name: yarn dev:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
         yarn dev:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql
@@ -211,7 +211,7 @@ jobs:
     - name: yarn start:prod:ci
     - name: yarn start:prod:ci
       working-directory: ./packages/slackbot-proxy
       working-directory: ./packages/slackbot-proxy
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn start:prod:ci
         yarn start:prod:ci
       env:
       env:
         TYPEORM_CONNECTION: mysql
         TYPEORM_CONNECTION: mysql

+ 12 - 9
.github/workflows/ci.yml

@@ -51,10 +51,10 @@ jobs:
         yarn list --depth=0
         yarn list --depth=0
     - name: lerna run lint for plugins
     - name: lerna run lint for plugins
       run: |
       run: |
-        yarn lerna run lint --scope @growi/plugin-pukiwiki-like-linker
+        yarn lerna run lint --scope @growi/plugin-*
     - name: lerna run lint for app
     - name: lerna run lint for app
       run: |
       run: |
-        yarn lerna run lint --scope @growi/app
+        yarn lerna run lint --scope @growi/app --scope @growi/core --scope @growi/ui
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
@@ -130,6 +130,12 @@ jobs:
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi_test
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi_test
 
 
+    - name: Upload coverage report as artifact
+      uses: actions/upload-artifact@v2
+      with:
+        name: Coverage Report
+        path: packages/app/coverage
+
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
       if: failure()
       if: failure()
@@ -206,7 +212,7 @@ jobs:
     - name: yarn dev:ci
     - name: yarn dev:ci
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
-        cp config/ci/.env.local.for-ci .env.local
+        cp config/ci/.env.local.for-ci .env.development.local
         yarn dev:ci
         yarn dev:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
@@ -281,7 +287,7 @@ jobs:
         yarn list --depth=0
         yarn list --depth=0
     - name: Build
     - name: Build
       run: |
       run: |
-        yarn lerna run build --scope @growi/slack --scope @growi/app
+        yarn lerna run build --scope @growi/core --scope @growi/slack --scope @growi/plugin-* --scope @growi/app
     - name: lerna bootstrap --production
     - name: lerna bootstrap --production
       run: |
       run: |
         npx lerna bootstrap -- --production
         npx lerna bootstrap -- --production
@@ -297,20 +303,17 @@ jobs:
     - name: yarn server:ci
     - name: yarn server:ci
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
         yarn server:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
     - name: yarn server:ci with MongoDB 3.6
     - name: yarn server:ci with MongoDB 3.6
       working-directory: ./packages/app
       working-directory: ./packages/app
       run: |
       run: |
+        cp config/ci/.env.local.for-ci .env.production.local
         yarn server:ci
         yarn server:ci
       env:
       env:
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
         MONGO_URI: mongodb://localhost:${{ job.services.mongodb36.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
-    - name: Upload report as artifact
-      uses: actions/upload-artifact@v2
-      with:
-        name: Report
-        path: report
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master

+ 1 - 1
.github/workflows/release-rc.yml

@@ -55,7 +55,7 @@ jobs:
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2
       with:
       with:
         context: .
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache

+ 2 - 2
.github/workflows/release.yml

@@ -113,7 +113,7 @@ jobs:
       uses: docker/build-push-action@v2
       uses: docker/build-push-action@v2
       with:
       with:
         context: .
         context: .
-        file: ./docker/Dockerfile
+        file: ./packages/app/docker/Dockerfile
         platforms: linux/amd64
         platforms: linux/amd64
         push: true
         push: true
         cache-from: type=local,src=/tmp/.buildx-cache
         cache-from: type=local,src=/tmp/.buildx-cache
@@ -131,7 +131,7 @@ jobs:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         repository: weseek/growi
         repository: weseek/growi
-        readme-filepath: ./docker/README.md
+        readme-filepath: ./packages/app/docker/README.md
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-release-slack-notification@master
       uses: weseek/ghaction-release-slack-notification@master

+ 4 - 0
.markdownlint.yml

@@ -7,3 +7,7 @@ no-multiple-blanks: false
 no-duplicate-heading: false
 no-duplicate-heading: false
 no-inline-html: false
 no-inline-html: false
 no-trailing-punctuation: false
 no-trailing-punctuation: false
+
+MD002: false
+MD012: false
+MD041: false

+ 20 - 0
.vscode/launch.json

@@ -44,6 +44,26 @@
         "url": "http://localhost:3000",
         "url": "http://localhost:3000",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "webRoot": "${workspaceFolder}/packages/app/public",
         "pathMappings": [
         "pathMappings": [
+          {
+            "url": "webpack:///core",
+            "path": "${workspaceFolder}/packages/core"
+          },
+          {
+            "url": "webpack:///plugin-attachment-refs",
+            "path": "${workspaceFolder}/packages/plugin-attachment-refs"
+          },
+          {
+            "url": "webpack:///plugin-pukiwiki-like-linker",
+            "path": "${workspaceFolder}/packages/plugin-pukiwiki-like-linker"
+          },
+          {
+            "url": "webpack:///plugin-lsx",
+            "path": "${workspaceFolder}/packages/plugin-lsx"
+          },
+          {
+            "url": "webpack:///ui",
+            "path": "${workspaceFolder}/packages/ui"
+          },
           {
           {
             "url": "webpack:///src",
             "url": "webpack:///src",
             "path": "${workspaceFolder}/packages/app/src"
             "path": "${workspaceFolder}/packages/app/src"

+ 6 - 0
CHANGES.md

@@ -2,6 +2,12 @@
 
 
 ## v4.3.3-RC
 ## v4.3.3-RC
 
 
+* Fix: Encode spaces in page path in LinkEditModal
+* Fix: Layout is broken when editing users page ([#4128](https://github.com/weseek/growi/issues/4128))
+* Support: Create @growi/core package
+* Support: Create @growi/ui package
+* Support: Improve error handling for @growi/slackbot-proxy
+* Support: Include official plugins as sub packages
 * Support: Upgrade libs
 * Support: Upgrade libs
     * @slack/web-api
     * @slack/web-api
     * escape-string-regexp
     * escape-string-regexp

+ 21 - 3
package.json

@@ -21,8 +21,12 @@
   },
   },
   "private": true,
   "private": true,
   "workspaces": {
   "workspaces": {
-    "packages": ["packages/*"],
-    "nohoist": ["**/slackbot-proxy/bootstrap"]
+    "packages": [
+      "packages/*"
+    ],
+    "nohoist": [
+      "**/slackbot-proxy/bootstrap"
+    ]
   },
   },
   "scripts": {
   "scripts": {
     "start": "yarn app:server",
     "start": "yarn app:server",
@@ -37,8 +41,17 @@
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
     "server:prod": "echo !!! CAUTION !!! ==> The script 'server:prod' is deprecated. Use 'yarn app:build' instead. && yarn app:server"
   },
   },
   "dependencies": {
   "dependencies": {
+    "cross-env": "^7.0.0",
+    "dotenv-flow": "^3.2.0",
+    "npm-run-all": "^4.1.5",
+    "ts-node": "^9.1.1",
+    "tsconfig-paths": "^3.9.0",
+    "typescript": "^4.2.3"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@types/jest": "^26.0.22",
+    "@types/node": "^14.14.35",
+    "@types/rewire": "^2.5.28",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/eslint-plugin": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "@typescript-eslint/parser": "^4.28.5",
     "eslint": "^7.31.0",
     "eslint": "^7.31.0",
@@ -48,7 +61,12 @@
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-jest": "^24.3.2",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react": "^7.24.0",
     "eslint-plugin-react-hooks": "^4.2.0",
     "eslint-plugin-react-hooks": "^4.2.0",
-    "lerna": "^4.0.0"
+    "jest": "^27.0.6",
+    "jest-date-mock": "^1.0.8",
+    "jest-localstorage-mock": "^2.4.14",
+    "lerna": "^4.0.0",
+    "rewire": "^5.0.0",
+    "ts-jest": "^27.0.4"
   },
   },
   "engines": {
   "engines": {
     "node": "^12 || ^14",
     "node": "^12 || ^14",

+ 2 - 2
packages/app/.env.development

@@ -3,7 +3,7 @@
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
 FILE_UPLOAD=mongodb
 FILE_UPLOAD=mongodb
-# MONGO_GRIDFS_TOTAL_LIMIT=10485760 # 10MB
+# MONGO_GRIDFS_TOTAL_LIMIT=10485760
 MATHJAX=1
 MATHJAX=1
 # NO_CDN=true
 # NO_CDN=true
 MONGO_URI="mongodb://mongo:27017/growi"
 MONGO_URI="mongodb://mongo:27017/growi"
@@ -17,7 +17,7 @@ HACKMD_URI_FOR_SERVER="http://hackmd:3000"
 # PUBLISH_OPEN_API=true
 # PUBLISH_OPEN_API=true
 # USER_UPPER_LIMIT=0
 # USER_UPPER_LIMIT=0
 # DEV_HTTPS=true
 # DEV_HTTPS=true
-# FORCE_WIKI_MODE=private # 'public', 'private', undefined
+# FORCE_WIKI_MODE=private
 # PROMSTER_ENABLED=true
 # PROMSTER_ENABLED=true
 # SLACK_SIGNING_SECRET=''
 # SLACK_SIGNING_SECRET=''
 # SLACK_BOT_TOKEN=''
 # SLACK_BOT_TOKEN=''

+ 1 - 1
packages/app/.env.production

@@ -2,4 +2,4 @@
 ## Handled by Next.js with dotenv or dotenv-flow
 ## Handled by Next.js with dotenv or dotenv-flow
 ## https://nextjs.org/docs/basic-features/environment-variables
 ## https://nextjs.org/docs/basic-features/environment-variables
 ##
 ##
-FORMAT_NODE_LOG=false # default: true
+FORMAT_NODE_LOG=false

+ 1 - 0
packages/app/.eslintignore

@@ -1,3 +1,4 @@
+/dist/**
 /public/**
 /public/**
 /src/client/legacy/thirdparty-js/**
 /src/client/legacy/thirdparty-js/**
 /src/client/util/reveal/plugins/markdown.js
 /src/client/util/reveal/plugins/markdown.js

+ 1 - 1
packages/app/.gitignore

@@ -13,7 +13,7 @@
 
 
 # dist (for GROWI v4.x and below)
 # dist (for GROWI v4.x and below)
 /public/*.chunk.js
 /public/*.chunk.js
-/public/*.chunk.js.LICENSE
+/public/*.chunk.js.LICENSE.txt
 /public/*.bundle.js
 /public/*.bundle.js
 /public/manifest.json
 /public/manifest.json
 /public/dll
 /public/dll

+ 22 - 14
packages/app/bin/generate-plugin-definitions-source.ts

@@ -7,6 +7,8 @@ import fs from 'graceful-fs';
 import normalize from 'normalize-path';
 import normalize from 'normalize-path';
 import swig from 'swig-templates';
 import swig from 'swig-templates';
 
 
+import { PluginDefinitionV4 } from '@growi/core';
+
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import PluginUtils from '../src/server/plugins/plugin-utils';
 import loggerFactory from '../src/utils/logger';
 import loggerFactory from '../src/utils/logger';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
 import { resolveFromRoot } from '../src/utils/project-dir-utils';
@@ -23,26 +25,32 @@ const OUT = resolveFromRoot('tmp/plugins/plugin-definitions.js');
 const pluginNames: string[] = pluginUtils.listPluginNames();
 const pluginNames: string[] = pluginUtils.listPluginNames();
 logger.info('Detected plugins: ', pluginNames);
 logger.info('Detected plugins: ', pluginNames);
 
 
-// get definitions
-const definitions = pluginNames
-  .map((name) => {
-    return pluginUtils.generatePluginDefinition(name, true);
-  })
-  .map((definition) => {
-    if (definition == null) {
-      return null;
+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
     // convert backslash to slash
     definition.entries = definition.entries.map((entryPath) => {
     definition.entries = definition.entries.map((entryPath) => {
       return normalize(entryPath);
       return normalize(entryPath);
     });
     });
     return definition;
     return definition;
-  })
-  .filter(definition => definition != null);
+  });
+
+  const compiledTemplate = swig.compileFile(TEMPLATE);
+  const code = compiledTemplate({ definitions });
+
+  // write
+  fs.writeFileSync(OUT, code);
 
 
-const compiledTemplate = swig.compileFile(TEMPLATE);
-const code = compiledTemplate({ definitions });
+}
 
 
-// write
-fs.writeFileSync(OUT, code);
+main();

+ 2 - 0
packages/app/config/ci/.env.local.for-ci

@@ -1,2 +1,4 @@
+FORMAT_NODE_LOG=true
+
 # disable Elasticsearch
 # disable Elasticsearch
 ELASTICSEARCH_URI=
 ELASTICSEARCH_URI=

+ 18 - 13
packages/app/docker/Dockerfile

@@ -17,18 +17,21 @@ COPY ./package.json .
 COPY ./yarn.lock .
 COPY ./yarn.lock .
 COPY ./lerna.json .
 COPY ./lerna.json .
 COPY ./packages/app/package.json packages/app/
 COPY ./packages/app/package.json packages/app/
-COPY ./packages/slack/package.json packages/slack/
+COPY ./packages/core/package.json packages/core/
+COPY ./packages/plugin-attachment-refs/package.json packages/plugin-attachment-refs/
+COPY ./packages/plugin-lsx/package.json packages/plugin-lsx/
 COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
 COPY ./packages/plugin-pukiwiki-like-linker/package.json packages/plugin-pukiwiki-like-linker/
+COPY ./packages/slack/package.json packages/slack/
+COPY ./packages/ui/package.json packages/ui/
 
 
 # setup
 # setup
 RUN yarn config set network-timeout 300000
 RUN yarn config set network-timeout 300000
 RUN npx lerna bootstrap
 RUN npx lerna bootstrap
 
 
 # make artifacts
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules \
-  packages/plugin-pukiwiki-like-linker/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
 
 
 
@@ -41,10 +44,9 @@ FROM deps-resolver AS deps-resolver-prod
 RUN yarn install --production
 RUN yarn install --production
 
 
 # make artifacts
 # make artifacts
-RUN tar cf node_modules.tar node_modules \
-  packages/app/node_modules \
-  packages/slack/node_modules \
-  packages/plugin-pukiwiki-like-linker/node_modules
+RUN tar cf node_modules.tar \
+  node_modules \
+  packages/*/node_modules
 
 
 
 
 
 
@@ -92,9 +94,13 @@ COPY ./yarn.lock ./
 COPY ./lerna.json ./
 COPY ./lerna.json ./
 COPY ./tsconfig.base.json ./
 COPY ./tsconfig.base.json ./
 # copy all related packages
 # copy all related packages
-COPY packages/slack packages/slack
 COPY packages/app packages/app
 COPY packages/app packages/app
+COPY packages/core packages/core
+COPY packages/plugin-attachment-refs packages/plugin-attachment-refs
+COPY packages/plugin-lsx packages/plugin-lsx
 COPY packages/plugin-pukiwiki-like-linker packages/plugin-pukiwiki-like-linker
 COPY packages/plugin-pukiwiki-like-linker packages/plugin-pukiwiki-like-linker
+COPY packages/slack packages/slack
+COPY packages/ui packages/ui
 
 
 # build
 # build
 RUN yarn lerna run build
 RUN yarn lerna run build
@@ -113,9 +119,8 @@ RUN tar cf packages.tar \
   packages/app/.env.production \
   packages/app/.env.production \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.base.json \
   packages/app/tsconfig.json \
   packages/app/tsconfig.json \
-  packages/slack/package.json \
-  packages/slack/dist
-
+  packages/*/package.json \
+  packages/*/dist
 
 
 
 
 
 

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

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

+ 9 - 14
packages/app/package.json

@@ -55,7 +55,9 @@
   "dependencies": {
   "dependencies": {
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
+    "@growi/plugin-attachment-refs": "^4.3.3-RC",
     "@growi/plugin-pukiwiki-like-linker": "^4.3.3-RC",
     "@growi/plugin-pukiwiki-like-linker": "^4.3.3-RC",
+    "@growi/plugin-lsx": "^4.3.3-RC",
     "@growi/slack": "^4.3.3-RC",
     "@growi/slack": "^4.3.3-RC",
     "@kobalab/socket.io-session": "^1.0.3",
     "@kobalab/socket.io-session": "^1.0.3",
     "@promster/express": "^5.0.1",
     "@promster/express": "^5.0.1",
@@ -77,12 +79,10 @@
     "connect-mongo": "^4.4.1",
     "connect-mongo": "^4.4.1",
     "connect-redis": "^4.0.4",
     "connect-redis": "^4.0.4",
     "cookie-parser": "^1.4.5",
     "cookie-parser": "^1.4.5",
-    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
     "date-fns": "^2.0.0",
     "detect-indent": "^6.0.0",
     "detect-indent": "^6.0.0",
     "diff": "^5.0.0",
     "diff": "^5.0.0",
-    "dotenv-flow": "^3.2.0",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "entities": "^2.0.0",
     "esa-nodejs": "^0.0.7",
     "esa-nodejs": "^0.0.7",
@@ -96,10 +96,14 @@
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^5.0.4",
     "growi-commons": "^5.0.4",
+<<<<<<< HEAD
     "growi-plugin-attachment-refs": "^2.0.2",
     "growi-plugin-attachment-refs": "^2.0.2",
     "growi-plugin-lsx": "^4.0.3",
     "growi-plugin-lsx": "^4.0.3",
     "helmet": "^4.6.0",
     "helmet": "^4.6.0",
     "nocache": "^3.0.1",
     "nocache": "^3.0.1",
+=======
+    "helmet": "^3.13.0",
+>>>>>>> f656a3e1b8ac561c4acccae032edc3ee207413a2
     "http-errors": "~1.6.2",
     "http-errors": "~1.6.2",
     "i18next": "^20.3.2",
     "i18next": "^20.3.2",
     "i18next-express-middleware": "^2.0.0",
     "i18next-express-middleware": "^2.0.0",
@@ -119,7 +123,6 @@
     "multer-autoreap": "^1.0.3",
     "multer-autoreap": "^1.0.3",
     "nodemailer": "^6.6.2",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
-    "npm-run-all": "^4.1.2",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
@@ -135,14 +138,11 @@
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",
-    "socket.io": "^2.3.0",
+    "socket.io": "^4.0.0",
     "stream-to-promise": "^3.0.0",
     "stream-to-promise": "^3.0.0",
     "string-width": "=4.2.2",
     "string-width": "=4.2.2",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swig-templates": "^2.0.2",
     "swig-templates": "^2.0.2",
-    "ts-node": "^9.1.1",
-    "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.2.3",
     "uglifycss": "^0.0.29",
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "universal-bunyan": "^0.9.2",
     "unzipper": "^0.10.5",
     "unzipper": "^0.10.5",
@@ -160,11 +160,11 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
+    "@growi/ui": "^4.3.3-RC",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",
     "@types/multer": "^1.4.5",
     "@types/multer": "^1.4.5",
-    "@types/node": "^14.14.35",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "autoprefixer": "^9.0.0",
     "autoprefixer": "^9.0.0",
     "bootstrap": "^4.5.0",
     "bootstrap": "^4.5.0",
@@ -184,8 +184,6 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
     "imports-loader": "^0.8.0",
-    "jest": "^27.0.6",
-    "jest-date-mock": "^1.0.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
@@ -223,15 +221,13 @@
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
     "react-i18next": "^11.1.0",
-    "react-images": "1.0.0",
-    "react-motion": "^0.5.2",
     "react-waypoint": "^10.1.0",
     "react-waypoint": "^10.1.0",
     "reactstrap": "^8.9.0",
     "reactstrap": "^8.9.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
     "sass-loader": "^8.0.0",
     "sass-loader": "^8.0.0",
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
-    "socket.io-client": "^2.3.0",
+    "socket.io-client": "^4.0.0",
     "sticky-events": "^3.1.3",
     "sticky-events": "^3.1.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
     "styled-components": "^5.0.1",
     "styled-components": "^5.0.1",
@@ -241,7 +237,6 @@
     "terser-webpack-plugin": "^4.1.0",
     "terser-webpack-plugin": "^4.1.0",
     "throttle-debounce": "^2.0.0",
     "throttle-debounce": "^2.0.0",
     "toastr": "^2.1.2",
     "toastr": "^2.1.2",
-    "ts-jest": "^27.0.4",
     "ts-loader": "^8.3.0",
     "ts-loader": "^8.3.0",
     "ts-node-dev": "^1.1.6",
     "ts-node-dev": "^1.1.6",
     "tsc-alias": "^1.2.9",
     "tsc-alias": "^1.2.9",

+ 20 - 1
packages/app/src/client/models/Linker.js

@@ -1,3 +1,8 @@
+
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
+
 export default class Linker {
 export default class Linker {
 
 
   constructor(
   constructor(
@@ -5,10 +10,15 @@ export default class Linker {
       label = '',
       label = '',
       link = '',
       link = '',
   ) {
   ) {
+
     this.type = type;
     this.type = type;
     this.label = label;
     this.label = label;
     this.link = link;
     this.link = link;
 
 
+    if (type === Linker.types.markdownLink) {
+      this.initWhenMarkdownLink();
+    }
+
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
   }
 
 
@@ -25,9 +35,18 @@ export default class Linker {
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
     markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
   }
   }
 
 
+  initWhenMarkdownLink() {
+    // fill label with link if empty
+    if (this.label === '') {
+      this.label = this.link;
+    }
+    // encode spaces
+    this.link = encodeSpaces(this.link);
+  }
+
   generateMarkdownText() {
   generateMarkdownText() {
     if (this.type === Linker.types.pukiwikiLink) {
     if (this.type === Linker.types.pukiwikiLink) {
-      if (this.label === this.link) return `[[${this.link}]]`;
+      if (this.label === '') return `[[${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
       return `[[${this.label}>${this.link}]]`;
     }
     }
     if (this.type === Linker.types.growiLink) {
     if (this.type === Linker.types.growiLink) {

+ 4 - 2
packages/app/src/client/plugin.js

@@ -29,15 +29,17 @@ export default class GrowiPlugin {
       const meta = definition.meta;
       const meta = definition.meta;
 
 
       switch (meta.pluginSchemaVersion) {
       switch (meta.pluginSchemaVersion) {
-        // v1 is deprecated
+        // v1, v2 and v3 is deprecated
         case 1:
         case 1:
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           logger.warn('pluginSchemaVersion 1 is deprecated', definition);
           break;
           break;
-        // v2 is deprecated
         case 2:
         case 2:
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           logger.warn('pluginSchemaVersion 2 is deprecated', definition);
           break;
           break;
         case 3:
         case 3:
+          logger.warn('pluginSchemaVersion 2 is deprecated', definition);
+          break;
+        case 4:
           definition.entries.forEach((entry) => {
           definition.entries.forEach((entry) => {
             entry(appContainer);
             entry(appContainer);
           });
           });

+ 1 - 1
packages/app/src/client/services/AdminGitHubSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminGoogleSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminOidcSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminSamlSecurityContainer.js

@@ -1,7 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 1 - 1
packages/app/src/client/services/AdminTwitterSecurityContainer.js

@@ -1,6 +1,6 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';
 import { removeNullPropertyFromObject } from '~/utils/object-utils';

+ 3 - 1
packages/app/src/client/services/PageContainer.js

@@ -3,8 +3,8 @@ import { Container } from 'unstated';
 
 
 import * as entities from 'entities';
 import * as entities from 'entities';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { isTrashPage } from '~/utils/path-utils';
 import { toastError } from '../util/apiNotification';
 import { toastError } from '../util/apiNotification';
 
 
 import {
 import {
@@ -16,6 +16,8 @@ import {
   DrawioInterceptor,
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
 } from '../util/interceptor/drawio-interceptor';
 
 
+const { isTrashPage } = pagePathUtils;
+
 const logger = loggerFactory('growi:services:PageContainer');
 const logger = loggerFactory('growi:services:PageContainer');
 
 
 /**
 /**

+ 1 - 1
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -6,7 +6,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -4,11 +4,11 @@ import { withTranslation } from 'react-i18next';
 
 
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import UserPicture from '../../User/UserPicture';
 
 
 class UserGroupUserFormByInput extends React.Component {
 class UserGroupUserFormByInput extends React.Component {
 
 

+ 1 - 1
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
 import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';

+ 1 - 1
packages/app/src/components/Admin/Users/UserTable.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 
 
-import UserPicture from '../../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import UserMenu from './UserMenu';
 import UserMenu from './UserMenu';
 
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';

+ 3 - 1
packages/app/src/components/ComparePathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function ComparePathsTable(props) {
 function ComparePathsTable(props) {
   const {
   const {

+ 3 - 1
packages/app/src/components/ContentLinkButtons.jsx

@@ -1,7 +1,7 @@
 import React, { useMemo } from 'react';
 import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -10,6 +10,8 @@ import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 const WIKI_HEADER_LINK = 120;
 const WIKI_HEADER_LINK = 120;
 
 
 /**
 /**

+ 1 - 1
packages/app/src/components/CreateTemplateModal.jsx

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 

+ 3 - 1
packages/app/src/components/DuplicatedPathsTable.jsx

@@ -2,10 +2,12 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import { convertToNewAffiliationPath } from '~/utils/path-utils';
+
+const { convertToNewAffiliationPath } = pagePathUtils;
 
 
 function DuplicatedPathsTable(props) {
 function DuplicatedPathsTable(props) {
   const {
   const {

+ 1 - 1
packages/app/src/components/Fab.jsx

@@ -62,7 +62,7 @@ const Fab = (props) => {
   }
   }
 
 
   return (
   return (
-    <div className="grw-fab d-none d-md-block">
+    <div className="grw-fab d-none d-md-block d-edit-none">
       {currentUser != null && renderPageCreateButton()}
       {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button
         <button

+ 4 - 2
packages/app/src/components/Navbar/AuthorInfo.jsx

@@ -1,9 +1,11 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
-import { userPageRoot } from '~/utils/path-utils';
+import { UserPicture } from '@growi/ui';
+import { pagePathUtils } from '@growi/core';
+
+const { userPageRoot } = pagePathUtils;
 
 
-import UserPicture from '../User/UserPicture';
 
 
 const formatType = 'yyyy/MM/dd HH:mm';
 const formatType = 'yyyy/MM/dd HH:mm';
 const AuthorInfo = (props) => {
 const AuthorInfo = (props) => {

+ 1 - 1
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';

+ 1 - 1
packages/app/src/components/Navbar/PersonalDropdown.jsx

@@ -5,6 +5,7 @@ import { withTranslation } from 'react-i18next';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -18,7 +19,6 @@ import {
   updateUserPreferenceWithOsSettings,
   updateUserPreferenceWithOsSettings,
 } from '~/client/util/color-scheme';
 } from '~/client/util/color-scheme';
 
 
-import UserPicture from '../User/UserPicture';
 
 
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDrawerIcon from '../Icons/SidebarDrawerIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';
 import SidebarDockIcon from '../Icons/SidebarDockIcon';

+ 3 - 1
packages/app/src/components/Page/CopyDropdown.jsx

@@ -12,7 +12,9 @@ import {
 
 
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 
 
-import { encodeSpaces } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
+
+const { encodeSpaces } = pagePathUtils;
 
 
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 const DropdownItemContents = ({ title, contents }) => (

+ 3 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -4,7 +4,7 @@ import { UncontrolledTooltip } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { isTopPage } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
@@ -15,6 +15,8 @@ import CreateTemplateModal from '../CreateTemplateModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PagePresentationModal from '../PagePresentationModal';
 import PresentationIcon from '../Icons/PresentationIcon';
 import PresentationIcon from '../Icons/PresentationIcon';
 
 
+const { isTopPage } = pagePathUtils;
+
 
 
 const PageManagement = (props) => {
 const PageManagement = (props) => {
   const {
   const {

+ 1 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UserPicture } from '@growi/ui';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
-import UserPicture from '../User/UserPicture';
 import PutbackPageModal from '../PutbackPageModal';
 import PutbackPageModal from '../PutbackPageModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import EmptyTrashModal from '../EmptyTrashModal';
 import PageDeleteModal from '../PageDeleteModal';
 import PageDeleteModal from '../PageDeleteModal';

+ 1 - 1
packages/app/src/components/PageAttachment/DeleteAttachmentModal.jsx

@@ -6,7 +6,7 @@ import {
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteAttachmentModal extends React.Component {
 export default class DeleteAttachmentModal extends React.Component {

+ 1 - 1
packages/app/src/components/PageAttachment/PageAttachmentList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import Attachment from './Attachment';
+import { Attachment } from '@growi/ui';
 
 
 export default class PageAttachmentList extends React.Component {
 export default class PageAttachmentList extends React.Component {
 
 

+ 1 - 1
packages/app/src/components/PageComment/Comment.jsx

@@ -6,6 +6,7 @@ import { format } from 'date-fns';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 
 
@@ -13,7 +14,6 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import RevisionBody from '../Page/RevisionBody';
 import RevisionBody from '../Page/RevisionBody';
-import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
 import CommentEditor from './CommentEditor';
 import CommentControl from './CommentControl';
 import CommentControl from './CommentControl';

+ 1 - 1
packages/app/src/components/PageComment/CommentEditor.jsx

@@ -8,6 +8,7 @@ import {
 
 
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
+import { UserPicture } from '@growi/ui';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import CommentContainer from '~/client/services/CommentContainer';
 import CommentContainer from '~/client/services/CommentContainer';
@@ -15,7 +16,6 @@ import EditorContainer from '~/client/services/EditorContainer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 import GrowiRenderer from '~/client/util/GrowiRenderer';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
-import UserPicture from '../User/UserPicture';
 import Editor from '../PageEditor/Editor';
 import Editor from '../PageEditor/Editor';
 import SlackNotification from '../SlackNotification';
 import SlackNotification from '../SlackNotification';
 
 

+ 1 - 1
packages/app/src/components/PageComment/DeleteCommentModal.jsx

@@ -7,7 +7,7 @@ import {
 
 
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
-import UserPicture from '../User/UserPicture';
+import { UserPicture } from '@growi/ui';
 import Username from '../User/Username';
 import Username from '../User/Username';
 
 
 export default class DeleteCommentModal extends React.Component {
 export default class DeleteCommentModal extends React.Component {

+ 6 - 4
packages/app/src/components/PageCreateModal.jsx

@@ -7,10 +7,8 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
-import { pathUtils } from 'growi-commons';
-import {
-  userPageRoot, isCreatablePage, generateEditorPath,
-} from '~/utils/path-utils';
+import { pagePathUtils, pathUtils } from '@growi/core';
+
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
@@ -19,6 +17,10 @@ import { toastError } from '~/client/util/apiNotification';
 
 
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 
+const {
+  userPageRoot, isCreatablePage, generateEditorPath,
+} = pagePathUtils;
+
 const PageCreateModal = (props) => {
 const PageCreateModal = (props) => {
   const { t, appContainer, navigationContainer } = props;
   const { t, appContainer, navigationContainer } = props;
 
 

+ 6 - 19
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -13,17 +13,18 @@ import {
 import path from 'path';
 import path from 'path';
 import validator from 'validator';
 import validator from 'validator';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-import PreviewWithSuspense from './PreviewWithSuspense';
-import PagePreviewIcon from '../Icons/PagePreviewIcon';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import Linker from '~/client/models/Linker';
 
 
+import PreviewWithSuspense from './PreviewWithSuspense';
+import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import SearchTypeahead from '../SearchTypeahead';
 import SearchTypeahead from '../SearchTypeahead';
-import Linker from '~/client/models/Linker';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
+
 class LinkEditModal extends React.PureComponent {
 class LinkEditModal extends React.PureComponent {
 
 
   constructor(props) {
   constructor(props) {
@@ -175,22 +176,8 @@ class LinkEditModal extends React.PureComponent {
     this.setState({ markdown, previewError, permalink });
     this.setState({ markdown, previewError, permalink });
   }
   }
 
 
-  getLinkForPreview() {
-    const linker = this.generateLink();
-
-    if (this.isUsePermanentLink && this.permalink != null) {
-      linker.link = this.permalink;
-    }
-
-    if (linker.label === '') {
-      linker.label = linker.link;
-    }
-
-    return linker;
-  }
-
   renderLinkPreview() {
   renderLinkPreview() {
-    const linker = this.getLinkForPreview();
+    const linker = this.generateLink();
     return (
     return (
       <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
       <div className="d-flex justify-content-between mb-3 flex-column flex-sm-row">
         <div className="card card-disabled w-100 p-1 mb-0">
         <div className="card card-disabled w-100 p-1 mb-0">
@@ -245,7 +232,7 @@ class LinkEditModal extends React.PureComponent {
   }
   }
 
 
   save() {
   save() {
-    const linker = this.getLinkForPreview();
+    const linker = this.generateLink();
 
 
     if (this.props.onSave != null) {
     if (this.props.onSave != null) {
       this.props.onSave(linker.generateMarkdownText());
       this.props.onSave(linker.generateMarkdownText());

+ 1 - 1
packages/app/src/components/PageHistory/Revision.jsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+import { UserPicture } from '@growi/ui';
 import UserDate from '../User/UserDate';
 import UserDate from '../User/UserDate';
 import Username from '../User/Username';
 import Username from '../User/Username';
-import UserPicture from '../User/UserPicture';
 
 
 export default class Revision extends React.Component {
 export default class Revision extends React.Component {
 
 

+ 2 - 3
packages/app/src/components/PageList/Page.jsx

@@ -1,9 +1,8 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from '../User/UserPicture';
-import PageListMeta from './PageListMeta';
-import PagePathLabel from './PagePathLabel';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
 
 
 export default class Page extends React.Component {
 export default class Page extends React.Component {
 
 

+ 0 - 24
packages/app/src/components/PageList/PagePath.jsx

@@ -1,24 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import PagePathLabel from './PagePathLabel';
-
-/**
- * !!DEPRECATED!!
- *
- * maintained for backward compatibility for growi-lsx-plugin(<= 3.1.1)
- */
-const PagePath = props => (
-  <PagePathLabel isLatterOnly={props.isShortPathOnly} {...props} />
-);
-
-PagePath.propTypes = {
-  isShortPathOnly: PropTypes.bool,
-  ...PagePathLabel.propTypes,
-};
-
-PagePath.defaultProps = {
-  ...PagePathLabel.defaultProps,
-};
-
-export default PagePath;

+ 1 - 1
packages/app/src/components/PagePathAutoComplete.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import SearchTypeahead from './SearchTypeahead';
 import SearchTypeahead from './SearchTypeahead';
 
 

+ 3 - 1
packages/app/src/components/RevisionComparer/RevisionComparer.jsx

@@ -6,7 +6,7 @@ import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { encodeSpaces } from '~/utils/path-utils';
+import { pagePathUtils } from '@growi/core';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
@@ -14,6 +14,8 @@ import RevisionComparerContainer from '~/client/services/RevisionComparerContain
 
 
 import RevisionDiff from '../PageHistory/RevisionDiff';
 import RevisionDiff from '../PageHistory/RevisionDiff';
 
 
+const { encodeSpaces } = pagePathUtils;
+
 /* eslint-disable react/prop-types */
 /* eslint-disable react/prop-types */
 const DropdownItemContents = ({ title, contents }) => (
 const DropdownItemContents = ({ title, contents }) => (
   <>
   <>

+ 2 - 3
packages/app/src/components/SearchTypeahead.jsx

@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
 import { noop } from 'lodash/noop';
 import { noop } from 'lodash/noop';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
-import UserPicture from './User/UserPicture';
-import PageListMeta from './PageList/PageListMeta';
-import PagePathLabel from './PageList/PagePathLabel';
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 

+ 2 - 2
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -3,10 +3,11 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UserPicture } from '@growi/ui';
+import { DevidedPagePath } from '@growi/core';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import DevidedPagePath from '~/models/devided-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
@@ -14,7 +15,6 @@ import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
-import UserPicture from '../User/UserPicture';
 
 
 const logger = loggerFactory('growi:History');
 const logger = loggerFactory('growi:History');
 class RecentChanges extends React.Component {
 class RecentChanges extends React.Component {

+ 1 - 1
packages/app/src/components/User/UserInfo.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from './UserPicture';
+import { UserPicture } from '@growi/ui';
 
 
 const UserInfo = (props) => {
 const UserInfo = (props) => {
   const { pageUser } = props;
   const { pageUser } = props;

+ 1 - 1
packages/app/src/components/User/UserPictureList.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import UserPicture from './UserPicture';
+import { UserPicture } from '@growi/ui';
 
 
 export default class UserPictureList extends React.Component {
 export default class UserPictureList extends React.Component {
 
 

+ 1 - 1
packages/app/src/migrations/20191126173016-adjust-pages-path.js

@@ -1,5 +1,5 @@
 import mongoose from 'mongoose';
 import mongoose from 'mongoose';
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import config from '^/config/migrate';
 import config from '^/config/migrate';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';

+ 3 - 6
packages/app/src/models/linked-page-path.js

@@ -1,12 +1,11 @@
-const { pathUtils } = require('growi-commons');
-const { isTrashPage } = require('~/utils/path-utils');
+import { pagePathUtils, DevidedPagePath, pathUtils } from '@growi/core';
 
 
-const DevidedPagePath = require('./devided-page-path');
+const { isTrashPage } = pagePathUtils;
 
 
 /**
 /**
  * Linked Array Structured PagePath Model
  * Linked Array Structured PagePath Model
  */
  */
-class LinkedPagePath {
+export default class LinkedPagePath {
 
 
   constructor(path, skipNormalize = false) {
   constructor(path, skipNormalize = false) {
 
 
@@ -34,5 +33,3 @@ class LinkedPagePath {
   }
   }
 
 
 }
 }
-
-module.exports = LinkedPagePath;

+ 1 - 1
packages/app/src/server/crowi/index.js

@@ -427,7 +427,7 @@ Crowi.prototype.start = async function() {
 
 
   // setup plugins
   // setup plugins
   this.pluginService = new PluginService(this, express);
   this.pluginService = new PluginService(this, express);
-  this.pluginService.autoDetectAndLoadPlugins();
+  await this.pluginService.autoDetectAndLoadPlugins();
 
 
   const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
   const server = (this.node_env === 'development') ? this.crowiDev.setupServer(express) : express;
 
 

+ 3 - 4
packages/app/src/server/middlewares/admin-required.js

@@ -7,14 +7,13 @@ module.exports = (crowi, fallback = null) => {
   return async(req, res, next) => {
   return async(req, res, next) => {
     if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
     if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user.admin) {
       if (req.user.admin) {
-        next();
-        return;
+        return next();
       }
       }
 
 
       logger.warn('This user is not admin.');
       logger.warn('This user is not admin.');
 
 
       if (fallback != null) {
       if (fallback != null) {
-        return fallback(req, res);
+        return fallback(req, res, next);
       }
       }
       return res.redirect('/');
       return res.redirect('/');
     }
     }
@@ -22,7 +21,7 @@ module.exports = (crowi, fallback = null) => {
     logger.warn('This user has not logged in.');
     logger.warn('This user has not logged in.');
 
 
     if (fallback != null) {
     if (fallback != null) {
-      return fallback(req, res);
+      return fallback(req, res, next);
     }
     }
     return res.redirect('/login');
     return res.redirect('/login');
   };
   };

+ 5 - 3
packages/app/src/server/models/page.js

@@ -1,3 +1,4 @@
+import { templateChecker, pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 // disable no-return-await for model functions
 // disable no-return-await for model functions
@@ -15,8 +16,9 @@ const differenceInYears = require('date-fns/differenceInYears');
 
 
 const { pathUtils } = require('growi-commons');
 const { pathUtils } = require('growi-commons');
 const escapeStringRegexp = require('escape-string-regexp');
 const escapeStringRegexp = require('escape-string-regexp');
-const templateChecker = require('~/utils/template-checker');
-const { isTopPage, isTrashPage } = require('~/utils/path-utils');
+
+const { isTopPage, isTrashPage } = pagePathUtils;
+const { checkTemplatePath } = templateChecker;
 
 
 const logger = loggerFactory('growi:models:page');
 const logger = loggerFactory('growi:models:page');
 
 
@@ -311,7 +313,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   pageSchema.methods.isTemplate = function() {
   pageSchema.methods.isTemplate = function() {
-    return templateChecker(this.path);
+    return checkTemplatePath(this.path);
   };
   };
 
 
   pageSchema.methods.isLatestRevision = function() {
   pageSchema.methods.isLatestRevision = function() {

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

@@ -0,0 +1,38 @@
+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,
+    };
+  }
+
+}

+ 15 - 7
packages/app/src/server/plugins/plugin-utils.js

@@ -1,13 +1,13 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
 
-const fs = require('graceful-fs');
+import { PluginUtilsV4 } from './plugin-utils-v4';
 
 
-const PluginUtilsV2 = require('./plugin-utils-v2');
+const fs = require('graceful-fs');
 
 
 const logger = loggerFactory('growi:plugins:plugin-utils');
 const logger = loggerFactory('growi:plugins:plugin-utils');
 
 
-const pluginUtilsV2 = new PluginUtilsV2();
+const pluginUtilsV4 = new PluginUtilsV4();
 
 
 class PluginUtils {
 class PluginUtils {
 
 
@@ -26,19 +26,27 @@ class PluginUtils {
    * @return
    * @return
    * @memberOf PluginService
    * @memberOf PluginService
    */
    */
-  generatePluginDefinition(name, isForClient = false) {
+  async generatePluginDefinition(name, isForClient = false) {
     const meta = require(name);
     const meta = require(name);
     let definition;
     let definition;
 
 
     switch (meta.pluginSchemaVersion) {
     switch (meta.pluginSchemaVersion) {
-      // v1 is deprecated
+      // v1, v2 and v3 is deprecated
       case 1:
       case 1:
         logger.debug('pluginSchemaVersion 1 is deprecated');
         logger.debug('pluginSchemaVersion 1 is deprecated');
         break;
         break;
-      // v2 or above
       case 2:
       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:
       default:
-        definition = pluginUtilsV2.generatePluginDefinition(name, isForClient);
+        logger.warn('Unsupported schema version', meta.pluginSchemaVersion);
     }
     }
 
 
     return definition;
     return definition;

+ 16 - 11
packages/app/src/server/plugins/plugin.service.js

@@ -12,13 +12,13 @@ class PluginService {
     this.pluginUtils = new PluginUtils();
     this.pluginUtils = new PluginUtils();
   }
   }
 
 
-  autoDetectAndLoadPlugins() {
+  async autoDetectAndLoadPlugins() {
     const isEnabledPlugins = this.crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
     const isEnabledPlugins = this.crowi.configManager.getConfig('crowi', 'plugin:isEnabledPlugins');
 
 
     // import plugins
     // import plugins
     if (isEnabledPlugins) {
     if (isEnabledPlugins) {
       logger.debug('Plugins are enabled');
       logger.debug('Plugins are enabled');
-      this.loadPlugins(this.pluginUtils.listPluginNames(this.crowi.rootDir));
+      return this.loadPlugins(this.pluginUtils.listPluginNames(this.crowi.rootDir));
     }
     }
 
 
   }
   }
@@ -28,29 +28,34 @@ class PluginService {
    *
    *
    * @memberOf PluginService
    * @memberOf PluginService
    */
    */
-  loadPlugins(pluginNames) {
-    pluginNames
-      .map((name) => {
-        return this.pluginUtils.generatePluginDefinition(name);
-      })
-      .forEach((definition) => {
+  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);
         this.loadPlugin(definition);
-      });
+      }
+    }
   }
   }
 
 
   loadPlugin(definition) {
   loadPlugin(definition) {
     const meta = definition.meta;
     const meta = definition.meta;
 
 
     switch (meta.pluginSchemaVersion) {
     switch (meta.pluginSchemaVersion) {
-      // v1 is deprecated
+      // v1, v2 and v3 is deprecated
       case 1:
       case 1:
         logger.warn('pluginSchemaVersion 1 is deprecated', definition);
         logger.warn('pluginSchemaVersion 1 is deprecated', definition);
         break;
         break;
-      // v2 is deprecated
       case 2:
       case 2:
         logger.warn('pluginSchemaVersion 2 is deprecated', definition);
         logger.warn('pluginSchemaVersion 2 is deprecated', definition);
         break;
         break;
       case 3:
       case 3:
+        logger.warn('pluginSchemaVersion 3 is deprecated', definition);
+        break;
+      // v4 or above
+      case 4:
         logger.info(`load plugin '${definition.name}'`);
         logger.info(`load plugin '${definition.name}'`);
         definition.entries.forEach((entryPath) => {
         definition.entries.forEach((entryPath) => {
           const entry = require(entryPath);
           const entry = require(entryPath);

+ 3 - 2
packages/app/src/server/routes/apiv3/page.js

@@ -1,13 +1,14 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
 
 
 const express = require('express');
 const express = require('express');
 const { body, query } = require('express-validator');
 const { body, query } = require('express-validator');
 
 
 const router = express.Router();
 const router = express.Router();
-
-const { convertToNewAffiliationPath } = require('~/utils/path-utils');
+const { convertToNewAffiliationPath } = pagePathUtils;
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 
 

+ 7 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -1,3 +1,4 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
@@ -6,9 +7,11 @@ const pathUtils = require('growi-commons').pathUtils;
 
 
 const { body } = require('express-validator');
 const { body } = require('express-validator');
 const { query } = require('express-validator');
 const { query } = require('express-validator');
-const { isCreatablePage } = require('~/utils/path-utils');
+
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
+const { isCreatablePage } = pagePathUtils;
+
 const router = express.Router();
 const router = express.Router();
 
 
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_LIST = 10;
@@ -458,7 +461,9 @@ module.exports = (crowi) => {
   ];
   ];
 
 
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
   router.get('/list', accessTokenParser, loginRequired, validator.displayList, apiV3FormValidator, async(req, res) => {
-    const { isTrashPage } = require('~/utils/path-utils');
+
+
+    const { isTrashPage } = pagePathUtils;
 
 
     const { path } = req.query;
     const { path } = req.query;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationS') || 10;

+ 2 - 1
packages/app/src/server/routes/page.js

@@ -1,6 +1,7 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-const { isCreatablePage } = require('~/utils/path-utils');
+const { isCreatablePage } = pagePathUtils;
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');

+ 2 - 2
packages/app/src/server/service/app.ts

@@ -1,4 +1,4 @@
-import { pathUtils } from 'growi-commons';
+import { pathUtils } from '@growi/core';
 
 
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -119,7 +119,7 @@ class AppService implements S2sMessageHandlable {
   }
   }
 
 
   async setupAfterInstall() {
   async setupAfterInstall() {
-    this.crowi.pluginService.autoDetectAndLoadPlugins();
+    await this.crowi.pluginService.autoDetectAndLoadPlugins();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupRoutesAtLast();
     this.crowi.setupGlobalErrorHandlers();
     this.crowi.setupGlobalErrorHandlers();
 
 

+ 1 - 1
packages/app/src/server/service/customize.ts

@@ -1,8 +1,8 @@
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 import uglifycss from 'uglifycss';
 import uglifycss from 'uglifycss';
 
 
+import { DevidedPagePath } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import DevidedPagePath from '~/models/devided-page-path';
 
 
 import S2sMessage from '../models/vo/s2s-message';
 import S2sMessage from '../models/vo/s2s-message';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';
 import { S2sMessageHandlable } from './s2s-messaging/handlable';

+ 3 - 1
packages/app/src/server/service/global-notification/global-notification-slack.js

@@ -1,9 +1,11 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:service:GlobalNotificationSlackService'); // eslint-disable-line no-unused-vars
 const urljoin = require('url-join');
 const urljoin = require('url-join');
 
 
-const { encodeSpaces } = require('~/utils/path-utils');
+const { encodeSpaces } = pagePathUtils;
 
 
 /**
 /**
  * sub service class of GlobalNotificationSetting
  * sub service class of GlobalNotificationSetting

+ 3 - 1
packages/app/src/server/service/page.js

@@ -1,3 +1,4 @@
+import { pagePathUtils } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const mongoose = require('mongoose');
 const mongoose = require('mongoose');
@@ -7,7 +8,8 @@ const logger = loggerFactory('growi:models:page');
 const debug = require('debug')('growi:models:page');
 const debug = require('debug')('growi:models:page');
 const { Writable } = require('stream');
 const { Writable } = require('stream');
 const { createBatchStream } = require('~/server/util/batch-stream');
 const { createBatchStream } = require('~/server/util/batch-stream');
-const { isTrashPage } = require('~/utils/path-utils');
+
+const { isTrashPage } = pagePathUtils;
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 
 
 const BULK_REINDEX_SIZE = 100;
 const BULK_REINDEX_SIZE = 100;

+ 2 - 13
packages/app/src/server/service/socket-io.js

@@ -117,22 +117,11 @@ class SocketIoService {
     });
     });
   }
   }
 
 
-  async getClients(namespace) {
-    return new Promise((resolve, reject) => {
-      namespace.clients((error, clients) => {
-        if (error) {
-          reject(error);
-        }
-        resolve(clients);
-      });
-    });
-  }
-
   async checkConnectionLimitsForAdmin(socket, next) {
   async checkConnectionLimitsForAdmin(socket, next) {
     const namespaceName = socket.nsp.name;
     const namespaceName = socket.nsp.name;
 
 
     if (namespaceName === '/admin') {
     if (namespaceName === '/admin') {
-      const clients = await this.getClients(this.getAdminSocket());
+      const clients = await this.getAdminSocket().allSockets();
       const clientsCount = clients.length;
       const clientsCount = clients.length;
 
 
       logger.debug('Current count of clients for \'/admin\':', clientsCount);
       logger.debug('Current count of clients for \'/admin\':', clientsCount);
@@ -178,7 +167,7 @@ class SocketIoService {
       next();
       next();
     }
     }
 
 
-    const clients = await this.getClients(this.getDefaultSocket());
+    const clients = await this.getDefaultSocket().allSockets();
     const clientsCount = clients.length;
     const clientsCount = clients.length;
 
 
     logger.debug('Current count of clients for \'/\':', clientsCount);
     logger.debug('Current count of clients for \'/\':', clientsCount);

+ 3 - 1
packages/app/src/server/util/createGrowiPagesFromImports.js

@@ -1,4 +1,6 @@
-const { isCreatablePage } = require('~/utils/path-utils');
+import { pagePathUtils } from '@growi/core';
+
+const { isCreatablePage } = pagePathUtils;
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');

+ 1 - 1
packages/app/src/server/views/layout-growi/base/layout.html

@@ -33,7 +33,7 @@
     </div>
     </div>
   </div>
   </div>
 
 
-  <footer class="footer">
+  <footer class="footer d-edit-none">
     {% block content_footer %}{% endblock %}
     {% block content_footer %}{% endblock %}
   </footer>
   </footer>
 
 

+ 1 - 0
packages/app/src/styles/_on-edit.scss

@@ -144,6 +144,7 @@ body.on-edit {
         flex-flow: row nowrap;
         flex-flow: row nowrap;
         width: 100%;
         width: 100%;
         overflow-x: auto;
         overflow-x: auto;
+        overflow-y: hidden;
         scrollbar-width: thin;
         scrollbar-width: thin;
       }
       }
     }
     }

+ 2 - 5
packages/app/tsconfig.base.json

@@ -1,9 +1,6 @@
 {
 {
   "extends": "../../tsconfig.base.json",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
-  },
-  "exclude": [
-    "node_modules",
-    "**/*.test.ts"
-  ]
+    "jsx": "react"
+  }
 }
 }

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

@@ -2,7 +2,6 @@
   "extends": "./tsconfig.base.json",
   "extends": "./tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
     "module": "esnext",
     "module": "esnext",
-    "jsx": "react",
     "noFallthroughCasesInSwitch": true,
     "noFallthroughCasesInSwitch": true,
     "noUnusedLocals": true,
     "noUnusedLocals": true,
     "noUnusedParameters": true,
     "noUnusedParameters": true,

+ 1 - 0
packages/core/.eslintignore

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

+ 1 - 0
packages/core/.gitignore

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

+ 27 - 0
packages/core/README.md

@@ -0,0 +1,27 @@
+# growi-commons
+
+[![dependencies status](https://david-dm.org/weseek/growi-commons.svg)](https://david-dm.org/weseek/growi-commons)
+[![devDependencies Status](https://david-dm.org/weseek/growi-commons/dev-status.svg)](https://david-dm.org/weseek/growi-commons?type=dev)
+[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
+
+[GROWI](https://growi.org) Commons Libraries to develop GROWI and plugins
+
+
+Overview
+--------
+
+growi-commons package is includes some functions, classes and modules to develop GROWI substance and GROWI plugins.
+
+Install
+--------
+
+1. install plugin
+
+    ```
+    $ npm install --save growi-commons
+    ```
+
+Documentation
+------------
+
+See https://docs.growi.org/api/commons/

+ 60 - 0
packages/core/jest.config.js

@@ -0,0 +1,60 @@
+// For a detailed explanation regarding each configuration property, visit:
+// https://jestjs.io/docs/en/configuration.html
+
+const MODULE_NAME_MAPPING = {
+  '^~/(.+)$': '<rootDir>/src/$1',
+};
+
+module.exports = {
+
+  preset: 'ts-jest/presets/js-with-ts',
+
+  moduleNameMapper: MODULE_NAME_MAPPING,
+
+  // Automatically clear mock calls and instances between every test
+  clearMocks: true,
+
+  // Indicates whether the coverage information should be collected while executing the test
+  collectCoverage: true,
+
+  // An array of glob patterns indicating a set of files for which coverage information should be collected
+  // collectCoverageFrom: undefined,
+
+  // The directory where Jest should output its coverage files
+  coverageDirectory: 'coverage',
+
+  // An array of regexp pattern strings used to skip coverage collection
+  coveragePathIgnorePatterns: [
+    '/node_modules/',
+  ],
+
+  // An object that configures minimum threshold enforcement for coverage results
+  // TODO: activate -- 2020.03.24 Yuki Takei
+  // coverageThreshold: {
+  //   global: {
+  //     branches: 70,
+  //     functions: 70,
+  //     lines: 70,
+  //     statements: 70,
+  //   },
+  // },
+
+  // An array of file extensions your modules use
+  moduleFileExtensions: [
+    'js',
+    'json',
+    'jsx',
+    'ts',
+    'tsx',
+    'node',
+  ],
+
+  // The test environment that will be used for testing
+  testEnvironment: 'node',
+
+  // The glob patterns Jest uses to detect test files
+  testMatch: [
+    '**/src/**/__tests__/**/*.[jt]s?(x)',
+    '**/src/**/?(*.)+(spec|test).[tj]s?(x)',
+  ],
+};

+ 24 - 0
packages/core/package.json

@@ -0,0 +1,24 @@
+{
+  "name": "@growi/core",
+  "version": "4.3.3-RC",
+  "description": "GROWI Core Libraries",
+  "license": "MIT",
+  "keywords": [
+    "growi"
+  ],
+  "main": "dist/cjs/index.js",
+  "module": "dist/esm/index.js",
+  "files": ["dist"],
+  "scripts": {
+    "build": "run-p build:*",
+    "build:cjs": "tsc -p tsconfig.build.cjs.json && tsc-alias -p tsconfig.build.cjs.json",
+    "build:esm": "tsc -p tsconfig.build.esm.json && tsc-alias -p tsconfig.build.esm.json",
+    "lint:js": "eslint **/*.{js,ts}",
+    "lint:styles": "stylelint src/styles/scss/**/*.scss",
+    "lint": "npm-run-all -p lint:*",
+    "test": "jest --verbose"
+  },
+  "dependencies": {},
+  "devDependencies": {
+  }
+}

+ 21 - 0
packages/core/src/index.js

@@ -0,0 +1,21 @@
+import * as _pathUtils from './utils/path-utils';
+import * as _pagePathUtils from './utils/page-path-utils';
+import * as _templateChecker from './utils/template-checker';
+
+// module.exports = {
+//   BasicInterceptor: require('./utils/basic-interceptor'),
+//   envUtils: require('./utils/env-utils'),
+//   // plugin
+//   customTagUtils: require('./plugin/util/custom-tag-utils'),
+//   TagCacheManager: require('./plugin/service/tag-cache-manager'),
+//   // service
+//   LocalStorageManager: require('./service/localstorage-manager'),
+// };
+
+export * from './plugin/interfaces/plugin-definition-v4';
+export * from './models/devided-page-path';
+
+// export utils
+export const pathUtils = _pathUtils;
+export const pagePathUtils = _pagePathUtils;
+export const templateChecker = _templateChecker;

+ 2 - 4
packages/app/src/models/devided-page-path.js → packages/core/src/models/devided-page-path.js

@@ -1,11 +1,11 @@
-const { pathUtils } = require('growi-commons');
+import * as pathUtils from '../utils/path-utils';
 
 
 // https://regex101.com/r/BahpKX/2
 // https://regex101.com/r/BahpKX/2
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 // https://regex101.com/r/WVpPpY/1
 // https://regex101.com/r/WVpPpY/1
 const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
 const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
 
 
-class DevidedPagePath {
+export class DevidedPagePath {
 
 
   constructor(path, skipNormalize = false, evalDatePath = false) {
   constructor(path, skipNormalize = false, evalDatePath = false) {
 
 
@@ -43,5 +43,3 @@ class DevidedPagePath {
   }
   }
 
 
 }
 }
-
-module.exports = DevidedPagePath;

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

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

+ 14 - 0
packages/core/src/plugin/model/tag-context.js

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

+ 71 - 0
packages/core/src/plugin/service/tag-cache-manager.js

@@ -0,0 +1,71 @@
+const LocalStorageManager = require('../../service/localstorage-manager');
+
+/**
+ * Service Class for caching React state and TagContext
+ */
+class TagCacheManager {
+
+  /**
+   * @callback generateCacheKey
+   * @param {TagContext} tagContext - TagContext instance
+   * @returns {string} Cache key from TagContext
+   *
+   */
+
+  /**
+   * Constructor
+   * @param {string} cacheNs Used as LocalStorageManager namespace
+   * @param {generateCacheKey} generateCacheKey
+   */
+  constructor(cacheNs, generateCacheKey) {
+    if (cacheNs == null) {
+      throw new Error('args \'cacheNs\' is required.');
+    }
+    if (generateCacheKey == null) {
+      throw new Error('args \'generateCacheKey\' is required.');
+    }
+    if (typeof generateCacheKey !== 'function') {
+      throw new Error('args \'generateCacheKey\' should be function.');
+    }
+
+    this.cacheNs = cacheNs;
+    this.generateCacheKey = generateCacheKey;
+  }
+
+  /**
+   * Retrieve state cache object from local storage
+   * @param {TagContext} tagContext
+   * @returns {object} a cache object that correspont to the specified `tagContext`
+   */
+  getStateCache(tagContext) {
+    const localStorageManager = LocalStorageManager.getInstance();
+
+    const key = this.generateCacheKey(tagContext);
+    const stateCache = localStorageManager.retrieveFromSessionStorage(this.cacheNs, key);
+
+    return stateCache;
+  }
+
+  /**
+   * store state object of React Component with specified key
+   *
+   * @param {TagContext} tagContext
+   * @param {object} state state object of React Component
+   */
+  cacheState(tagContext, state) {
+    const localStorageManager = LocalStorageManager.getInstance();
+    const key = this.generateCacheKey(tagContext);
+    localStorageManager.saveToSessionStorage(this.cacheNs, key, state);
+  }
+
+  /**
+   * clear all state caches
+   */
+  clearAllStateCaches() {
+    const localStorageManager = LocalStorageManager.getInstance();
+    localStorageManager.clearAllStateCaches(this.cacheNs);
+  }
+
+}
+
+module.exports = TagCacheManager;

+ 59 - 0
packages/core/src/plugin/util/args-parser.js

@@ -0,0 +1,59 @@
+/**
+ * Arguments parser for custom tag
+ */
+class ArgsParser {
+
+  /**
+   * @typedef ParseArgsResult
+   * @property {string} firstArgsKey - key of the first argument
+   * @property {string} firstArgsValue - value of the first argument
+   * @property {object} options - key of the first argument
+   */
+
+  /**
+   * parse plugin argument strings
+   *
+   * @static
+   * @param {string} str
+   * @returns {ParseArgsResult}
+   */
+  static parse(str) {
+    let firstArgsKey = null;
+    let firstArgsValue = null;
+    const options = {};
+
+    if (str != null && str.length > 0) {
+      const splittedArgs = str.split(',');
+
+      splittedArgs.forEach((rawArg, index) => {
+        const arg = rawArg.trim();
+
+        // parse string like 'key1=value1, key2=value2, ...'
+        // see https://regex101.com/r/pYHcOM/1
+        const match = arg.match(/([^=]+)=?(.+)?/);
+
+        if (match == null) {
+          return;
+        }
+
+        const key = match[1];
+        const value = match[2] || true;
+        options[key] = value;
+
+        if (index === 0) {
+          firstArgsKey = key;
+          firstArgsValue = value;
+        }
+      });
+    }
+
+    return {
+      firstArgsKey,
+      firstArgsValue,
+      options,
+    };
+  }
+
+}
+
+module.exports = ArgsParser;

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

@@ -0,0 +1,88 @@
+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'),
+};

+ 70 - 0
packages/core/src/plugin/util/option-parser.js

@@ -0,0 +1,70 @@
+/**
+ * Options parser for custom tag
+ */
+class OptionParser {
+
+  /**
+   * @typedef ParseRangeResult
+   * @property {number} start - start index
+   * @property {number} end - end index
+   */
+
+  /**
+   * Parse range expression
+   *
+   * <ul>
+   *  <li>ex:</li>
+   *  <ul>
+   *    <li>1:2 -> { start: 1, end: 2 }</li>
+   *    <li>1:  -> { start: 1, end: -1 }</li>
+   *    <li>2+3 -> { start: 1, end: 5 }</li>
+   *  </ul>
+   * </ul>
+   *
+   * @see https://regex101.com/r/w4KCwC/4
+   *
+   * @static
+   * @param {string} str
+   * @returns {ParseRangeResult}
+   */
+  static parseRange(str) {
+    if (str == null) {
+      return null;
+    }
+
+    // see: https://regex101.com/r/w4KCwC/4
+    const match = str.match(/^(-?[0-9]+)(([:+]{1})(-?[0-9]+)?)?$/);
+    if (!match) {
+      return null;
+    }
+
+    // determine start
+    let start;
+    let end;
+
+    // has operator
+    if (match[3] != null) {
+      start = +match[1];
+      const operator = match[3];
+
+      // determine end
+      if (operator === ':') {
+        end = +match[4] || -1; // set last(-1) if undefined
+      }
+      else if (operator === '+') {
+        end = +match[4] || 0; // plus zero if undefined
+        end += start;
+      }
+    }
+    // don't have operator
+    else {
+      start = 1;
+      end = +match[1];
+    }
+
+    return { start, end };
+  }
+
+}
+
+module.exports = OptionParser;

+ 56 - 0
packages/core/src/service/localstorage-manager.js

@@ -0,0 +1,56 @@
+let _instance = null;
+class LocalStorageManager {
+
+  static getInstance() {
+    if (_instance == null) {
+      _instance = new LocalStorageManager();
+    }
+
+    return _instance;
+  }
+
+  /**
+   * retrieve and return parsed JSON object
+   * @param {string} namespace
+   * @param {string} key
+   * @returns {object}
+   */
+  retrieveFromSessionStorage(namespace, key) {
+    const item = JSON.parse(sessionStorage.getItem(namespace)) || {};
+    if (key != null) {
+      return item[key];
+    }
+    return item;
+  }
+
+  /**
+   * save JavaScript object as stringified JSON object
+   *
+   * @param {string} namespace
+   * @param {string | object} cacheObjOrKey cache object or key (if third param is undefined)
+   * @param {object} cacheObj
+   */
+  saveToSessionStorage(namespace, cacheObjOrKey, cacheObj) {
+    let item = JSON.parse(sessionStorage.getItem(namespace)) || {};
+    if (cacheObj !== undefined) {
+      const key = cacheObjOrKey;
+      item[key] = cacheObj;
+    }
+    else {
+      item = cacheObjOrKey;
+    }
+    sessionStorage.setItem(namespace, JSON.stringify(item));
+  }
+
+  /**
+   * clear all state caches
+   *
+   * @param {string} namespace
+   */
+  clearAllStateCaches(namespace) {
+    sessionStorage.removeItem(namespace);
+  }
+
+}
+
+module.exports = LocalStorageManager;

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

@@ -0,0 +1,125 @@
+/* eslint-disable import/first */
+
+// import each from 'jest-each';
+jest.mock('~/service/localstorage-manager');
+
+import LocalStorageManager from '~/service/localstorage-manager';
+import TagCacheManager from '~/plugin/service/tag-cache-manager';
+/* eslint-enable import/first */
+
+describe('TagCacheManager.constructor', () => {
+
+  test('throws Exception when \'cacheNs\' is null', () => {
+    const generateCacheKeyMock = jest.fn();
+
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager(null, generateCacheKeyMock);
+    }).toThrowError(/cacheNs/);
+  });
+
+  test('throws Exception when \'generateCacheKey\' is null', () => {
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager('dummy ns', null);
+    }).toThrowError(/generateCacheKey/);
+  });
+
+  test('throws Exception when \'generateCacheKey\' is not function', () => {
+    expect(() => {
+      // eslint-disable-next-line no-new
+      new TagCacheManager('dummy ns', {});
+    }).toThrowError(/generateCacheKey/);
+  });
+
+  test('set params', () => {
+    const generateCacheKeyMock = jest.fn();
+
+    const instance = new TagCacheManager('dummy ns', generateCacheKeyMock);
+    expect(instance).not.toBeNull();
+    expect(instance.cacheNs).toBe('dummy ns');
+    expect(instance.generateCacheKey).toBe(generateCacheKeyMock);
+  });
+
+});
+
+describe('TagCacheManager', () => {
+
+  let generateCacheKeyMock = null;
+  let localStorageManagerMock = null;
+
+  let tagCacheManager = null;
+
+
+  beforeEach(() => {
+    generateCacheKeyMock = jest.fn();
+    localStorageManagerMock = jest.fn();
+
+    // mock for LocalStorageManager.getInstance
+    LocalStorageManager.getInstance = jest.fn();
+    LocalStorageManager.getInstance.mockReturnValue(localStorageManagerMock);
+
+    tagCacheManager = new TagCacheManager('dummy ns', generateCacheKeyMock);
+  });
+
+  test('.getStateCache', () => {
+    // partial mock
+    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
+
+    // mock for LocalStorageManager
+    const stateCacheMock = jest.fn();
+    localStorageManagerMock.retrieveFromSessionStorage = jest.fn();
+    localStorageManagerMock.retrieveFromSessionStorage
+      .mockReturnValue(stateCacheMock);
+
+    const tagContextMock = jest.fn();
+
+    // when
+    const result = tagCacheManager.getStateCache(tagContextMock);
+    // then
+    expect(result).not.toBeNull();
+    expect(result).toBe(stateCacheMock);
+    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
+    expect(generateCacheKeyMockCalls.length).toBe(1);
+    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
+    const retrieveFromSessionStorageMockCalls = localStorageManagerMock.retrieveFromSessionStorage.mock.calls;
+    expect(retrieveFromSessionStorageMockCalls.length).toBe(1);
+    expect(retrieveFromSessionStorageMockCalls[0][0]).toBe('dummy ns');
+    expect(retrieveFromSessionStorageMockCalls[0][1]).toBe('dummy key');
+  });
+
+  test('.getStateCache with state object', () => {
+    // partial mock
+    tagCacheManager.generateCacheKey = jest.fn().mockReturnValue('dummy key');
+
+    // mock for LocalStorageManager
+    localStorageManagerMock.saveToSessionStorage = jest.fn();
+
+    const tagContextMock = jest.fn();
+    const stateMock = jest.fn();
+
+    // when
+    tagCacheManager.cacheState(tagContextMock, stateMock);
+    // then
+    const generateCacheKeyMockCalls = tagCacheManager.generateCacheKey.mock.calls;
+    expect(generateCacheKeyMockCalls.length).toBe(1);
+    expect(generateCacheKeyMockCalls[0][0]).toBe(tagContextMock);
+    const saveToSessionStorageMockCalls = localStorageManagerMock.saveToSessionStorage.mock.calls;
+    expect(saveToSessionStorageMockCalls.length).toBe(1);
+    expect(saveToSessionStorageMockCalls[0][0]).toBe('dummy ns');
+    expect(saveToSessionStorageMockCalls[0][1]).toBe('dummy key');
+    expect(saveToSessionStorageMockCalls[0][2]).toBe(stateMock);
+  });
+
+  test('.clearAllStateCaches', () => {
+    // mock for LocalStorageManager
+    localStorageManagerMock.clearAllStateCaches = jest.fn();
+
+    // when
+    tagCacheManager.clearAllStateCaches();
+    // then
+    const clearAllStateCachesMockCalls = localStorageManagerMock.clearAllStateCaches.mock.calls;
+    expect(clearAllStateCachesMockCalls.length).toBe(1);
+    expect(clearAllStateCachesMockCalls[0][0]).toBe('dummy ns');
+  });
+});

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

@@ -0,0 +1,42 @@
+import ArgsParser from '~/plugin/util/args-parser';
+
+describe('args-parser', () => {
+
+  test('.parse(null) returns default object', () => {
+    const result = ArgsParser.parse(null);
+
+    expect(result.firstArgsKey).toBeNull();
+    expect(result.firstArgsValue).toBeNull();
+    expect(result.options).toEqual({});
+  });
+
+  test('.parse(\'prefix=/Level1\') returns a valid results', () => {
+    const result = ArgsParser.parse('prefix=/Level1');
+
+    expect(result.firstArgsKey).toBe('prefix');
+    expect(result.firstArgsValue).toBe('/Level1');
+  });
+
+  test('.parse(\'key, opt1=1, opt2=2\') returns a valid results', () => {
+    const result = ArgsParser.parse('key, opt1=1, opt2=2');
+
+    expect(result.firstArgsKey).toBe('key');
+    expect(result.firstArgsValue).toBeTruthy();
+
+    expect(Object.keys(result.options).length).toBe(3);
+    expect(result.options.key).toBeTruthy();
+    expect(result.options.opt1).toBe('1');
+    expect(result.options.opt2).toBe('2');
+  });
+
+  test('.parse(\'key, \') returns a valid results', () => {
+    const result = ArgsParser.parse('key, ');
+
+    expect(result.firstArgsKey).toBe('key');
+    expect(result.firstArgsValue).toBeTruthy();
+
+    expect(Object.keys(result.options).length).toBe(1);
+    expect(result.options.key).toBeTruthy();
+  });
+
+});

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

@@ -0,0 +1,72 @@
+import rewire from 'rewire';
+
+import customTagUtils from '~/plugin/util/custom-tag-utils';
+
+const rewiredCustomTagUtils = rewire('../../../plugin/util/custom-tag-utils');
+
+describe('customTagUtils', () => {
+
+  test('exports TagContext', () => {
+    expect(customTagUtils.TagContext).not.toBeNull();
+    expect(typeof customTagUtils.TagContext).toBe('function');
+  });
+
+  test('exports ArgsParser', () => {
+    expect(customTagUtils.ArgsParser).not.toBeNull();
+    expect(typeof customTagUtils.ArgsParser).toBe('function');
+  });
+
+  test('exports OptionParser', () => {
+    expect(customTagUtils.OptionParser).not.toBeNull();
+    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: '/',
+      },
+    });
+  });
+
+
+});

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

@@ -0,0 +1,32 @@
+import each from 'jest-each';
+
+import OptionParser from '~/plugin/util/option-parser';
+
+describe('option-parser', () => {
+
+  test('.parseRange(null) returns null', () => {
+    expect(OptionParser.parseRange(null)).toBeNull();
+  });
+
+  each`
+    arg
+    ${'aaa'}
+    ${'5++2'}
+    ${'5:+2'}
+  `.test('.parseRange(\'$arg\') returns null', ({ arg }) => {
+    expect(OptionParser.parseRange(arg)).toBeNull();
+  });
+
+  each`
+    arg       | start | end
+    ${'1'}    | ${1} | ${1}
+    ${'2:1'}  | ${2} | ${1}
+    ${'2:'}   | ${2} | ${-1}
+    ${'10:-3'}   | ${10} | ${-3}
+    ${'5+2'}   | ${5} | ${7}
+    ${'5+'}   | ${5} | ${5}
+  `.test('.parseRange(\'$arg\') returns { start: $start, end : $end }', ({ arg, start, end }) => {
+    expect(OptionParser.parseRange(arg)).toEqual({ start, end });
+  });
+
+});

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

@@ -0,0 +1,86 @@
+// eslint-disable-next-line import/no-unresolved
+import 'jest-localstorage-mock';
+
+import LocalStorageManager from '~/service/localstorage-manager';
+
+let localStorageManager = null;
+
+beforeEach(() => {
+  localStorageManager = LocalStorageManager.getInstance();
+
+  // == init jest-localstorage-mock
+  // reset the storage
+  localStorage.clear();
+  sessionStorage.clear();
+  // set preset data
+  sessionStorage.setItem('localstorage-manager-test', JSON.stringify({ foo: 'bar' }));
+  // reset mocks
+  localStorage.setItem.mockClear();
+  sessionStorage.setItem.mockClear();
+});
+
+describe('LocalStorageManager', () => {
+  test('.getInstance() returns the same instance', () => {
+    expect(LocalStorageManager.getInstance()).toBe(localStorageManager);
+  });
+
+  test('.retrieveFromSessionStorage() with unknown namespace returns the empty object', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('unknown namespace');
+    expect(item).toEqual({});
+  });
+
+  test('.retrieveFromSessionStorage() without key returns the preset data', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('localstorage-manager-test');
+    expect(item).toEqual({ foo: 'bar' });
+  });
+
+  test('.retrieveFromSessionStorage() with key returns the preset data', () => {
+    const item = localStorageManager.retrieveFromSessionStorage('localstorage-manager-test', 'foo');
+    expect(item).toBe('bar');
+  });
+
+  test('.saveToSessionStorage() without key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test',
+      { foo: { qux: 'quux' } },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(1);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test',
+        JSON.stringify({ foo: { qux: 'quux' } }),
+      );
+  });
+
+  test('.saveToSessionStorage() with key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test',
+      'baz',
+      { qux: 'quux' },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(1);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test',
+        JSON.stringify({ foo: 'bar', baz: { qux: 'quux' } }),
+      );
+  });
+
+  test('.saveToSessionStorage() with unknown key works fine', () => {
+    localStorageManager.saveToSessionStorage(
+      'localstorage-manager-test-unknown-key',
+      'baz',
+      { qux: 'quux' },
+    );
+
+    expect(sessionStorage.__STORE__.length).toBe(2);
+    expect(sessionStorage.setItem)
+      .toHaveBeenLastCalledWith(
+        'localstorage-manager-test-unknown-key',
+        JSON.stringify({ baz: { qux: 'quux' } }),
+      );
+  });
+
+});

+ 21 - 0
packages/core/src/test/util/env-utils.test.js

@@ -0,0 +1,21 @@
+import envUtils from '~/utils/env-utils';
+
+
+describe('env-utils', () => {
+  describe('.toBoolean', () => {
+
+    test('should convert to true', () => {
+      expect(envUtils.toBoolean('true')).toBe(true);
+      expect(envUtils.toBoolean('True')).toBe(true);
+      expect(envUtils.toBoolean(1)).toBe(true);
+    });
+
+    test('should convert to false', () => {
+      expect(envUtils.toBoolean(undefined)).toBe(false);
+      expect(envUtils.toBoolean(null)).toBe(false);
+      expect(envUtils.toBoolean('false')).toBe(false);
+      expect(envUtils.toBoolean(0)).toBe(false);
+    });
+
+  });
+});

Неке датотеке нису приказане због велике количине промена