@cntrl-site/sdk 1.0.0 → 1.1.1

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/.env.local ADDED
@@ -0,0 +1 @@
1
+ CNTRL_API_URL=https://01GJ2SPNXG3V5P35ZA35YM1JTW:68e1bacad1b3adb6cb8be99ca7167c2b@preview.cntrl.site
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__: 375;
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 first layout's exemplary (mobile-first)
65
+ $__CNTRL_LAYOUT_WIDTH__: 375 !global;
66
+ }
@@ -18,28 +18,50 @@ class Client {
18
18
  throw new Error('API key is missing in the URL.');
19
19
  }
20
20
  }
21
- async getProject() {
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) {
22
31
  try {
23
- const response = await this.fetchProject();
24
- const data = await response.json();
25
- const project = core_1.ProjectSchema.parse(data);
26
- return project;
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
+ };
27
47
  }
28
48
  catch (e) {
29
49
  throw e;
30
50
  }
31
51
  }
32
- async getPageArticle(pageSlug) {
52
+ async getProjectPagesPaths() {
33
53
  try {
34
- const projectResponse = await this.fetchProject();
35
- const data = await projectResponse.json();
36
- const project = core_1.ProjectSchema.parse(data);
37
- const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
38
- const articleResponse = await this.fetchArticle(articleId);
39
- const articleData = await articleResponse.json();
40
- const article = core_1.ArticleSchema.parse(articleData.article);
41
- const keyframes = core_1.KeyframesSchema.parse(articleData.keyframes);
42
- return { article, keyframes };
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,18 +69,7 @@ class Client {
47
69
  }
48
70
  async getTypePresets() {
49
71
  const response = await this.fetchTypePresets();
50
- const data = await response.json();
51
- const typePresets = core_1.TypePresetsSchema.parse(data);
52
- return typePresets;
53
- }
54
- static getPageMeta(projectMeta, pageMeta) {
55
- return pageMeta.enabled ? {
56
- title: pageMeta.title ? pageMeta.title : projectMeta.title,
57
- description: pageMeta.description ? pageMeta.description : projectMeta.description,
58
- keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
59
- opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
60
- favicon: projectMeta.favicon
61
- } : projectMeta;
72
+ return response;
62
73
  }
63
74
  async fetchProject() {
64
75
  const { username: projectId, password: apiKey, origin } = this.url;
@@ -71,7 +82,9 @@ class Client {
71
82
  if (!response.ok) {
72
83
  throw new Error(`Failed to fetch project with id #${projectId}: ${response.statusText}`);
73
84
  }
74
- return response;
85
+ const data = await response.json();
86
+ const project = core_1.ProjectSchema.parse(data);
87
+ return project;
75
88
  }
76
89
  async fetchArticle(articleId) {
77
90
  const { username: projectId, password: apiKey, origin } = this.url;
@@ -84,7 +97,10 @@ class Client {
84
97
  if (!response.ok) {
85
98
  throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
86
99
  }
87
- 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 };
88
104
  }
89
105
  async fetchTypePresets() {
90
106
  const { username: projectId, password: apiKey, origin } = this.url;
@@ -97,7 +113,9 @@ class Client {
97
113
  if (!response.ok) {
98
114
  throw new Error(`Failed to fetch type presets for the project with id #${projectId}: ${response.statusText}`);
99
115
  }
100
- return response;
116
+ const data = await response.json();
117
+ const typePresets = core_1.TypePresetsSchema.parse(data);
118
+ return typePresets;
101
119
  }
102
120
  findArticleIdByPageSlug(slug, pages) {
103
121
  const { username: projectId } = this.url;
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": "1.0.0",
3
+ "version": "1.1.1",
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.0",
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__: <%= ranges[0].exemplary %>;
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 first layout's exemplary (mobile-first)
49
+ $__CNTRL_LAYOUT_WIDTH__: <%= ranges[0].exemplary %> !global;
50
+ }
@@ -2,72 +2,97 @@ 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('returns project', async () => {
10
- const projectId = 'MY_PROJECT_ID';
8
+ it('throws an error when no project ID passed to the connect URL', async () => {
9
+ const projectId = '';
11
10
  const apiKey = 'MY_API_KEY';
12
- let fetchCalledTimes = 0;
13
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 () => {
25
+ const projectId = 'projectId';
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}/`;
14
38
  const fetch = async (url: string) => {
15
39
  fetchCalledTimes += 1;
16
- expect(url).toBe(`https://api.cntrl.site/projects/${projectId}`);
17
40
  return Promise.resolve({
18
41
  ok: true,
19
- json: () => Promise.resolve(projectMock),
42
+ json: () => Promise.resolve(fetchesMap[url]),
20
43
  statusText: ''
21
44
  });
22
45
  };
23
46
  const client = new Client(apiUrl, fetch);
24
- const project = await client.getProject();
25
- expect(fetchCalledTimes).toBe(1);
26
- expect(project).toEqual(projectMock);
27
- });
28
-
29
- it('throws an error upon project fetch failure', async () => {
30
- const projectId = 'MY_PROJECT_ID';
31
- const apiKey = 'MY_API_KEY';
32
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
33
- const fetch = async () => Promise.resolve({
34
- ok: false,
35
- statusText: 'reason',
36
- json: () => Promise.resolve()
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'
37
59
  });
38
- const client = new Client(apiUrl, fetch);
39
- await expect(client.getProject()).rejects.toEqual(new Error('Failed to fetch project with id #MY_PROJECT_ID: reason'));
40
60
  });
41
61
 
42
- it('returns article by page slug', async () => {
43
- let fetchCalledTimes = 0;
44
- const projectId = 'MY_PROJECT_ID';
62
+ it('ignores page meta if it is not enabled and uses project meta instead', async () => {
63
+ const projectId = 'projectId';
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
+ };
45
73
  const apiKey = 'MY_API_KEY';
46
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
47
- const projectApiUrl = `https://api.cntrl.site/projects/${projectId}`;
48
- const articleApiUrl = `https://api.cntrl.site/projects/${projectId}/articles/articleId`;
74
+ let fetchCalledTimes = 0;
75
+ const apiUrl = `https://${projectId}:${apiKey}@${API_BASE_URL}/`;
49
76
  const fetch = async (url: string) => {
50
77
  fetchCalledTimes += 1;
51
- if (fetchCalledTimes === 1) {
52
- expect(url).toBe(projectApiUrl);
53
- }
54
- if (fetchCalledTimes === 2) {
55
- expect(url).toBe(articleApiUrl);
56
- }
57
78
  return Promise.resolve({
58
79
  ok: true,
59
- json: () => Promise.resolve(url === projectApiUrl ? projectMock : { article: articleMock, keyframes: keyframesMock }),
80
+ json: () => Promise.resolve(fetchesMap[url]),
60
81
  statusText: ''
61
82
  });
62
83
  };
63
84
  const client = new Client(apiUrl, fetch);
64
- const { article, keyframes } = await client.getPageArticle('/');
65
- expect(fetchCalledTimes).toBe(2);
66
- expect(article).toEqual(articleMock);
67
- expect(keyframes).toEqual(keyframesMock);
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
+ });
68
93
  });
69
94
 
70
- it('throws an error upon project fetch failure when trying to get article by slug', async () => {
95
+ it('throws an error upon page data fetch failure', async () => {
71
96
  const projectId = 'MY_PROJECT_ID';
72
97
  const apiKey = 'MY_API_KEY';
73
98
  const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
@@ -77,25 +102,7 @@ describe('Client', () => {
77
102
  json: () => Promise.resolve()
78
103
  });
79
104
  const client = new Client(apiUrl, fetch);
80
- await expect(client.getPageArticle('/'))
81
- .rejects.toEqual(new Error(`Failed to fetch project with id #${projectId}: reason`));
82
- });
83
-
84
- it('throws an error upon article fetch failure when trying to get article by slug', async () => {
85
- const projectId = 'MY_PROJECT_ID';
86
- const apiKey = 'MY_API_KEY';
87
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
88
- const projectApiUrl = `https://api.cntrl.site/projects/${projectId}`;
89
- const fetch = (url: string) => {
90
- return Promise.resolve({
91
- ok: url === projectApiUrl,
92
- json: () => Promise.resolve(projectMock),
93
- statusText: 'reason'
94
- });
95
- };
96
- const client = new Client(apiUrl, fetch);
97
- await expect(client.getPageArticle('/'))
98
- .rejects.toEqual(new Error('Failed to fetch article with id #articleId: reason'));
105
+ await expect(client.getPageData('/')).rejects.toEqual(new Error('Failed to fetch project with id #MY_PROJECT_ID: reason'));
99
106
  });
100
107
 
101
108
  it('throws an error when trying to fetch article by nonexistent slug', async () => {
@@ -112,99 +119,7 @@ describe('Client', () => {
112
119
  });
113
120
  };
114
121
  const client = new Client(apiUrl, fetch);
115
- await expect(client.getPageArticle(slug))
122
+ await expect(client.getPageData(slug))
116
123
  .rejects.toEqual(new Error(`Page with a slug ${slug} was not found in project with id #${projectId}`));
117
124
  });
118
-
119
- it('returns type presets by project id', async () => {
120
- let fetchCalledTimes = 0;
121
- const projectId = 'MY_PROJECT_ID';
122
- const apiKey = 'MY_API_KEY';
123
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
124
- const fetch = (url: string) => {
125
- fetchCalledTimes += 1;
126
- expect(url).toBe(`https://api.cntrl.site/projects/${projectId}/type-presets`);
127
- return Promise.resolve({
128
- ok: true,
129
- json: () => Promise.resolve(typePresetsMock),
130
- statusText: ''
131
- });
132
- };
133
- const client = new Client(apiUrl, fetch);
134
- const presets = await client.getTypePresets();
135
- expect(presets).toEqual(typePresetsMock);
136
- expect(fetchCalledTimes).toEqual(1);
137
- });
138
-
139
- it('throws an error upon type presets fetch failure', async () => {
140
- const projectId = 'MY_PROJECT_ID';
141
- const apiKey = 'MY_API_KEY';
142
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
143
- const fetch = () => {
144
- return Promise.resolve({
145
- ok: false,
146
- json: () => Promise.resolve(),
147
- statusText: 'reason'
148
- });
149
- };
150
- const client = new Client(apiUrl, fetch);
151
- await expect(client.getTypePresets()).rejects.toEqual(
152
- new Error(`Failed to fetch type presets for the project with id #${projectId}: reason`)
153
- );
154
- });
155
-
156
- it('merges two meta objects into one with priority of page-based over project-based', () => {
157
- const pageMeta: TPageMeta = {
158
- enabled: true,
159
- description: 'page-desc',
160
- title: 'page-title'
161
- };
162
-
163
- const projectMeta: TMeta = {
164
- opengraphThumbnail: 'proj-og',
165
- description: 'proj-desc',
166
- title: 'proj-title',
167
- keywords: 'project, keywords'
168
- };
169
- const meta = Client.getPageMeta(projectMeta, pageMeta);
170
- expect(meta.keywords).toBe(projectMeta.keywords);
171
- expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
172
- expect(meta.description).toBe(pageMeta.description);
173
- expect(meta.title).toBe(pageMeta.title);
174
- });
175
-
176
- it('ignores page meta when `enabled` is set to `false` and uses only generic project meta', () => {
177
- const pageMeta: TPageMeta = {
178
- enabled: false,
179
- description: 'page-desc',
180
- title: 'page-title'
181
- };
182
-
183
- const projectMeta: TMeta = {
184
- opengraphThumbnail: 'proj-og',
185
- keywords: 'project, keywords'
186
- };
187
-
188
- const meta = Client.getPageMeta(projectMeta, pageMeta);
189
- expect(meta.keywords).toBe(projectMeta.keywords);
190
- expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
191
- expect(meta.description).toBeUndefined();
192
- expect(meta.title).toBeUndefined();
193
- });
194
-
195
- it('throws an error when no project ID passed to the connect URL', async () => {
196
- const projectId = '';
197
- const apiKey = 'MY_API_KEY';
198
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
199
- expect(() => new Client(apiUrl)).toThrow(new Error('Project ID is missing in the URL.'));
200
- expect(() => new Client('https://api.cntrl.site'))
201
- .toThrow(new Error('Project ID is missing in the URL.'));
202
- });
203
-
204
- it('throws an error when no API key passed to the connect URL', async () => {
205
- const projectId = 'whatever';
206
- const apiKey = '';
207
- const apiUrl = `https://${projectId}:${apiKey}@api.cntrl.site/`;
208
- expect(() => new Client(apiUrl)).toThrow(new Error('API key is missing in the URL.'));
209
- });
210
125
  });
@@ -9,7 +9,8 @@ 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';
@@ -29,51 +30,62 @@ export class Client {
29
30
  }
30
31
  }
31
32
 
32
- async getProject(): Promise<TProject> {
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> {
33
44
  try {
34
- const response = await this.fetchProject();
35
- const data = await response.json();
36
- const project = ProjectSchema.parse(data);
37
- return project;
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
+ };
38
60
  } catch (e) {
39
61
  throw e;
40
62
  }
41
63
  }
42
64
 
43
- async getPageArticle(pageSlug: string): Promise<{ article: TArticle, keyframes: TKeyframeAny[] }> {
65
+ async getProjectPagesPaths(): Promise<string[]> {
44
66
  try {
45
- const projectResponse = await this.fetchProject();
46
- const data = await projectResponse.json();
47
- const project = ProjectSchema.parse(data);
48
- const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
49
- const articleResponse = await this.fetchArticle(articleId);
50
- const articleData = await articleResponse.json();
51
- const article = ArticleSchema.parse(articleData.article);
52
- const keyframes = KeyframesSchema.parse(articleData.keyframes);
53
- return { article, keyframes };
67
+ const { pages } = await this.fetchProject();
68
+ return pages.map(p => p.slug);
54
69
  } catch (e) {
55
70
  throw e;
56
71
  }
57
72
  }
58
73
 
59
- async getTypePresets(): Promise<TTypePresets> {
60
- const response = await this.fetchTypePresets();
61
- const data = await response.json();
62
- const typePresets = TypePresetsSchema.parse(data);
63
- return typePresets;
74
+ async getLayouts(): Promise<TLayout[]> {
75
+ try {
76
+ const { layouts } = await this.fetchProject();
77
+ return layouts;
78
+ } catch (e) {
79
+ throw e;
80
+ }
64
81
  }
65
82
 
66
- public static getPageMeta(projectMeta: TMeta, pageMeta: TPageMeta): TMeta {
67
- return pageMeta.enabled ? {
68
- title: pageMeta.title ? pageMeta.title : projectMeta.title,
69
- description: pageMeta.description ? pageMeta.description : projectMeta.description,
70
- keywords: pageMeta.keywords ? pageMeta.keywords : projectMeta.keywords,
71
- opengraphThumbnail: pageMeta.opengraphThumbnail ? pageMeta.opengraphThumbnail : projectMeta.opengraphThumbnail,
72
- favicon: projectMeta.favicon
73
- } : projectMeta;
83
+ async getTypePresets(): Promise<TTypePresets> {
84
+ const response = await this.fetchTypePresets();
85
+ return response;
74
86
  }
75
87
 
76
- private async fetchProject(): Promise<FetchImplResponse> {
88
+ private async fetchProject(): Promise<TProject> {
77
89
  const { username: projectId, password: apiKey, origin } = this.url;
78
90
  const url = new URL(`/projects/${projectId}`, origin);
79
91
  const response = await this.fetchImpl(url.href, {
@@ -84,10 +96,12 @@ export class Client {
84
96
  if (!response.ok) {
85
97
  throw new Error(`Failed to fetch project with id #${projectId}: ${response.statusText}`);
86
98
  }
87
- return response;
99
+ const data = await response.json();
100
+ const project = ProjectSchema.parse(data);
101
+ return project;
88
102
  }
89
103
 
90
- private async fetchArticle(articleId: string): Promise<FetchImplResponse> {
104
+ private async fetchArticle(articleId: string): Promise<ArticleData> {
91
105
  const { username: projectId, password: apiKey, origin } = this.url;
92
106
  const url = new URL(`/projects/${projectId}/articles/${articleId}`, origin);
93
107
  const response = await this.fetchImpl(url.href, {
@@ -98,10 +112,13 @@ export class Client {
98
112
  if (!response.ok) {
99
113
  throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
100
114
  }
101
- return response;
115
+ const data = await response.json();
116
+ const article = ArticleSchema.parse(data.article);
117
+ const keyframes = KeyframesSchema.parse(data.keyframes);
118
+ return { article, keyframes };
102
119
  }
103
120
 
104
- private async fetchTypePresets(): Promise<FetchImplResponse> {
121
+ private async fetchTypePresets(): Promise<TTypePresets> {
105
122
  const { username: projectId, password: apiKey, origin } = this.url;
106
123
  const url = new URL(`/projects/${projectId}/type-presets`, origin);
107
124
  const response = await this.fetchImpl(url.href, {
@@ -114,7 +131,9 @@ export class Client {
114
131
  `Failed to fetch type presets for the project with id #${projectId}: ${response.statusText}`
115
132
  );
116
133
  }
117
- return response;
134
+ const data = await response.json();
135
+ const typePresets = TypePresetsSchema.parse(data);
136
+ return typePresets;
118
137
  }
119
138
 
120
139
  private findArticleIdByPageSlug(slug: string, pages: TPage[]): string {
@@ -134,3 +153,12 @@ interface FetchImplResponse {
134
153
  }
135
154
 
136
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: undefined,
18
- title: undefined,
19
- opengraphThumbnail: undefined,
20
- keywords: undefined,
21
- description: undefined
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-thumbnail',
34
- title: 'page-title',
35
- description: 'page-description',
33
+ opengraphThumbnail: 'page thumbnail',
34
+ title: 'page title',
35
+ description: 'page description',
36
36
  enabled: true,
37
- keywords: 'page-keywords'
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);
Binary file