@strapi/plugin-documentation 4.3.4 → 4.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__tests__/build-component-schema.test.js +8 -8
- package/admin/src/index.js +3 -3
- package/admin/src/pages/PluginPage/index.js +3 -3
- package/admin/src/pages/PluginPage/tests/index.test.js +1 -1
- package/admin/src/pages/SettingsPage/index.js +3 -3
- package/admin/src/pages/SettingsPage/tests/index.test.js +1 -1
- package/admin/src/pages/utils/api.js +1 -1
- package/admin/src/pages/utils/useReactQuery.js +3 -3
- package/admin/src/utils/getTrad.js +1 -1
- package/admin/src/utils/openWithNewTab.js +1 -1
- package/package.json +9 -5
- package/server/bootstrap.js +2 -4
- package/server/controllers/documentation.js +3 -6
- package/server/public/index.html +27 -14
- package/server/public/login.html +131 -121
- package/server/routes/index.js +1 -0
- package/server/services/documentation.js +5 -5
- package/server/services/helpers/build-api-endpoint-path.js +9 -9
- package/server/services/helpers/build-component-schema.js +81 -21
- package/server/services/helpers/utils/clean-schema-attributes.js +50 -27
- package/server/services/helpers/utils/loop-content-type-names.js +1 -0
- package/server/services/helpers/utils/pascal-case.js +1 -1
- package/server/services/helpers/utils/routes.js +2 -2
|
@@ -79,8 +79,8 @@ describe('Build Component Schema', () => {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
const schemaNames = Object.keys(schemas);
|
|
82
|
-
const pluginListResponseValue = schemas
|
|
83
|
-
const apiListResponseValue = schemas
|
|
82
|
+
const pluginListResponseValue = schemas.UsersPermissionsRoleListResponse;
|
|
83
|
+
const apiListResponseValue = schemas.RestaurantListResponse;
|
|
84
84
|
|
|
85
85
|
const expectedShape = {
|
|
86
86
|
type: 'object',
|
|
@@ -144,8 +144,8 @@ describe('Build Component Schema', () => {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const schemaNames = Object.keys(schemas);
|
|
147
|
-
const pluginListResponseValue = schemas
|
|
148
|
-
const apiListResponseValue = schemas
|
|
147
|
+
const pluginListResponseValue = schemas.UsersPermissionsRoleRequest;
|
|
148
|
+
const apiListResponseValue = schemas.RestaurantRequest;
|
|
149
149
|
|
|
150
150
|
const expectedShape = {
|
|
151
151
|
type: 'object',
|
|
@@ -192,8 +192,8 @@ describe('Build Component Schema', () => {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const schemaNames = Object.keys(schemas);
|
|
195
|
-
const pluginListResponseValue = schemas
|
|
196
|
-
const apiListResponseValue = schemas
|
|
195
|
+
const pluginListResponseValue = schemas.UsersPermissionsRoleLocalizationResponse;
|
|
196
|
+
const apiListResponseValue = schemas.RestaurantLocalizationResponse;
|
|
197
197
|
|
|
198
198
|
const expectedShape = {
|
|
199
199
|
type: 'object',
|
|
@@ -236,8 +236,8 @@ describe('Build Component Schema', () => {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
const schemaNames = Object.keys(schemas);
|
|
239
|
-
const pluginListResponseValue = schemas
|
|
240
|
-
const apiListResponseValue = schemas
|
|
239
|
+
const pluginListResponseValue = schemas.UsersPermissionsRoleLocalizationRequest;
|
|
240
|
+
const apiListResponseValue = schemas.RestaurantLocalizationRequest;
|
|
241
241
|
|
|
242
242
|
const expectedShape = {
|
|
243
243
|
type: 'object',
|
package/admin/src/index.js
CHANGED
|
@@ -22,7 +22,7 @@ export default {
|
|
|
22
22
|
defaultMessage: 'Documentation',
|
|
23
23
|
},
|
|
24
24
|
permissions: pluginPermissions.main,
|
|
25
|
-
|
|
25
|
+
async Component() {
|
|
26
26
|
const component = await import(
|
|
27
27
|
/* webpackChunkName: "documentation-page" */ './pages/PluginPage'
|
|
28
28
|
);
|
|
@@ -44,7 +44,7 @@ export default {
|
|
|
44
44
|
},
|
|
45
45
|
id: 'documentation',
|
|
46
46
|
to: `/settings/${pluginId}`,
|
|
47
|
-
|
|
47
|
+
async Component() {
|
|
48
48
|
const component = await import(
|
|
49
49
|
/* webpackChunkName: "documentation-settings" */ './pages/SettingsPage'
|
|
50
50
|
);
|
|
@@ -56,7 +56,7 @@ export default {
|
|
|
56
56
|
},
|
|
57
57
|
async registerTrads({ locales }) {
|
|
58
58
|
const importedTrads = await Promise.all(
|
|
59
|
-
locales.map(locale => {
|
|
59
|
+
locales.map((locale) => {
|
|
60
60
|
return import(
|
|
61
61
|
/* webpackChunkName: "documentation-translation-[request]" */ `./translations/${locale}.json`
|
|
62
62
|
)
|
|
@@ -49,7 +49,7 @@ const PluginPage = () => {
|
|
|
49
49
|
openWithNewTab(`${slash}${data?.prefix}/v${data?.currentVersion}`);
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const handleRegenerateDoc = version => {
|
|
52
|
+
const handleRegenerateDoc = (version) => {
|
|
53
53
|
regenerateDocMutation.mutate({ version, prefix: data?.prefix });
|
|
54
54
|
};
|
|
55
55
|
|
|
@@ -64,7 +64,7 @@ const PluginPage = () => {
|
|
|
64
64
|
setIsConfirmButtonLoading(false);
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const handleClickDelete = version => {
|
|
67
|
+
const handleClickDelete = (version) => {
|
|
68
68
|
setVersionToDelete(version);
|
|
69
69
|
setShowConfirmDelete(!showConfirmDelete);
|
|
70
70
|
};
|
|
@@ -123,7 +123,7 @@ const PluginPage = () => {
|
|
|
123
123
|
<Tbody>
|
|
124
124
|
{data.docVersions
|
|
125
125
|
.sort((a, b) => (a.generatedDate < b.generatedDate ? 1 : -1))
|
|
126
|
-
.map(doc => (
|
|
126
|
+
.map((doc) => (
|
|
127
127
|
<Tr key={doc.version}>
|
|
128
128
|
<Td width="50%">
|
|
129
129
|
<Typography>{doc.version}</Typography>
|
|
@@ -36,7 +36,7 @@ const SettingsPage = () => {
|
|
|
36
36
|
const { submitMutation, data, isLoading } = useReactQuery();
|
|
37
37
|
const [passwordShown, setPasswordShown] = useState(false);
|
|
38
38
|
|
|
39
|
-
const handleUpdateSettingsSubmit = body => {
|
|
39
|
+
const handleUpdateSettingsSubmit = (body) => {
|
|
40
40
|
submitMutation.mutate({
|
|
41
41
|
prefix: data?.prefix,
|
|
42
42
|
body,
|
|
@@ -143,9 +143,9 @@ const SettingsPage = () => {
|
|
|
143
143
|
}
|
|
144
144
|
endAction={
|
|
145
145
|
<FieldActionWrapper
|
|
146
|
-
onClick={e => {
|
|
146
|
+
onClick={(e) => {
|
|
147
147
|
e.stopPropagation();
|
|
148
|
-
setPasswordShown(prev => !prev);
|
|
148
|
+
setPasswordShown((prev) => !prev);
|
|
149
149
|
}}
|
|
150
150
|
label={formatMessage(
|
|
151
151
|
passwordShown
|
|
@@ -5,7 +5,7 @@ const deleteDoc = ({ prefix, version }) => {
|
|
|
5
5
|
return request(`${prefix}/deleteDoc/${version}`, { method: 'DELETE' });
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
const fetchDocumentationVersions = async toggleNotification => {
|
|
8
|
+
const fetchDocumentationVersions = async (toggleNotification) => {
|
|
9
9
|
try {
|
|
10
10
|
const data = await request(`/${pluginId}/getInfos`, { method: 'GET' });
|
|
11
11
|
|
|
@@ -10,7 +10,7 @@ const useReactQuery = () => {
|
|
|
10
10
|
fetchDocumentationVersions(toggleNotification)
|
|
11
11
|
);
|
|
12
12
|
|
|
13
|
-
const handleError = err => {
|
|
13
|
+
const handleError = (err) => {
|
|
14
14
|
toggleNotification({
|
|
15
15
|
type: 'warning',
|
|
16
16
|
message: err.response.payload.message,
|
|
@@ -27,7 +27,7 @@ const useReactQuery = () => {
|
|
|
27
27
|
|
|
28
28
|
const deleteMutation = useMutation(deleteDoc, {
|
|
29
29
|
onSuccess: () => handleSuccess('info', 'notification.delete.success'),
|
|
30
|
-
onError: error => handleError(error),
|
|
30
|
+
onError: (error) => handleError(error),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
const submitMutation = useMutation(updateSettings, {
|
|
@@ -37,7 +37,7 @@ const useReactQuery = () => {
|
|
|
37
37
|
|
|
38
38
|
const regenerateDocMutation = useMutation(regenerateDoc, {
|
|
39
39
|
onSuccess: () => handleSuccess('info', 'notification.generate.success'),
|
|
40
|
-
onError: error => handleError(error),
|
|
40
|
+
onError: (error) => handleError(error),
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
return { data, isLoading, deleteMutation, submitMutation, regenerateDocMutation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/plugin-documentation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0-alpha.0",
|
|
4
4
|
"description": "Create an OpenAPI Document and visualize your API with SWAGGER UI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "echo \"no tests yet\""
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@strapi/helper-plugin": "4.
|
|
28
|
-
"@strapi/utils": "4.
|
|
27
|
+
"@strapi/helper-plugin": "4.4.0-alpha.0",
|
|
28
|
+
"@strapi/utils": "4.4.0-alpha.0",
|
|
29
29
|
"bcryptjs": "2.4.3",
|
|
30
30
|
"cheerio": "^1.0.0-rc.12",
|
|
31
31
|
"fs-extra": "10.0.0",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"react": "^17.0.2",
|
|
37
37
|
"react-copy-to-clipboard": "^5.1.0",
|
|
38
38
|
"react-dom": "^17.0.2",
|
|
39
|
-
"react-intl": "5.
|
|
39
|
+
"react-intl": "5.25.1",
|
|
40
40
|
"react-redux": "7.2.8",
|
|
41
41
|
"react-router": "^5.2.0",
|
|
42
42
|
"react-router-dom": "5.2.0",
|
|
@@ -48,6 +48,10 @@
|
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@strapi/strapi": "^4.0.0"
|
|
50
50
|
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@testing-library/react": "11.2.7",
|
|
53
|
+
"msw": "0.42.3"
|
|
54
|
+
},
|
|
51
55
|
"engines": {
|
|
52
56
|
"node": ">=14.19.1 <=16.x.x",
|
|
53
57
|
"npm": ">=6.0.0"
|
|
@@ -58,5 +62,5 @@
|
|
|
58
62
|
"description": "Create an OpenAPI Document and visualize your API with SWAGGER UI.",
|
|
59
63
|
"kind": "plugin"
|
|
60
64
|
},
|
|
61
|
-
"gitHead": "
|
|
65
|
+
"gitHead": "fc78298ae4f9b247d636beda568734d5f8ed7b3e"
|
|
62
66
|
}
|
package/server/bootstrap.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-unreachable */
|
|
2
|
+
|
|
2
3
|
'use strict';
|
|
3
4
|
|
|
4
5
|
// Add permissions
|
|
@@ -49,8 +50,5 @@ module.exports = async ({ strapi }) => {
|
|
|
49
50
|
pluginStore.set({ key: 'config', value: { restrictedAccess: false } });
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
await strapi
|
|
53
|
-
.plugin('documentation')
|
|
54
|
-
.service('documentation')
|
|
55
|
-
.generateFullDoc();
|
|
53
|
+
await strapi.plugin('documentation').service('documentation').generateFullDoc();
|
|
56
54
|
};
|
|
@@ -43,10 +43,7 @@ module.exports = {
|
|
|
43
43
|
const version =
|
|
44
44
|
major && minor && patch
|
|
45
45
|
? `${major}.${minor}.${patch}`
|
|
46
|
-
: strapi
|
|
47
|
-
.plugin('documentation')
|
|
48
|
-
.service('documentation')
|
|
49
|
-
.getDocumentationVersion();
|
|
46
|
+
: strapi.plugin('documentation').service('documentation').getDocumentationVersion();
|
|
50
47
|
|
|
51
48
|
const openAPISpecsPath = path.join(
|
|
52
49
|
strapi.dirs.app.extensions,
|
|
@@ -177,7 +174,7 @@ module.exports = {
|
|
|
177
174
|
|
|
178
175
|
const service = strapi.service('plugin::documentation.documentation');
|
|
179
176
|
|
|
180
|
-
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
|
|
177
|
+
const documentationVersions = service.getDocumentationVersions().map((el) => el.version);
|
|
181
178
|
|
|
182
179
|
if (_.isEmpty(version)) {
|
|
183
180
|
return ctx.badRequest('Please provide a version.');
|
|
@@ -201,7 +198,7 @@ module.exports = {
|
|
|
201
198
|
|
|
202
199
|
const service = strapi.service('plugin::documentation.documentation');
|
|
203
200
|
|
|
204
|
-
const documentationVersions = service.getDocumentationVersions().map(el => el.version);
|
|
201
|
+
const documentationVersions = service.getDocumentationVersions().map((el) => el.version);
|
|
205
202
|
|
|
206
203
|
if (_.isEmpty(version)) {
|
|
207
204
|
return ctx.badRequest('Please provide a version.');
|
package/server/public/index.html
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
<!-- HTML for static distribution bundle build --><!DOCTYPE html
|
|
2
|
-
|
|
1
|
+
<!-- HTML for static distribution bundle build --><!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
3
5
|
<title>Swagger UI</title>
|
|
4
|
-
<link
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
<link
|
|
7
|
+
rel="stylesheet"
|
|
8
|
+
type="text/css"
|
|
9
|
+
href="<%=backendUrl%>/plugins/documentation/swagger-ui.css"
|
|
10
|
+
/>
|
|
11
|
+
<link
|
|
12
|
+
rel="icon"
|
|
13
|
+
type="image/png"
|
|
14
|
+
href="<%=backendUrl%>/plugins/documentation/favicon-32x32.png"
|
|
15
|
+
sizes="32x32"
|
|
16
|
+
/>
|
|
17
|
+
<link
|
|
18
|
+
rel="icon"
|
|
19
|
+
type="image/png"
|
|
20
|
+
href="<%=backendUrl%>/plugins/documentation/favicon-16x16.png"
|
|
21
|
+
sizes="16x16"
|
|
22
|
+
/>
|
|
7
23
|
<style>
|
|
8
|
-
html
|
|
9
|
-
{
|
|
24
|
+
html {
|
|
10
25
|
box-sizing: border-box;
|
|
11
26
|
overflow: -moz-scrollbars-vertical;
|
|
12
27
|
overflow-y: scroll;
|
|
@@ -14,14 +29,12 @@
|
|
|
14
29
|
|
|
15
30
|
*,
|
|
16
31
|
*:before,
|
|
17
|
-
*:after
|
|
18
|
-
{
|
|
32
|
+
*:after {
|
|
19
33
|
box-sizing: inherit;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
body
|
|
23
|
-
|
|
24
|
-
margin:0;
|
|
36
|
+
body {
|
|
37
|
+
margin: 0;
|
|
25
38
|
background: #fafafa;
|
|
26
39
|
}
|
|
27
40
|
</style>
|
|
@@ -51,7 +64,7 @@
|
|
|
51
64
|
}
|
|
52
65
|
</script>
|
|
53
66
|
|
|
54
|
-
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-bundle.js"
|
|
55
|
-
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-standalone-preset.js"
|
|
67
|
+
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-bundle.js"></script>
|
|
68
|
+
<script src="<%=backendUrl%>/plugins/documentation/swagger-ui-standalone-preset.js"></script>
|
|
56
69
|
</body>
|
|
57
70
|
</html>
|
package/server/public/login.html
CHANGED
|
@@ -1,135 +1,145 @@
|
|
|
1
|
-
<!DOCTYPE html
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Login - Documentation</title>
|
|
5
|
+
<link href="https://fonts.googleapis.com/css?family=Lato:400,700" rel="stylesheet" />
|
|
6
|
+
<style>
|
|
7
|
+
html {
|
|
8
|
+
font-size: 62.5%;
|
|
9
|
+
height: 100%;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
body {
|
|
15
|
+
height: 100%;
|
|
16
|
+
margin: 0;
|
|
17
|
+
background-color: #ffffff;
|
|
18
|
+
font-family: 'Lato';
|
|
19
|
+
font-size: 1.4rem;
|
|
20
|
+
font-weight: 400;
|
|
21
|
+
text-rendering: optimizeLegibility;
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
-moz-osx-font-smoothing: grayscale;
|
|
24
|
+
}
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
.login {
|
|
27
|
+
height: 100%;
|
|
28
|
+
background-color: #f6f9fc;
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
.login .login-form {
|
|
32
|
+
height: calc(100% - 70px);
|
|
33
|
+
padding: 68px 0 0;
|
|
34
|
+
text-align: center;
|
|
35
|
+
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
37
|
+
.login .login-form form {
|
|
38
|
+
position: relative;
|
|
39
|
+
max-width: 460px;
|
|
40
|
+
padding: 26px 30px;
|
|
41
|
+
margin: 55px auto 0;
|
|
42
|
+
background-color: #ffffff;
|
|
43
|
+
border-radius: 3px;
|
|
44
|
+
box-shadow: 0px 2px 4px rgba(91, 107, 174, 0.15);
|
|
45
|
+
text-align: center;
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
.login .login-form form:before {
|
|
49
|
+
position: absolute;
|
|
50
|
+
content: '';
|
|
51
|
+
top: 0px;
|
|
52
|
+
left: 0;
|
|
53
|
+
display: inline-block;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 2px;
|
|
56
|
+
background-color: #2b66cc;
|
|
57
|
+
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
.login .login-form form .error {
|
|
60
|
+
display: block;
|
|
61
|
+
color: #ff4e00;
|
|
62
|
+
padding-bottom: 20px;
|
|
63
|
+
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
.login .login-form .sub-title {
|
|
66
|
+
margin-top: 35px;
|
|
67
|
+
font-size: 1.6rem;
|
|
68
|
+
font-weight: 400;
|
|
69
|
+
}
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
.login .login-form .logo {
|
|
72
|
+
max-height: 40px;
|
|
73
|
+
}
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
.login .login-form form label {
|
|
76
|
+
display: block;
|
|
77
|
+
margin-bottom: 18px;
|
|
78
|
+
width: 100%;
|
|
79
|
+
text-align: left;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
.login .login-form form input {
|
|
84
|
+
outline: none;
|
|
85
|
+
width: calc(100% - 30px);
|
|
86
|
+
height: 36px;
|
|
87
|
+
padding: 0 15px;
|
|
88
|
+
border: 1px solid #ececec;
|
|
89
|
+
border-radius: 2px;
|
|
90
|
+
margin-bottom: 20px;
|
|
91
|
+
line-height: 36px;
|
|
92
|
+
text-align: left;
|
|
93
|
+
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
.login .login-form form input[type='submit'] {
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
display: inline-block;
|
|
98
|
+
width: auto;
|
|
99
|
+
margin: 12px auto 0;
|
|
100
|
+
padding: 0 75px;
|
|
101
|
+
background: transparent;
|
|
102
|
+
border-radius: 36px;
|
|
103
|
+
border: 1px solid #2b66cc;
|
|
104
|
+
color: #2b66cc;
|
|
105
|
+
text-transform: uppercase;
|
|
106
|
+
font-size: 1.4rem;
|
|
107
|
+
font-weight: 700;
|
|
108
|
+
transition: all 0.2s ease-out;
|
|
109
|
+
}
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
.login .login-form form input[type='submit']:hover {
|
|
112
|
+
background: #2b66cc;
|
|
113
|
+
color: #ffffff;
|
|
114
|
+
}
|
|
115
|
+
</style>
|
|
116
|
+
</head>
|
|
117
|
+
<body>
|
|
118
|
+
<div class="login">
|
|
119
|
+
<section class="login-form">
|
|
120
|
+
<div class="container">
|
|
121
|
+
<div class="row">
|
|
122
|
+
<div class="col-lg-6 col-lg-offset-3 col-md-12">
|
|
123
|
+
<img
|
|
124
|
+
alt="Strapi logo"
|
|
125
|
+
class="logo"
|
|
126
|
+
src="https://strapi.io/assets/images/logo_login.png"
|
|
127
|
+
/>
|
|
128
|
+
<h2 class="sub-title">Enter the password to access the documentation.</h2>
|
|
129
|
+
<form method="post" action="<%=actionUrl%>">
|
|
130
|
+
<span class="error">Wrong password...</span>
|
|
131
|
+
<label>Password</label>
|
|
132
|
+
<input
|
|
133
|
+
type="password"
|
|
134
|
+
name="password"
|
|
135
|
+
placeholder="•••••••••"
|
|
136
|
+
/>
|
|
137
|
+
<input type="submit" value="Login" />
|
|
138
|
+
</form>
|
|
130
139
|
</div>
|
|
131
140
|
</div>
|
|
132
|
-
</
|
|
133
|
-
</
|
|
134
|
-
|
|
135
|
-
</body
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
</div>
|
|
144
|
+
</body>
|
|
145
|
+
</html>
|
package/server/routes/index.js
CHANGED
|
@@ -38,7 +38,7 @@ module.exports = ({ strapi }) => {
|
|
|
38
38
|
getDocumentationVersions() {
|
|
39
39
|
return fs
|
|
40
40
|
.readdirSync(this.getFullDocumentationPath())
|
|
41
|
-
.map(version => {
|
|
41
|
+
.map((version) => {
|
|
42
42
|
try {
|
|
43
43
|
const doc = JSON.parse(
|
|
44
44
|
fs.readFileSync(
|
|
@@ -52,7 +52,7 @@ module.exports = ({ strapi }) => {
|
|
|
52
52
|
return null;
|
|
53
53
|
}
|
|
54
54
|
})
|
|
55
|
-
.filter(x => x);
|
|
55
|
+
.filter((x) => x);
|
|
56
56
|
},
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -99,7 +99,7 @@ module.exports = ({ strapi }) => {
|
|
|
99
99
|
|
|
100
100
|
getPluginAndApiInfo() {
|
|
101
101
|
const plugins = _.get(config, 'x-strapi-config.plugins');
|
|
102
|
-
const pluginsToDocument = plugins.map(plugin => {
|
|
102
|
+
const pluginsToDocument = plugins.map((plugin) => {
|
|
103
103
|
return {
|
|
104
104
|
name: plugin,
|
|
105
105
|
getter: 'plugin',
|
|
@@ -107,7 +107,7 @@ module.exports = ({ strapi }) => {
|
|
|
107
107
|
};
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
const apisToDocument = Object.keys(strapi.api).map(api => {
|
|
110
|
+
const apisToDocument = Object.keys(strapi.api).map((api) => {
|
|
111
111
|
return {
|
|
112
112
|
name: api,
|
|
113
113
|
getter: 'api',
|
|
@@ -186,7 +186,7 @@ module.exports = ({ strapi }) => {
|
|
|
186
186
|
|
|
187
187
|
const finalDoc = { ...config, paths };
|
|
188
188
|
|
|
189
|
-
registeredDocs.forEach(doc => {
|
|
189
|
+
registeredDocs.forEach((doc) => {
|
|
190
190
|
// Add tags
|
|
191
191
|
finalDoc.tags = finalDoc.tags || [];
|
|
192
192
|
finalDoc.tags.push(...(doc.tags || []));
|
|
@@ -15,12 +15,12 @@ const { hasFindMethod, isLocalizedPath } = require('./utils/routes');
|
|
|
15
15
|
* @param {string} routePath - The route's path property
|
|
16
16
|
* @returns {string}
|
|
17
17
|
*/
|
|
18
|
-
const parsePathWithVariables = routePath => {
|
|
18
|
+
const parsePathWithVariables = (routePath) => {
|
|
19
19
|
return pathToRegexp
|
|
20
20
|
.parse(routePath)
|
|
21
|
-
.map(token => {
|
|
21
|
+
.map((token) => {
|
|
22
22
|
if (_.isObject(token)) {
|
|
23
|
-
return token.prefix
|
|
23
|
+
return `${token.prefix}{${token.name}}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return token;
|
|
@@ -35,11 +35,11 @@ const parsePathWithVariables = routePath => {
|
|
|
35
35
|
*
|
|
36
36
|
* @returns {object } Swagger path params object
|
|
37
37
|
*/
|
|
38
|
-
const getPathParams = routePath => {
|
|
38
|
+
const getPathParams = (routePath) => {
|
|
39
39
|
return pathToRegexp
|
|
40
40
|
.parse(routePath)
|
|
41
|
-
.filter(token => _.isObject(token))
|
|
42
|
-
.map(param => {
|
|
41
|
+
.filter((token) => _.isObject(token))
|
|
42
|
+
.map((param) => {
|
|
43
43
|
return {
|
|
44
44
|
name: param.name,
|
|
45
45
|
in: 'path',
|
|
@@ -83,7 +83,7 @@ const getPathWithPrefix = (prefix, route) => {
|
|
|
83
83
|
*/
|
|
84
84
|
const getPaths = ({ routeInfo, uniqueName, contentTypeInfo }) => {
|
|
85
85
|
// Get the routes for the current content type
|
|
86
|
-
const contentTypeRoutes = routeInfo.routes.filter(route => {
|
|
86
|
+
const contentTypeRoutes = routeInfo.routes.filter((route) => {
|
|
87
87
|
return (
|
|
88
88
|
route.path.includes(contentTypeInfo.pluralName) ||
|
|
89
89
|
route.path.includes(contentTypeInfo.singularName)
|
|
@@ -152,7 +152,7 @@ const getPaths = ({ routeInfo, uniqueName, contentTypeInfo }) => {
|
|
|
152
152
|
*
|
|
153
153
|
* @returns {object} Open API paths
|
|
154
154
|
*/
|
|
155
|
-
const getAllPathsForContentType = apiInfo => {
|
|
155
|
+
const getAllPathsForContentType = (apiInfo) => {
|
|
156
156
|
let paths = {};
|
|
157
157
|
|
|
158
158
|
const pathsObject = getPaths(apiInfo);
|
|
@@ -175,7 +175,7 @@ const getAllPathsForContentType = apiInfo => {
|
|
|
175
175
|
*
|
|
176
176
|
* @returns {object}
|
|
177
177
|
*/
|
|
178
|
-
const buildApiEndpointPath = api => {
|
|
178
|
+
const buildApiEndpointPath = (api) => {
|
|
179
179
|
// A reusable loop for building paths and component schemas
|
|
180
180
|
// Uses the api param to build a new set of params for each content type
|
|
181
181
|
// Passes these new params to the function provided
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
|
|
2
3
|
const _ = require('lodash');
|
|
3
4
|
|
|
4
5
|
const cleanSchemaAttributes = require('./utils/clean-schema-attributes');
|
|
@@ -19,10 +20,24 @@ const { hasFindMethod, isLocalizedPath } = require('./utils/routes');
|
|
|
19
20
|
const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
|
|
20
21
|
// Store response and request schemas in an object
|
|
21
22
|
let schemas = {};
|
|
23
|
+
let componentSchemas = {};
|
|
24
|
+
// adds a ComponentSchema to the Schemas so it can be used as Ref
|
|
25
|
+
const addComponentSchema = (schemaName, schema) => {
|
|
26
|
+
if (!Object.keys(schema) || !Object.keys(schema.properties)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
componentSchemas = {
|
|
30
|
+
...componentSchemas,
|
|
31
|
+
[schemaName]: schema,
|
|
32
|
+
};
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
22
35
|
// Get all the route methods
|
|
23
|
-
const routeMethods = routeInfo.routes.map(route => route.method);
|
|
36
|
+
const routeMethods = routeInfo.routes.map((route) => route.method);
|
|
24
37
|
// Check for localized paths
|
|
25
|
-
const hasLocalizationPath = routeInfo.routes.filter(route =>
|
|
38
|
+
const hasLocalizationPath = routeInfo.routes.filter((route) =>
|
|
39
|
+
isLocalizedPath(route.path)
|
|
40
|
+
).length;
|
|
26
41
|
// When the route methods contain any post or put requests
|
|
27
42
|
if (routeMethods.includes('POST') || routeMethods.includes('PUT')) {
|
|
28
43
|
const attributesToOmit = [
|
|
@@ -53,7 +68,10 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
|
|
|
53
68
|
[`${pascalCase(uniqueName)}LocalizationRequest`]: {
|
|
54
69
|
required: [...requiredAttributes, 'locale'],
|
|
55
70
|
type: 'object',
|
|
56
|
-
properties: cleanSchemaAttributes(attributesForRequest, {
|
|
71
|
+
properties: cleanSchemaAttributes(attributesForRequest, {
|
|
72
|
+
isRequest: true,
|
|
73
|
+
addComponentSchema,
|
|
74
|
+
}),
|
|
57
75
|
},
|
|
58
76
|
};
|
|
59
77
|
}
|
|
@@ -68,7 +86,10 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
|
|
|
68
86
|
data: {
|
|
69
87
|
required: requiredAttributes,
|
|
70
88
|
type: 'object',
|
|
71
|
-
properties: cleanSchemaAttributes(attributesForRequest, {
|
|
89
|
+
properties: cleanSchemaAttributes(attributesForRequest, {
|
|
90
|
+
isRequest: true,
|
|
91
|
+
addComponentSchema,
|
|
92
|
+
}),
|
|
72
93
|
},
|
|
73
94
|
},
|
|
74
95
|
},
|
|
@@ -82,29 +103,49 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
|
|
|
82
103
|
type: 'object',
|
|
83
104
|
properties: {
|
|
84
105
|
id: { type: 'string' },
|
|
85
|
-
...cleanSchemaAttributes(attributes),
|
|
106
|
+
...cleanSchemaAttributes(attributes, { addComponentSchema }),
|
|
86
107
|
},
|
|
87
108
|
},
|
|
88
109
|
};
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
// Check for routes that need to return a list
|
|
92
|
-
const hasListOfEntities = routeInfo.routes.filter(route => hasFindMethod(route.handler)).length;
|
|
113
|
+
const hasListOfEntities = routeInfo.routes.filter((route) => hasFindMethod(route.handler)).length;
|
|
93
114
|
if (hasListOfEntities) {
|
|
94
115
|
// Build the list response schema
|
|
95
116
|
schemas = {
|
|
96
117
|
...schemas,
|
|
97
|
-
[`${pascalCase(uniqueName)}
|
|
118
|
+
[`${pascalCase(uniqueName)}ListResponseDataItem`]: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
id: { type: 'string' },
|
|
122
|
+
attributes: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: cleanSchemaAttributes(attributes, {
|
|
125
|
+
addComponentSchema,
|
|
126
|
+
componentSchemaRefName: `#/components/schemas/${pascalCase(
|
|
127
|
+
uniqueName
|
|
128
|
+
)}ListResponseDataItemLocalized`,
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
[`${pascalCase(uniqueName)}ListResponseDataItemLocalized`]: {
|
|
98
134
|
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
id: { type: 'string' },
|
|
137
|
+
attributes: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: cleanSchemaAttributes(attributes, { addComponentSchema }),
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
[`${pascalCase(uniqueName)}ListResponse`]: {
|
|
99
144
|
properties: {
|
|
100
145
|
data: {
|
|
101
146
|
type: 'array',
|
|
102
147
|
items: {
|
|
103
|
-
|
|
104
|
-
properties: {
|
|
105
|
-
id: { type: 'string' },
|
|
106
|
-
attributes: { type: 'object', properties: cleanSchemaAttributes(attributes) },
|
|
107
|
-
},
|
|
148
|
+
$ref: `#/components/schemas/${pascalCase(uniqueName)}ListResponseDataItem`,
|
|
108
149
|
},
|
|
109
150
|
},
|
|
110
151
|
meta: {
|
|
@@ -128,25 +169,44 @@ const getAllSchemasForContentType = ({ routeInfo, attributes, uniqueName }) => {
|
|
|
128
169
|
// Build the response schema
|
|
129
170
|
schemas = {
|
|
130
171
|
...schemas,
|
|
131
|
-
[`${pascalCase(uniqueName)}
|
|
172
|
+
[`${pascalCase(uniqueName)}ResponseDataObject`]: {
|
|
132
173
|
type: 'object',
|
|
133
174
|
properties: {
|
|
134
|
-
|
|
175
|
+
id: { type: 'string' },
|
|
176
|
+
attributes: {
|
|
135
177
|
type: 'object',
|
|
136
|
-
properties: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
178
|
+
properties: cleanSchemaAttributes(attributes, {
|
|
179
|
+
addComponentSchema,
|
|
180
|
+
componentSchemaRefName: `#/components/schemas/${pascalCase(
|
|
181
|
+
uniqueName
|
|
182
|
+
)}ResponseDataObjectLocalized`,
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
[`${pascalCase(uniqueName)}ResponseDataObjectLocalized`]: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
id: { type: 'string' },
|
|
191
|
+
attributes: {
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: cleanSchemaAttributes(attributes, { addComponentSchema }),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
[`${pascalCase(uniqueName)}Response`]: {
|
|
198
|
+
properties: {
|
|
199
|
+
data: {
|
|
200
|
+
$ref: `#/components/schemas/${pascalCase(uniqueName)}ResponseDataObject`,
|
|
140
201
|
},
|
|
141
202
|
meta: { type: 'object' },
|
|
142
203
|
},
|
|
143
204
|
},
|
|
144
205
|
};
|
|
145
|
-
|
|
146
|
-
return schemas;
|
|
206
|
+
return { ...schemas, ...componentSchemas };
|
|
147
207
|
};
|
|
148
208
|
|
|
149
|
-
const buildComponentSchema = api => {
|
|
209
|
+
const buildComponentSchema = (api) => {
|
|
150
210
|
// A reusable loop for building paths and component schemas
|
|
151
211
|
// Uses the api param to build a new set of params for each content type
|
|
152
212
|
// Passes these new params to the function provided
|
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
const getSchemaData = require('./get-schema-data');
|
|
5
|
-
|
|
5
|
+
const pascalCase = require('./pascal-case');
|
|
6
6
|
/**
|
|
7
7
|
* @description - Converts types found on attributes to OpenAPI acceptable data types
|
|
8
8
|
*
|
|
9
9
|
* @param {object} attributes - The attributes found on a contentType
|
|
10
|
-
* @param {{ typeMap: Map, isRequest: boolean }} opts
|
|
10
|
+
* @param {{ typeMap: Map, isRequest: boolean, addComponentSchema: function, componentSchemaRefName: string }} opts
|
|
11
11
|
* @returns Attributes using OpenAPI acceptable data types
|
|
12
12
|
*/
|
|
13
|
-
const cleanSchemaAttributes = (
|
|
13
|
+
const cleanSchemaAttributes = (
|
|
14
|
+
attributes,
|
|
15
|
+
{
|
|
16
|
+
typeMap = new Map(),
|
|
17
|
+
isRequest = false,
|
|
18
|
+
addComponentSchema = () => {},
|
|
19
|
+
componentSchemaRefName = '',
|
|
20
|
+
} = {}
|
|
21
|
+
) => {
|
|
14
22
|
const attributesCopy = _.cloneDeep(attributes);
|
|
15
23
|
|
|
16
|
-
for (const prop
|
|
24
|
+
for (const prop of Object.keys(attributesCopy)) {
|
|
17
25
|
const attribute = attributesCopy[prop];
|
|
18
26
|
if (attribute.default) {
|
|
19
27
|
delete attributesCopy[prop].default;
|
|
@@ -86,43 +94,53 @@ const cleanSchemaAttributes = (attributes, { typeMap = new Map(), isRequest = fa
|
|
|
86
94
|
}
|
|
87
95
|
case 'component': {
|
|
88
96
|
const componentAttributes = strapi.components[attribute.component].attributes;
|
|
89
|
-
|
|
97
|
+
const rawComponentSchema = {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
...(isRequest ? {} : { id: { type: 'string' } }),
|
|
101
|
+
...cleanSchemaAttributes(componentAttributes, {
|
|
102
|
+
typeMap,
|
|
103
|
+
isRequest,
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const refComponentSchema = {
|
|
108
|
+
$ref: `#/components/schemas/${pascalCase(attribute.component)}Component`,
|
|
109
|
+
};
|
|
110
|
+
const componentExists = addComponentSchema(
|
|
111
|
+
`${pascalCase(attribute.component)}Component`,
|
|
112
|
+
rawComponentSchema
|
|
113
|
+
);
|
|
114
|
+
const finalComponentSchema = componentExists ? refComponentSchema : rawComponentSchema;
|
|
90
115
|
if (attribute.repeatable) {
|
|
91
116
|
attributesCopy[prop] = {
|
|
92
117
|
type: 'array',
|
|
93
|
-
items:
|
|
94
|
-
type: 'object',
|
|
95
|
-
properties: {
|
|
96
|
-
...(isRequest ? {} : { id: { type: 'string' } }),
|
|
97
|
-
...cleanSchemaAttributes(componentAttributes, { typeMap, isRequest }),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
118
|
+
items: finalComponentSchema,
|
|
100
119
|
};
|
|
101
120
|
} else {
|
|
102
|
-
attributesCopy[prop] =
|
|
103
|
-
type: 'object',
|
|
104
|
-
properties: {
|
|
105
|
-
...(isRequest ? {} : { id: { type: 'string' } }),
|
|
106
|
-
...cleanSchemaAttributes(componentAttributes, {
|
|
107
|
-
typeMap,
|
|
108
|
-
isRequest,
|
|
109
|
-
}),
|
|
110
|
-
},
|
|
111
|
-
};
|
|
121
|
+
attributesCopy[prop] = finalComponentSchema;
|
|
112
122
|
}
|
|
113
123
|
break;
|
|
114
124
|
}
|
|
115
125
|
case 'dynamiczone': {
|
|
116
|
-
const components = attribute.components.map(component => {
|
|
126
|
+
const components = attribute.components.map((component) => {
|
|
117
127
|
const componentAttributes = strapi.components[component].attributes;
|
|
118
|
-
|
|
128
|
+
const rawComponentSchema = {
|
|
119
129
|
type: 'object',
|
|
120
130
|
properties: {
|
|
121
131
|
...(isRequest ? {} : { id: { type: 'string' } }),
|
|
122
132
|
__component: { type: 'string' },
|
|
123
|
-
...cleanSchemaAttributes(componentAttributes, {
|
|
133
|
+
...cleanSchemaAttributes(componentAttributes, {
|
|
134
|
+
typeMap,
|
|
135
|
+
isRequest,
|
|
136
|
+
addComponentSchema,
|
|
137
|
+
}),
|
|
124
138
|
},
|
|
125
139
|
};
|
|
140
|
+
const refComponentSchema = { $ref: `#/components/schemas/${pascalCase(component)}` };
|
|
141
|
+
const componentExists = addComponentSchema(pascalCase(component), rawComponentSchema);
|
|
142
|
+
const finalComponentSchema = componentExists ? refComponentSchema : rawComponentSchema;
|
|
143
|
+
return finalComponentSchema;
|
|
126
144
|
});
|
|
127
145
|
|
|
128
146
|
attributesCopy[prop] = {
|
|
@@ -171,8 +189,13 @@ const cleanSchemaAttributes = (attributes, { typeMap = new Map(), isRequest = fa
|
|
|
171
189
|
|
|
172
190
|
if (prop === 'localizations') {
|
|
173
191
|
attributesCopy[prop] = {
|
|
174
|
-
type: '
|
|
175
|
-
|
|
192
|
+
type: 'object',
|
|
193
|
+
properties: {
|
|
194
|
+
data: {
|
|
195
|
+
type: 'array',
|
|
196
|
+
items: componentSchemaRefName.length ? { $ref: componentSchemaRefName } : {},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
176
199
|
};
|
|
177
200
|
break;
|
|
178
201
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const hasFindMethod = handler => handler.split('.').pop() === 'find';
|
|
3
|
+
const hasFindMethod = (handler) => handler.split('.').pop() === 'find';
|
|
4
4
|
|
|
5
|
-
const isLocalizedPath = routePath => routePath.includes('localizations');
|
|
5
|
+
const isLocalizedPath = (routePath) => routePath.includes('localizations');
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
isLocalizedPath,
|