@cntrl-site/sdk 0.7.2 → 1.1.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/cntrl-site-sdk-1.1.0.tgz +0 -0
- package/cntrl.scss +66 -0
- package/lib/Client/Client.js +79 -59
- package/lib/cli.js +55 -0
- package/package.json +10 -2
- package/resources/template.scss.ejs +50 -0
- package/src/Client/Client.test.ts +78 -175
- package/src/Client/Client.ts +96 -66
- package/src/Client/__mock__/projectMock.ts +24 -10
- package/src/cli.ts +67 -0
- package/cntrl-site-sdk-0.4.4.tgz +0 -0
|
Binary file
|
package/cntrl.scss
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// CAUTION: THIS FILE IS AUTO-GENERATED BASED ON
|
|
2
|
+
// LAYOUT CONFIGURATION IN YOUR CNTRL PROJECT
|
|
3
|
+
// WE HIGHLY ADVICE YOU TO NOT CHANGE IT MANUALLY
|
|
4
|
+
@use "sass:map";
|
|
5
|
+
|
|
6
|
+
$__CNTRL_LAYOUT_WIDTH__: 100;
|
|
7
|
+
|
|
8
|
+
$layout: (
|
|
9
|
+
|
|
10
|
+
mobile: (
|
|
11
|
+
start: 0,
|
|
12
|
+
end: 749,
|
|
13
|
+
exemplary: 375,
|
|
14
|
+
isFirst: true,
|
|
15
|
+
isLast: false
|
|
16
|
+
),
|
|
17
|
+
|
|
18
|
+
tablet: (
|
|
19
|
+
start: 750,
|
|
20
|
+
end: 1023,
|
|
21
|
+
exemplary: 768,
|
|
22
|
+
isFirst: false,
|
|
23
|
+
isLast: false
|
|
24
|
+
),
|
|
25
|
+
|
|
26
|
+
desktop: (
|
|
27
|
+
start: 1024,
|
|
28
|
+
end: 9007199254740991,
|
|
29
|
+
exemplary: 1440,
|
|
30
|
+
isFirst: false,
|
|
31
|
+
isLast: true
|
|
32
|
+
),
|
|
33
|
+
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
@function size($value) {
|
|
37
|
+
@return #{$value/$__CNTRL_LAYOUT_WIDTH__*100}vw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@mixin for($name) {
|
|
41
|
+
$start: map.get(map.get($layout, $name), "start");
|
|
42
|
+
$end: map.get(map.get($layout, $name), "end");
|
|
43
|
+
$isFirst: map.get(map.get($layout, $name), "isFirst");
|
|
44
|
+
$isLast: map.get(map.get($layout, $name), "isLast");
|
|
45
|
+
$exemplary: map.get(map.get($layout, $name), "exemplary");
|
|
46
|
+
$__CNTRL_LAYOUT_WIDTH__: $exemplary !global;
|
|
47
|
+
|
|
48
|
+
@if $isFirst == true and $isLast == true {
|
|
49
|
+
@content;
|
|
50
|
+
} @else if $isFirst == true {
|
|
51
|
+
@media (max-width: #{$end}px) {
|
|
52
|
+
@content;
|
|
53
|
+
}
|
|
54
|
+
} @else if $isLast == true {
|
|
55
|
+
@media (min-width: #{$start}px) {
|
|
56
|
+
@content;
|
|
57
|
+
}
|
|
58
|
+
} @else {
|
|
59
|
+
@media (min-width: #{$start}px) and (max-width: #{$end}px) {
|
|
60
|
+
@content;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// reset global variable back to it's initial state
|
|
65
|
+
$__CNTRL_LAYOUT_WIDTH__: 100 !global;
|
|
66
|
+
}
|
package/lib/Client/Client.js
CHANGED
|
@@ -8,38 +8,60 @@ const core_1 = require("@cntrl-site/core");
|
|
|
8
8
|
const isomorphic_fetch_1 = __importDefault(require("isomorphic-fetch"));
|
|
9
9
|
const url_1 = require("url");
|
|
10
10
|
class Client {
|
|
11
|
-
constructor(
|
|
12
|
-
this.projectId = projectId;
|
|
13
|
-
this.APIUrl = APIUrl;
|
|
11
|
+
constructor(APIUrl, fetchImpl = isomorphic_fetch_1.default) {
|
|
14
12
|
this.fetchImpl = fetchImpl;
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
this.url = new url_1.URL(APIUrl);
|
|
14
|
+
if (!this.url.username) {
|
|
15
|
+
throw new Error('Project ID is missing in the URL.');
|
|
17
16
|
}
|
|
18
|
-
if (
|
|
19
|
-
throw new Error('
|
|
17
|
+
if (!this.url.password) {
|
|
18
|
+
throw new Error('API key is missing in the URL.');
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
|
-
|
|
21
|
+
static getPageMeta(projectMeta, pageMeta) {
|
|
22
|
+
return pageMeta.enabled ? {
|
|
23
|
+
title: pageMeta.title ? pageMeta.title : projectMeta.title,
|
|
24
|
+
description: pageMeta.description ? pageMeta.description : projectMeta.description,
|
|
25
|
+
keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
|
|
26
|
+
opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
|
|
27
|
+
favicon: projectMeta.favicon
|
|
28
|
+
} : projectMeta;
|
|
29
|
+
}
|
|
30
|
+
async getPageData(pageSlug) {
|
|
23
31
|
try {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
32
|
+
const project = await this.fetchProject();
|
|
33
|
+
const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
|
|
34
|
+
const [{ article, keyframes }, typePresets] = await Promise.all([
|
|
35
|
+
this.fetchArticle(articleId),
|
|
36
|
+
this.fetchTypePresets()
|
|
37
|
+
]);
|
|
38
|
+
const page = project.pages.find(page => page.slug === pageSlug);
|
|
39
|
+
const meta = Client.getPageMeta(project.meta, page?.meta);
|
|
40
|
+
return {
|
|
41
|
+
project,
|
|
42
|
+
typePresets,
|
|
43
|
+
article,
|
|
44
|
+
keyframes,
|
|
45
|
+
meta
|
|
46
|
+
};
|
|
28
47
|
}
|
|
29
48
|
catch (e) {
|
|
30
49
|
throw e;
|
|
31
50
|
}
|
|
32
51
|
}
|
|
33
|
-
async
|
|
52
|
+
async getProjectPagesPaths() {
|
|
34
53
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
const { pages } = await this.fetchProject();
|
|
55
|
+
return pages.map(p => p.slug);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async getLayouts() {
|
|
62
|
+
try {
|
|
63
|
+
const { layouts } = await this.fetchProject();
|
|
64
|
+
return layouts;
|
|
43
65
|
}
|
|
44
66
|
catch (e) {
|
|
45
67
|
throw e;
|
|
@@ -47,61 +69,59 @@ class Client {
|
|
|
47
69
|
}
|
|
48
70
|
async getTypePresets() {
|
|
49
71
|
const response = await this.fetchTypePresets();
|
|
50
|
-
|
|
51
|
-
const typePresets = core_1.TypePresetsSchema.parse(data);
|
|
52
|
-
return typePresets;
|
|
53
|
-
}
|
|
54
|
-
async getKeyframes(articleId) {
|
|
55
|
-
const response = await this.fetchKeyframes(articleId);
|
|
56
|
-
const data = await response.json();
|
|
57
|
-
const keyframes = core_1.KeyframesSchema.parse(data);
|
|
58
|
-
return keyframes;
|
|
59
|
-
}
|
|
60
|
-
static getPageMeta(projectMeta, pageMeta) {
|
|
61
|
-
return pageMeta.enabled ? {
|
|
62
|
-
title: pageMeta.title ? pageMeta.title : projectMeta.title,
|
|
63
|
-
description: pageMeta.description ? pageMeta.description : projectMeta.description,
|
|
64
|
-
keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
|
|
65
|
-
opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
|
|
66
|
-
favicon: projectMeta.favicon
|
|
67
|
-
} : projectMeta;
|
|
72
|
+
return response;
|
|
68
73
|
}
|
|
69
74
|
async fetchProject() {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
75
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
76
|
+
const url = new url_1.URL(`/projects/${projectId}`, origin);
|
|
77
|
+
const response = await this.fetchImpl(url.href, {
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${apiKey}`
|
|
80
|
+
}
|
|
81
|
+
});
|
|
72
82
|
if (!response.ok) {
|
|
73
|
-
throw new Error(`Failed to fetch project with id #${
|
|
83
|
+
throw new Error(`Failed to fetch project with id #${projectId}: ${response.statusText}`);
|
|
74
84
|
}
|
|
75
|
-
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
const project = core_1.ProjectSchema.parse(data);
|
|
87
|
+
return project;
|
|
76
88
|
}
|
|
77
89
|
async fetchArticle(articleId) {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
90
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
91
|
+
const url = new url_1.URL(`/projects/${projectId}/articles/${articleId}`, origin);
|
|
92
|
+
const response = await this.fetchImpl(url.href, {
|
|
93
|
+
headers: {
|
|
94
|
+
Authorization: `Bearer ${apiKey}`
|
|
95
|
+
}
|
|
96
|
+
});
|
|
80
97
|
if (!response.ok) {
|
|
81
98
|
throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
|
|
82
99
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const response = await this.fetchImpl(url.href);
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
throw new Error(`Failed to fetch keyframes for the article with id #${articleId}: ${response.statusText}`);
|
|
90
|
-
}
|
|
91
|
-
return response;
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
const article = core_1.ArticleSchema.parse(data.article);
|
|
102
|
+
const keyframes = core_1.KeyframesSchema.parse(data.keyframes);
|
|
103
|
+
return { article, keyframes };
|
|
92
104
|
}
|
|
93
105
|
async fetchTypePresets() {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
106
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
107
|
+
const url = new url_1.URL(`/projects/${projectId}/type-presets`, origin);
|
|
108
|
+
const response = await this.fetchImpl(url.href, {
|
|
109
|
+
headers: {
|
|
110
|
+
Authorization: `Bearer ${apiKey}`
|
|
111
|
+
}
|
|
112
|
+
});
|
|
96
113
|
if (!response.ok) {
|
|
97
|
-
throw new Error(`Failed to fetch type presets for the project with id #${
|
|
114
|
+
throw new Error(`Failed to fetch type presets for the project with id #${projectId}: ${response.statusText}`);
|
|
98
115
|
}
|
|
99
|
-
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
const typePresets = core_1.TypePresetsSchema.parse(data);
|
|
118
|
+
return typePresets;
|
|
100
119
|
}
|
|
101
120
|
findArticleIdByPageSlug(slug, pages) {
|
|
121
|
+
const { username: projectId } = this.url;
|
|
102
122
|
const page = pages.find((page) => page.slug === slug);
|
|
103
123
|
if (!page) {
|
|
104
|
-
throw new Error(`Page with a slug ${slug} was not found in project with id #${
|
|
124
|
+
throw new Error(`Page with a slug ${slug} was not found in project with id #${projectId}`);
|
|
105
125
|
}
|
|
106
126
|
return page.articleId;
|
|
107
127
|
}
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const ejs_1 = __importDefault(require("ejs"));
|
|
10
|
+
const dotenv_1 = require("dotenv");
|
|
11
|
+
const commander_1 = require("commander");
|
|
12
|
+
const Client_1 = require("./Client/Client");
|
|
13
|
+
commander_1.program
|
|
14
|
+
.command('generate-layouts')
|
|
15
|
+
.option('-o, --output <outputFilePath>', 'Output file path', 'cntrl.scss')
|
|
16
|
+
.option('-e, --env <envFilename>', 'Name of the .env file', '.env.local')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
try {
|
|
19
|
+
(0, dotenv_1.config)({ path: options.env });
|
|
20
|
+
const templateFilePath = path_1.default.resolve(__dirname, '../resources/template.scss.ejs');
|
|
21
|
+
const scssTemplate = fs_1.default.readFileSync(templateFilePath, 'utf-8');
|
|
22
|
+
const apiUrl = process.env.CNTRL_API_URL;
|
|
23
|
+
if (!apiUrl) {
|
|
24
|
+
throw new Error('Environment variable "CNTRL_API_URL" must be set.');
|
|
25
|
+
}
|
|
26
|
+
const client = new Client_1.Client(apiUrl);
|
|
27
|
+
const layouts = await client.getLayouts();
|
|
28
|
+
const ranges = convertLayouts(layouts);
|
|
29
|
+
const compiledTemplate = ejs_1.default.compile(scssTemplate);
|
|
30
|
+
const renderedTemplate = compiledTemplate({ ranges });
|
|
31
|
+
const outputFilePath = path_1.default.resolve(process.cwd(), options.output);
|
|
32
|
+
fs_1.default.writeFileSync(outputFilePath, renderedTemplate);
|
|
33
|
+
console.log(`Generated .scss file at ${outputFilePath}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('An error occurred:', error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
function convertLayouts(layouts, maxLayoutWidth = Number.MAX_SAFE_INTEGER) {
|
|
41
|
+
const sorted = layouts.slice().sort((la, lb) => la.startsWith - lb.startsWith);
|
|
42
|
+
const mapped = sorted.map((layout, i, ls) => {
|
|
43
|
+
const next = ls[i + 1];
|
|
44
|
+
return {
|
|
45
|
+
start: layout.startsWith,
|
|
46
|
+
end: next ? next.startsWith - 1 : maxLayoutWidth,
|
|
47
|
+
exemplary: layout.exemplary,
|
|
48
|
+
name: layout.title,
|
|
49
|
+
isFirst: i === 0,
|
|
50
|
+
isLast: !next
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return mapped;
|
|
54
|
+
}
|
|
55
|
+
commander_1.program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntrl-site/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Generic SDK for use in public websites.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"build": "tsc --project tsconfig.build.json",
|
|
11
11
|
"prepublishOnly": "NODE_ENV=production npm run build"
|
|
12
12
|
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"cntrl-sdk": "lib/cli.js"
|
|
15
|
+
},
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
18
|
"url": "git+https://github.com/cntrl-site/sdk.git"
|
|
@@ -24,9 +27,14 @@
|
|
|
24
27
|
"lib": "lib"
|
|
25
28
|
},
|
|
26
29
|
"dependencies": {
|
|
27
|
-
"@cntrl-site/core": "^1.22.
|
|
30
|
+
"@cntrl-site/core": "^1.22.3",
|
|
31
|
+
"@types/ejs": "^3.1.2",
|
|
28
32
|
"@types/isomorphic-fetch": "^0.0.36",
|
|
33
|
+
"commander": "^10.0.1",
|
|
34
|
+
"dotenv": "^16.1.3",
|
|
35
|
+
"ejs": "^3.1.9",
|
|
29
36
|
"isomorphic-fetch": "^3.0.0",
|
|
37
|
+
"ts-node": "^10.9.1",
|
|
30
38
|
"url": "^0.11.0"
|
|
31
39
|
},
|
|
32
40
|
"devDependencies": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// CAUTION: THIS FILE IS AUTO-GENERATED BASED ON
|
|
2
|
+
// LAYOUT CONFIGURATION IN YOUR CNTRL PROJECT
|
|
3
|
+
// WE HIGHLY ADVICE YOU TO NOT CHANGE IT MANUALLY
|
|
4
|
+
@use "sass:map";
|
|
5
|
+
|
|
6
|
+
$__CNTRL_LAYOUT_WIDTH__: 100;
|
|
7
|
+
|
|
8
|
+
$layout: (
|
|
9
|
+
<% ranges.forEach(function(range) { %>
|
|
10
|
+
<%= range.name %>: (
|
|
11
|
+
start: <%= range.start %>,
|
|
12
|
+
end: <%= range.end %>,
|
|
13
|
+
exemplary: <%= range.exemplary %>,
|
|
14
|
+
isFirst: <%= range.isFirst %>,
|
|
15
|
+
isLast: <%= range.isLast %>
|
|
16
|
+
),
|
|
17
|
+
<% }); %>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
@function size($value) {
|
|
21
|
+
@return #{$value/$__CNTRL_LAYOUT_WIDTH__*100}vw;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@mixin for($name) {
|
|
25
|
+
$start: map.get(map.get($layout, $name), "start");
|
|
26
|
+
$end: map.get(map.get($layout, $name), "end");
|
|
27
|
+
$isFirst: map.get(map.get($layout, $name), "isFirst");
|
|
28
|
+
$isLast: map.get(map.get($layout, $name), "isLast");
|
|
29
|
+
$exemplary: map.get(map.get($layout, $name), "exemplary");
|
|
30
|
+
$__CNTRL_LAYOUT_WIDTH__: $exemplary !global;
|
|
31
|
+
|
|
32
|
+
@if $isFirst == true and $isLast == true {
|
|
33
|
+
@content;
|
|
34
|
+
} @else if $isFirst == true {
|
|
35
|
+
@media (max-width: #{$end}px) {
|
|
36
|
+
@content;
|
|
37
|
+
}
|
|
38
|
+
} @else if $isLast == true {
|
|
39
|
+
@media (min-width: #{$start}px) {
|
|
40
|
+
@content;
|
|
41
|
+
}
|
|
42
|
+
} @else {
|
|
43
|
+
@media (min-width: #{$start}px) and (max-width: #{$end}px) {
|
|
44
|
+
@content;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// reset global variable back to it's initial state
|
|
49
|
+
$__CNTRL_LAYOUT_WIDTH__: 100 !global;
|
|
50
|
+
}
|
|
@@ -2,100 +2,114 @@ import { Client } from './Client';
|
|
|
2
2
|
import { projectMock } from './__mock__/projectMock';
|
|
3
3
|
import { articleMock } from './__mock__/articleMock';
|
|
4
4
|
import { typePresetsMock } from './__mock__/typePresetsMock';
|
|
5
|
-
import { TMeta, TPageMeta } from '@cntrl-site/core';
|
|
6
5
|
import { keyframesMock } from './__mock__/keyframesMock';
|
|
7
6
|
|
|
8
7
|
describe('Client', () => {
|
|
9
|
-
it('
|
|
10
|
-
|
|
8
|
+
it('throws an error when no project ID passed to the connect URL', async () => {
|
|
9
|
+
const projectId = '';
|
|
10
|
+
const apiKey = 'MY_API_KEY';
|
|
11
|
+
const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
|
|
12
|
+
expect(() => new Client(apiUrl)).toThrow(new Error('Project ID is missing in the URL.'));
|
|
13
|
+
expect(() => new Client('https://api.cntrl.site'))
|
|
14
|
+
.toThrow(new Error('Project ID is missing in the URL.'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('throws an error when no API key passed to the connect URL', async () => {
|
|
18
|
+
const projectId = 'whatever';
|
|
19
|
+
const apiKey = '';
|
|
20
|
+
const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
|
|
21
|
+
expect(() => new Client(apiUrl)).toThrow(new Error('API key is missing in the URL.'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns page data', async () => {
|
|
11
25
|
const projectId = 'projectId';
|
|
12
|
-
const
|
|
26
|
+
const API_BASE_URL = 'api-test.cntrl.site';
|
|
27
|
+
const fetchesMap = {
|
|
28
|
+
[`https://${API_BASE_URL}/projects/${projectId}`]: projectMock,
|
|
29
|
+
[`https://${API_BASE_URL}/projects/${projectId}/type-presets`]: typePresetsMock,
|
|
30
|
+
[`https://${API_BASE_URL}/projects/${projectId}/articles/articleId`]: {
|
|
31
|
+
article: articleMock,
|
|
32
|
+
keyframes: keyframesMock
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const apiKey = 'MY_API_KEY';
|
|
36
|
+
let fetchCalledTimes = 0;
|
|
37
|
+
const apiUrl = `https://${projectId}:${apiKey}@${API_BASE_URL}/`;
|
|
13
38
|
const fetch = async (url: string) => {
|
|
14
39
|
fetchCalledTimes += 1;
|
|
15
|
-
expect(url).toBe(`${apiUrl}projects/${projectId}`);
|
|
16
40
|
return Promise.resolve({
|
|
17
41
|
ok: true,
|
|
18
|
-
json: () => Promise.resolve(
|
|
42
|
+
json: () => Promise.resolve(fetchesMap[url]),
|
|
19
43
|
statusText: ''
|
|
20
44
|
});
|
|
21
45
|
};
|
|
22
|
-
const client = new Client(
|
|
23
|
-
const
|
|
24
|
-
expect(fetchCalledTimes).toBe(
|
|
25
|
-
expect(project).toEqual(projectMock);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
const client = new Client(apiUrl, fetch);
|
|
47
|
+
const pageData = await client.getPageData('/');
|
|
48
|
+
expect(fetchCalledTimes).toBe(3);
|
|
49
|
+
expect(pageData.project).toEqual(projectMock);
|
|
50
|
+
expect(pageData.article).toEqual(articleMock);
|
|
51
|
+
expect(pageData.typePresets).toEqual(typePresetsMock);
|
|
52
|
+
expect(pageData.keyframes).toEqual(keyframesMock);
|
|
53
|
+
expect(pageData.meta).toEqual({
|
|
54
|
+
description: 'page description',
|
|
55
|
+
favicon: 'project favicon',
|
|
56
|
+
keywords: 'page keywords',
|
|
57
|
+
opengraphThumbnail: 'page thumbnail',
|
|
58
|
+
title: 'page title'
|
|
35
59
|
});
|
|
36
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
37
|
-
await expect(client.getProject()).rejects.toEqual(new Error('Failed to fetch project with id #projectId: reason'));
|
|
38
60
|
});
|
|
39
61
|
|
|
40
|
-
it('
|
|
41
|
-
let fetchCalledTimes = 0;
|
|
62
|
+
it('ignores page meta if it is not enabled and uses project meta instead', async () => {
|
|
42
63
|
const projectId = 'projectId';
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
64
|
+
const API_BASE_URL = 'api-test.cntrl.site';
|
|
65
|
+
const fetchesMap = {
|
|
66
|
+
[`https://${API_BASE_URL}/projects/${projectId}`]: projectMock,
|
|
67
|
+
[`https://${API_BASE_URL}/projects/${projectId}/type-presets`]: typePresetsMock,
|
|
68
|
+
[`https://${API_BASE_URL}/projects/${projectId}/articles/articleId2`]: {
|
|
69
|
+
article: articleMock,
|
|
70
|
+
keyframes: keyframesMock
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const apiKey = 'MY_API_KEY';
|
|
74
|
+
let fetchCalledTimes = 0;
|
|
75
|
+
const apiUrl = `https://${projectId}:${apiKey}@${API_BASE_URL}/`;
|
|
46
76
|
const fetch = async (url: string) => {
|
|
47
77
|
fetchCalledTimes += 1;
|
|
48
|
-
if (fetchCalledTimes === 1) {
|
|
49
|
-
expect(url).toBe(projectApiUrl);
|
|
50
|
-
}
|
|
51
|
-
if (fetchCalledTimes === 2) {
|
|
52
|
-
expect(url).toBe(articleApiUrl);
|
|
53
|
-
}
|
|
54
78
|
return Promise.resolve({
|
|
55
79
|
ok: true,
|
|
56
|
-
json: () => Promise.resolve(url
|
|
80
|
+
json: () => Promise.resolve(fetchesMap[url]),
|
|
57
81
|
statusText: ''
|
|
58
82
|
});
|
|
59
83
|
};
|
|
60
|
-
const client = new Client(
|
|
61
|
-
const
|
|
62
|
-
expect(
|
|
63
|
-
|
|
84
|
+
const client = new Client(apiUrl, fetch);
|
|
85
|
+
const pageData = await client.getPageData('/2');
|
|
86
|
+
expect(pageData.meta).toEqual({
|
|
87
|
+
description: 'project description',
|
|
88
|
+
favicon: 'project favicon',
|
|
89
|
+
keywords: 'project keywords',
|
|
90
|
+
opengraphThumbnail: 'project opengraph',
|
|
91
|
+
title: 'project title'
|
|
92
|
+
});
|
|
64
93
|
});
|
|
65
94
|
|
|
66
|
-
it('throws an error upon
|
|
67
|
-
const projectId = '
|
|
68
|
-
const
|
|
95
|
+
it('throws an error upon page data fetch failure', async () => {
|
|
96
|
+
const projectId = 'MY_PROJECT_ID';
|
|
97
|
+
const apiKey = 'MY_API_KEY';
|
|
98
|
+
const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
|
|
69
99
|
const fetch = async () => Promise.resolve({
|
|
70
100
|
ok: false,
|
|
71
101
|
statusText: 'reason',
|
|
72
102
|
json: () => Promise.resolve()
|
|
73
103
|
});
|
|
74
|
-
const client = new Client(
|
|
75
|
-
await expect(client.
|
|
76
|
-
.rejects.toEqual(new Error('Failed to fetch project with id #projectId: reason'));
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('throws an error upon article fetch failure when trying to get article by slug', async () => {
|
|
80
|
-
const projectId = 'projectId';
|
|
81
|
-
const apiUrl = 'https://api.cntrl.site/';
|
|
82
|
-
const projectApiUrl = `${apiUrl}projects/projectId`;
|
|
83
|
-
const fetch = (url: string) => {
|
|
84
|
-
return Promise.resolve({
|
|
85
|
-
ok: url === projectApiUrl,
|
|
86
|
-
json: () => Promise.resolve(projectMock),
|
|
87
|
-
statusText: 'reason'
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
91
|
-
await expect(client.getPageArticle('/'))
|
|
92
|
-
.rejects.toEqual(new Error('Failed to fetch article with id #articleId: reason'));
|
|
104
|
+
const client = new Client(apiUrl, fetch);
|
|
105
|
+
await expect(client.getPageData('/')).rejects.toEqual(new Error('Failed to fetch project with id #MY_PROJECT_ID: reason'));
|
|
93
106
|
});
|
|
94
107
|
|
|
95
108
|
it('throws an error when trying to fetch article by nonexistent slug', async () => {
|
|
96
|
-
const projectId = '
|
|
97
|
-
const
|
|
98
|
-
const
|
|
109
|
+
const projectId = 'MY_PROJECT_ID';
|
|
110
|
+
const apiKey = 'MY_API_KEY';
|
|
111
|
+
const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
|
|
112
|
+
const projectApiUrl = `https://api.cntrl.site/projects/${projectId}`;
|
|
99
113
|
const slug = '/nonexistent-slug';
|
|
100
114
|
const fetch = (url: string) => {
|
|
101
115
|
return Promise.resolve({
|
|
@@ -104,119 +118,8 @@ describe('Client', () => {
|
|
|
104
118
|
statusText: 'reason'
|
|
105
119
|
});
|
|
106
120
|
};
|
|
107
|
-
const client = new Client(
|
|
108
|
-
await expect(client.
|
|
121
|
+
const client = new Client(apiUrl, fetch);
|
|
122
|
+
await expect(client.getPageData(slug))
|
|
109
123
|
.rejects.toEqual(new Error(`Page with a slug ${slug} was not found in project with id #${projectId}`));
|
|
110
124
|
});
|
|
111
|
-
|
|
112
|
-
it('returns type presets by project id', async () => {
|
|
113
|
-
let fetchCalledTimes = 0;
|
|
114
|
-
const projectId = 'projectId';
|
|
115
|
-
const apiUrl = 'https://api.cntrl.site/';
|
|
116
|
-
const fetch = (url: string) => {
|
|
117
|
-
fetchCalledTimes += 1;
|
|
118
|
-
expect(url).toBe(`${apiUrl}projects/${projectId}/type-presets`);
|
|
119
|
-
return Promise.resolve({
|
|
120
|
-
ok: true,
|
|
121
|
-
json: () => Promise.resolve(typePresetsMock),
|
|
122
|
-
statusText: ''
|
|
123
|
-
});
|
|
124
|
-
};
|
|
125
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
126
|
-
const presets = await client.getTypePresets();
|
|
127
|
-
expect(presets).toEqual(typePresetsMock);
|
|
128
|
-
expect(fetchCalledTimes).toEqual(1);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('throws an error upon type presets fetch failure', async () => {
|
|
132
|
-
const projectId = 'projectId';
|
|
133
|
-
const apiUrl = 'https://api.cntrl.site/';
|
|
134
|
-
const fetch = () => {
|
|
135
|
-
return Promise.resolve({
|
|
136
|
-
ok: false,
|
|
137
|
-
json: () => Promise.resolve(),
|
|
138
|
-
statusText: 'reason'
|
|
139
|
-
});
|
|
140
|
-
};
|
|
141
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
142
|
-
await expect(client.getTypePresets()).rejects.toEqual(
|
|
143
|
-
new Error(`Failed to fetch type presets for the project with id #${projectId}: reason`)
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('returns keyframes by article id', async () => {
|
|
148
|
-
let fetchCalledTimes = 0;
|
|
149
|
-
const projectId = 'projectId';
|
|
150
|
-
const articleId = 'articleId';
|
|
151
|
-
const apiUrl = 'https://api.cntrl.site/';
|
|
152
|
-
const fetch = (url: string) => {
|
|
153
|
-
fetchCalledTimes += 1;
|
|
154
|
-
expect(url).toBe(`${apiUrl}keyframes/${articleId}`);
|
|
155
|
-
return Promise.resolve({
|
|
156
|
-
ok: true,
|
|
157
|
-
json: () => Promise.resolve(keyframesMock),
|
|
158
|
-
statusText: ''
|
|
159
|
-
});
|
|
160
|
-
};
|
|
161
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
162
|
-
const presets = await client.getKeyframes(articleId);
|
|
163
|
-
expect(presets).toEqual(keyframesMock);
|
|
164
|
-
expect(fetchCalledTimes).toEqual(1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('throws an error upon keyframes fetch failure', async () => {
|
|
168
|
-
const projectId = 'projectId';
|
|
169
|
-
const articleId = 'articleId';
|
|
170
|
-
const apiUrl = 'https://api.cntrl.site/';
|
|
171
|
-
const fetch = () => {
|
|
172
|
-
return Promise.resolve({
|
|
173
|
-
ok: false,
|
|
174
|
-
json: () => Promise.resolve(),
|
|
175
|
-
statusText: 'reason'
|
|
176
|
-
});
|
|
177
|
-
};
|
|
178
|
-
const client = new Client(projectId, apiUrl, fetch);
|
|
179
|
-
await expect(client.getKeyframes(articleId)).rejects.toEqual(
|
|
180
|
-
new Error(`Failed to fetch keyframes for the article with id #${articleId}: reason`)
|
|
181
|
-
);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('merges two meta objects into one with priority of page-based over project-based', () => {
|
|
185
|
-
const pageMeta: TPageMeta = {
|
|
186
|
-
enabled: true,
|
|
187
|
-
description: 'page-desc',
|
|
188
|
-
title: 'page-title'
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const projectMeta: TMeta = {
|
|
192
|
-
opengraphThumbnail: 'proj-og',
|
|
193
|
-
description: 'proj-desc',
|
|
194
|
-
title: 'proj-title',
|
|
195
|
-
keywords: 'project, keywords'
|
|
196
|
-
};
|
|
197
|
-
const meta = Client.getPageMeta(projectMeta, pageMeta);
|
|
198
|
-
expect(meta.keywords).toBe(projectMeta.keywords);
|
|
199
|
-
expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
|
|
200
|
-
expect(meta.description).toBe(pageMeta.description);
|
|
201
|
-
expect(meta.title).toBe(pageMeta.title);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('ignores page meta when `enabled` is set to `false` and uses only generic project meta', () => {
|
|
205
|
-
const pageMeta: TPageMeta = {
|
|
206
|
-
enabled: false,
|
|
207
|
-
description: 'page-desc',
|
|
208
|
-
title: 'page-title'
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const projectMeta: TMeta = {
|
|
212
|
-
opengraphThumbnail: 'proj-og',
|
|
213
|
-
keywords: 'project, keywords'
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const meta = Client.getPageMeta(projectMeta, pageMeta);
|
|
217
|
-
expect(meta.keywords).toBe(projectMeta.keywords);
|
|
218
|
-
expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
|
|
219
|
-
expect(meta.description).toBeUndefined();
|
|
220
|
-
expect(meta.title).toBeUndefined();
|
|
221
|
-
});
|
|
222
125
|
});
|
package/src/Client/Client.ts
CHANGED
|
@@ -9,117 +9,138 @@ import {
|
|
|
9
9
|
TypePresetsSchema,
|
|
10
10
|
TPage,
|
|
11
11
|
TKeyframeAny,
|
|
12
|
-
KeyframesSchema
|
|
12
|
+
KeyframesSchema,
|
|
13
|
+
TLayout
|
|
13
14
|
} from '@cntrl-site/core';
|
|
14
15
|
import fetch from 'isomorphic-fetch';
|
|
15
16
|
import { URL } from 'url';
|
|
16
17
|
|
|
17
18
|
export class Client {
|
|
19
|
+
private url: URL;
|
|
18
20
|
constructor(
|
|
19
|
-
|
|
20
|
-
private APIUrl: string,
|
|
21
|
+
APIUrl: string,
|
|
21
22
|
private fetchImpl: FetchImpl = fetch
|
|
22
23
|
) {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
this.url = new URL(APIUrl);
|
|
25
|
+
if (!this.url.username) {
|
|
26
|
+
throw new Error('Project ID is missing in the URL.');
|
|
25
27
|
}
|
|
26
|
-
if (
|
|
27
|
-
throw new Error('
|
|
28
|
+
if (!this.url.password) {
|
|
29
|
+
throw new Error('API key is missing in the URL.');
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
private static getPageMeta(projectMeta: TMeta, pageMeta: TPageMeta): TMeta {
|
|
34
|
+
return pageMeta.enabled ? {
|
|
35
|
+
title: pageMeta.title ? pageMeta.title : projectMeta.title,
|
|
36
|
+
description: pageMeta.description ? pageMeta.description : projectMeta.description,
|
|
37
|
+
keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
|
|
38
|
+
opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
|
|
39
|
+
favicon: projectMeta.favicon
|
|
40
|
+
} : projectMeta;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getPageData(pageSlug: string): Promise<CntrlPageData> {
|
|
32
44
|
try {
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
45
|
+
const project = await this.fetchProject();
|
|
46
|
+
const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
|
|
47
|
+
const [{ article, keyframes }, typePresets] = await Promise.all([
|
|
48
|
+
this.fetchArticle(articleId),
|
|
49
|
+
this.fetchTypePresets()
|
|
50
|
+
]);
|
|
51
|
+
const page = project.pages.find(page => page.slug === pageSlug)!;
|
|
52
|
+
const meta = Client.getPageMeta(project.meta, page?.meta!);
|
|
53
|
+
return {
|
|
54
|
+
project,
|
|
55
|
+
typePresets,
|
|
56
|
+
article,
|
|
57
|
+
keyframes,
|
|
58
|
+
meta
|
|
59
|
+
};
|
|
37
60
|
} catch (e) {
|
|
38
61
|
throw e;
|
|
39
62
|
}
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
async
|
|
65
|
+
async getProjectPagesPaths(): Promise<string[]> {
|
|
43
66
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const project = ProjectSchema.parse(data);
|
|
47
|
-
const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
|
|
48
|
-
const articleResponse = await this.fetchArticle(articleId);
|
|
49
|
-
const articleData = await articleResponse.json();
|
|
50
|
-
const article = ArticleSchema.parse(articleData);
|
|
51
|
-
return article;
|
|
67
|
+
const { pages } = await this.fetchProject();
|
|
68
|
+
return pages.map(p => p.slug);
|
|
52
69
|
} catch (e) {
|
|
53
70
|
throw e;
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
async
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
async getKeyframes(articleId: string): Promise<TKeyframeAny[]> {
|
|
65
|
-
const response = await this.fetchKeyframes(articleId);
|
|
66
|
-
const data = await response.json();
|
|
67
|
-
const keyframes = KeyframesSchema.parse(data);
|
|
68
|
-
return keyframes;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
public static getPageMeta(projectMeta: TMeta, pageMeta: TPageMeta): TMeta {
|
|
72
|
-
return pageMeta.enabled ? {
|
|
73
|
-
title: pageMeta.title ? pageMeta.title : projectMeta.title,
|
|
74
|
-
description: pageMeta.description ? pageMeta.description : projectMeta.description,
|
|
75
|
-
keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
|
|
76
|
-
opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
|
|
77
|
-
favicon: projectMeta.favicon
|
|
78
|
-
} : projectMeta;
|
|
74
|
+
async getLayouts(): Promise<TLayout[]> {
|
|
75
|
+
try {
|
|
76
|
+
const { layouts } = await this.fetchProject();
|
|
77
|
+
return layouts;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
const response = await this.fetchImpl(url.href);
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
throw new Error(`Failed to fetch project with id #${this.projectId}: ${response.statusText}`);
|
|
86
|
-
}
|
|
83
|
+
async getTypePresets(): Promise<TTypePresets> {
|
|
84
|
+
const response = await this.fetchTypePresets();
|
|
87
85
|
return response;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
private async
|
|
91
|
-
const
|
|
92
|
-
const
|
|
88
|
+
private async fetchProject(): Promise<TProject> {
|
|
89
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
90
|
+
const url = new URL(`/projects/${projectId}`, origin);
|
|
91
|
+
const response = await this.fetchImpl(url.href, {
|
|
92
|
+
headers: {
|
|
93
|
+
Authorization: `Bearer ${apiKey}`
|
|
94
|
+
}
|
|
95
|
+
});
|
|
93
96
|
if (!response.ok) {
|
|
94
|
-
throw new Error(`Failed to fetch
|
|
97
|
+
throw new Error(`Failed to fetch project with id #${projectId}: ${response.statusText}`);
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
const project = ProjectSchema.parse(data);
|
|
101
|
+
return project;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
private async
|
|
100
|
-
const
|
|
101
|
-
const
|
|
104
|
+
private async fetchArticle(articleId: string): Promise<ArticleData> {
|
|
105
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
106
|
+
const url = new URL(`/projects/${projectId}/articles/${articleId}`, origin);
|
|
107
|
+
const response = await this.fetchImpl(url.href, {
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${apiKey}`
|
|
110
|
+
}
|
|
111
|
+
});
|
|
102
112
|
if (!response.ok) {
|
|
103
|
-
throw new Error(`Failed to fetch
|
|
113
|
+
throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
|
|
104
114
|
}
|
|
105
|
-
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
const article = ArticleSchema.parse(data.article);
|
|
117
|
+
const keyframes = KeyframesSchema.parse(data.keyframes);
|
|
118
|
+
return { article, keyframes };
|
|
106
119
|
}
|
|
107
120
|
|
|
108
|
-
private async fetchTypePresets(): Promise<
|
|
109
|
-
const
|
|
110
|
-
const
|
|
121
|
+
private async fetchTypePresets(): Promise<TTypePresets> {
|
|
122
|
+
const { username: projectId, password: apiKey, origin } = this.url;
|
|
123
|
+
const url = new URL(`/projects/${projectId}/type-presets`, origin);
|
|
124
|
+
const response = await this.fetchImpl(url.href, {
|
|
125
|
+
headers: {
|
|
126
|
+
Authorization: `Bearer ${apiKey}`
|
|
127
|
+
}
|
|
128
|
+
});
|
|
111
129
|
if (!response.ok) {
|
|
112
130
|
throw new Error(
|
|
113
|
-
`Failed to fetch type presets for the project with id #${
|
|
131
|
+
`Failed to fetch type presets for the project with id #${projectId}: ${response.statusText}`
|
|
114
132
|
);
|
|
115
133
|
}
|
|
116
|
-
|
|
134
|
+
const data = await response.json();
|
|
135
|
+
const typePresets = TypePresetsSchema.parse(data);
|
|
136
|
+
return typePresets;
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
private findArticleIdByPageSlug(slug: string, pages: TPage[]): string {
|
|
140
|
+
const { username: projectId } = this.url;
|
|
120
141
|
const page = pages.find((page) => page.slug === slug);
|
|
121
142
|
if (!page) {
|
|
122
|
-
throw new Error(`Page with a slug ${slug} was not found in project with id #${
|
|
143
|
+
throw new Error(`Page with a slug ${slug} was not found in project with id #${projectId}`);
|
|
123
144
|
}
|
|
124
145
|
return page.articleId;
|
|
125
146
|
}
|
|
@@ -131,4 +152,13 @@ interface FetchImplResponse {
|
|
|
131
152
|
statusText: string;
|
|
132
153
|
}
|
|
133
154
|
|
|
134
|
-
type FetchImpl = (url: string) => Promise<FetchImplResponse>;
|
|
155
|
+
type FetchImpl = (url: string, init?: RequestInit) => Promise<FetchImplResponse>;
|
|
156
|
+
interface ArticleData {
|
|
157
|
+
article: TArticle;
|
|
158
|
+
keyframes: TKeyframeAny[];
|
|
159
|
+
}
|
|
160
|
+
interface CntrlPageData extends ArticleData {
|
|
161
|
+
project: TProject;
|
|
162
|
+
typePresets: TTypePresets;
|
|
163
|
+
meta: TMeta;
|
|
164
|
+
}
|
|
@@ -14,11 +14,11 @@ export const projectMock: TProject = {
|
|
|
14
14
|
head: ''
|
|
15
15
|
},
|
|
16
16
|
meta: {
|
|
17
|
-
favicon:
|
|
18
|
-
title:
|
|
19
|
-
opengraphThumbnail:
|
|
20
|
-
keywords:
|
|
21
|
-
description:
|
|
17
|
+
favicon: 'project favicon',
|
|
18
|
+
title: 'project title',
|
|
19
|
+
opengraphThumbnail: 'project opengraph',
|
|
20
|
+
keywords: 'project keywords',
|
|
21
|
+
description: 'project description'
|
|
22
22
|
},
|
|
23
23
|
grid: {
|
|
24
24
|
color: 'rgba(0, 0, 0, 1)'
|
|
@@ -30,11 +30,25 @@ export const projectMock: TProject = {
|
|
|
30
30
|
slug: '/',
|
|
31
31
|
isPublished: true,
|
|
32
32
|
meta: {
|
|
33
|
-
opengraphThumbnail: 'page
|
|
34
|
-
title: 'page
|
|
35
|
-
description: 'page
|
|
33
|
+
opengraphThumbnail: 'page thumbnail',
|
|
34
|
+
title: 'page title',
|
|
35
|
+
description: 'page description',
|
|
36
36
|
enabled: true,
|
|
37
|
-
keywords: 'page
|
|
37
|
+
keywords: 'page keywords'
|
|
38
38
|
}
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'pageId2',
|
|
42
|
+
title: 'Page 2',
|
|
43
|
+
articleId: 'articleId2',
|
|
44
|
+
slug: '/2',
|
|
45
|
+
isPublished: true,
|
|
46
|
+
meta: {
|
|
47
|
+
opengraphThumbnail: 'page thumbnail',
|
|
48
|
+
title: 'page title',
|
|
49
|
+
description: 'page description',
|
|
50
|
+
enabled: false,
|
|
51
|
+
keywords: 'page keywords'
|
|
52
|
+
}
|
|
53
|
+
}]
|
|
40
54
|
};
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import ejs from 'ejs';
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import { TLayout } from '@cntrl-site/core';
|
|
9
|
+
import { Client } from './Client/Client';
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command('generate-layouts')
|
|
13
|
+
.option('-o, --output <outputFilePath>', 'Output file path', 'cntrl.scss')
|
|
14
|
+
.option('-e, --env <envFilename>', 'Name of the .env file', '.env.local')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
config({ path: options.env });
|
|
18
|
+
const templateFilePath = path.resolve(__dirname, '../resources/template.scss.ejs');
|
|
19
|
+
const scssTemplate = fs.readFileSync(templateFilePath, 'utf-8');
|
|
20
|
+
const apiUrl = process.env.CNTRL_API_URL;
|
|
21
|
+
if (!apiUrl) {
|
|
22
|
+
throw new Error('Environment variable "CNTRL_API_URL" must be set.');
|
|
23
|
+
}
|
|
24
|
+
const client = new Client(apiUrl);
|
|
25
|
+
const layouts = await client.getLayouts();
|
|
26
|
+
const ranges = convertLayouts(layouts);
|
|
27
|
+
|
|
28
|
+
const compiledTemplate = ejs.compile(scssTemplate);
|
|
29
|
+
const renderedTemplate = compiledTemplate({ ranges });
|
|
30
|
+
|
|
31
|
+
const outputFilePath = path.resolve(process.cwd(), options.output);
|
|
32
|
+
fs.writeFileSync(outputFilePath, renderedTemplate);
|
|
33
|
+
|
|
34
|
+
console.log(`Generated .scss file at ${outputFilePath}`);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error('An error occurred:', error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function convertLayouts(layouts: TLayout[], maxLayoutWidth: number = Number.MAX_SAFE_INTEGER): LayoutRange[] {
|
|
42
|
+
const sorted = layouts.slice().sort((la, lb) => la.startsWith - lb.startsWith);
|
|
43
|
+
const mapped = sorted.map<LayoutRange>((layout, i, ls) => {
|
|
44
|
+
const next = ls[i + 1];
|
|
45
|
+
return {
|
|
46
|
+
start: layout.startsWith,
|
|
47
|
+
end: next ? next.startsWith - 1 : maxLayoutWidth,
|
|
48
|
+
exemplary: layout.exemplary,
|
|
49
|
+
name: layout.title,
|
|
50
|
+
isFirst: i === 0,
|
|
51
|
+
isLast: !next
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
return mapped;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LayoutRange {
|
|
58
|
+
/** closed range [start, end] */
|
|
59
|
+
start: number;
|
|
60
|
+
end: number;
|
|
61
|
+
exemplary: number;
|
|
62
|
+
name: string;
|
|
63
|
+
isFirst: boolean;
|
|
64
|
+
isLast: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
program.parse(process.argv);
|
package/cntrl-site-sdk-0.4.4.tgz
DELETED
|
Binary file
|