@squiz/component-cli-lib 1.2.1-alpha.99 → 1.2.3

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.
Files changed (37) hide show
  1. package/.gitlab-ci.yml +42 -27
  2. package/CHANGELOG.md +88 -0
  3. package/jest.config.ts +3 -0
  4. package/jest.integration.config.ts +4 -0
  5. package/lib/component-dev-folder-structures.spec.d.ts +1 -0
  6. package/lib/component-dev-folder-structures.spec.js +58 -0
  7. package/lib/component-dev-folder-structures.spec.js.map +1 -0
  8. package/lib/component-dev.d.ts +4 -1
  9. package/lib/component-dev.js +24 -18
  10. package/lib/component-dev.js.map +1 -1
  11. package/lib/component-dev.spec.js +28 -65
  12. package/lib/component-dev.spec.js.map +1 -1
  13. package/lib/integration-tests/__components__/big-package/manifest.json +5 -2
  14. package/lib/integration-tests/__components__/cmp-static-file-test/manifest.json +8 -5
  15. package/lib/integration-tests/__components__/invalid-manifest/manifest.json +5 -2
  16. package/lib/integration-tests/helper.d.ts +6 -0
  17. package/lib/integration-tests/helper.js +31 -6
  18. package/lib/integration-tests/helper.js.map +1 -1
  19. package/lib/integration-tests/service-deployment.spec.js +36 -1
  20. package/lib/integration-tests/service-deployment.spec.js.map +1 -1
  21. package/lib/integration-tests/upload-and-render-component.spec.js +38 -17
  22. package/lib/integration-tests/upload-and-render-component.spec.js.map +1 -1
  23. package/lib/upload-component-folder.d.ts +1 -1
  24. package/lib/upload-component-folder.js +10 -6
  25. package/lib/upload-component-folder.js.map +1 -1
  26. package/package.json +10 -10
  27. package/src/component-dev-folder-structures.spec.ts +69 -0
  28. package/src/component-dev.spec.ts +31 -83
  29. package/src/component-dev.ts +40 -18
  30. package/src/integration-tests/__components__/big-package/manifest.json +5 -2
  31. package/src/integration-tests/__components__/cmp-static-file-test/manifest.json +8 -6
  32. package/src/integration-tests/__components__/invalid-manifest/manifest.json +5 -2
  33. package/src/integration-tests/helper.ts +28 -1
  34. package/src/integration-tests/service-deployment.spec.ts +51 -2
  35. package/src/integration-tests/upload-and-render-component.spec.ts +78 -17
  36. package/src/upload-component-folder.ts +16 -7
  37. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,11 @@
1
- import { startRenderStack, stopRenderStack } from '@squiz/render-runtime-lib';
2
- import { getLogger } from '@squiz/dx-logger-lib';
1
+ import {
2
+ ComponentPreviewService,
3
+ ComponentRunnerServiceWithWorkers,
4
+ setupRenderRuntimeServer,
5
+ } from '@squiz/render-runtime-lib';
6
+ import { getLogger, LoggerOptions } from '@squiz/dx-logger-lib';
3
7
  import path from 'path';
8
+ import { ComponentFunctionService, ComponentSetServiceForLocalDev, ManifestServiceForDev } from '@squiz/component-lib';
4
9
 
5
10
  /**
6
11
  * startDevelopmentRender starts a dev-mode render stack for any
@@ -11,24 +16,41 @@ import path from 'path';
11
16
  * @param {object} options - Additional configuration for the dev stack
12
17
  * @returns a function to stop the render stack
13
18
  */
14
- export async function startDevelopmentRender(componentPath: string, options: { port: number; previewFile?: string }) {
15
- const logger = getLogger({ name: 'component-dev', format: 'human' });
16
- await startRenderStack({
17
- logger: logger,
18
- webserver: {
19
- port: options.port,
20
- rootUrl: `http://localhost:${options.port}`,
21
- shouldRunMigrations: false,
19
+ export function startDevelopmentRender(
20
+ componentPath: string,
21
+ options: { port: number; previewFile?: string; loggingFormat?: LoggerOptions['format'] },
22
+ ) {
23
+ const logger = getLogger({ name: 'component-dev', format: options.loggingFormat || 'human' });
24
+ const rootUrl = `http://localhost:${options.port}`;
25
+ const dataMountPoint = path.resolve(process.cwd(), componentPath);
26
+
27
+ const componentRunnerService = new ComponentRunnerServiceWithWorkers(
28
+ {
29
+ dataMountPoint,
30
+ shouldCacheResponses: false,
31
+ workerTimeout: 5_000,
22
32
  },
23
- componentRunner: {
24
- // Considering the component path will be passed in from CLI
25
- // We need to generate the path from the calling directory
26
- dataMountPoint: path.resolve(process.cwd(), componentPath),
27
- localDevMode: true,
28
- previewFile: options.previewFile,
29
- workerTimeout: 20_000,
33
+ logger,
34
+ );
35
+ const webServer = setupRenderRuntimeServer(
36
+ {
37
+ logger,
38
+ componentRunnerService,
39
+ componentSetService: new ComponentSetServiceForLocalDev(logger),
40
+ componentFunctionService: new ComponentFunctionService(rootUrl),
41
+ componentPreviewService: new ComponentPreviewService(options.previewFile),
42
+ manifestService: new ManifestServiceForDev(dataMountPoint, logger),
30
43
  },
44
+ { rootUrl },
45
+ );
46
+
47
+ const server = webServer.listen(options.port, () => {
48
+ logger.info(`Component development webserver started on port ${options.port}`);
49
+ });
50
+
51
+ server.on('close', async () => {
52
+ await componentRunnerService.stop();
31
53
  });
32
54
 
33
- return () => stopRenderStack();
55
+ return server;
34
56
  }
@@ -3,7 +3,10 @@
3
3
 
4
4
  "name": "big-package",
5
5
  "version": "1.0.2",
6
- "main-function": "render-json",
6
+ "mainFunction": "render-json",
7
+ "namespace": "smoke-test-components",
8
+ "displayName": "some-displayName",
9
+ "description": "some-description",
7
10
  "functions": [
8
11
  {
9
12
  "name": "render-json",
@@ -18,7 +21,7 @@
18
21
  "required": ["something"]
19
22
  },
20
23
  "output": {
21
- "response-type": "json",
24
+ "responseType": "json",
22
25
  "definition": {
23
26
  "properties": {
24
27
  "my-prop": {
@@ -3,8 +3,10 @@
3
3
 
4
4
  "name": "cmp-static-file-test",
5
5
  "version": "1.0.0",
6
- "main-function": "main",
7
-
6
+ "mainFunction": "main",
7
+ "namespace": "smoke-test-components",
8
+ "displayName": "some-displayName",
9
+ "description": "some-description",
8
10
  "functions": [
9
11
  {
10
12
  "entry": "main.js",
@@ -19,9 +21,9 @@
19
21
  "required": ["something"]
20
22
  },
21
23
  "output": {
22
- "response-type": "html",
24
+ "responseType": "html",
23
25
 
24
- "static-files": [
26
+ "staticFiles": [
25
27
  {
26
28
  "location": "header",
27
29
  "file": {
@@ -34,7 +36,7 @@
34
36
  }
35
37
  ],
36
38
 
37
- "static-files": {
38
- "location-root": "public"
39
+ "staticFiles": {
40
+ "locationRoot": "public"
39
41
  }
40
42
  }
@@ -3,7 +3,10 @@
3
3
 
4
4
  "name": "invalid-manifes@t",
5
5
  "version": "1.0.0",
6
- "main-function": "main",
6
+ "mainFunction": "main",
7
+ "namespace": "smoke-test-components",
8
+ "displayName": "some-displayName",
9
+ "description": "some-description",
7
10
 
8
11
  "functions": [
9
12
  {
@@ -19,7 +22,7 @@
19
22
  "required": ["something"]
20
23
  },
21
24
  "output": {
22
- "response-type": "html"
25
+ "responseType": "html"
23
26
  }
24
27
  }
25
28
  ]
@@ -4,7 +4,8 @@ import path from 'path';
4
4
 
5
5
  import fsp from 'fs/promises';
6
6
  import { randomBytes } from 'crypto';
7
- import { parseEnvVarForVar } from '@squiz/component-lib';
7
+ import { ComponentSetWebModel } from '@squiz/component-lib';
8
+ import { parseEnvVarForVar } from '@squiz/dx-common-lib';
8
9
  import { config } from 'dotenv';
9
10
 
10
11
  config();
@@ -12,6 +13,7 @@ config();
12
13
  interface Config {
13
14
  managementServiceUrl: string;
14
15
  renderServiceUrl: string;
16
+ contentServiceUrl: string;
15
17
  ci_buildVersion: string;
16
18
  ci_buildBranch: string;
17
19
  }
@@ -19,6 +21,7 @@ interface Config {
19
21
  const configObj: Config = {
20
22
  managementServiceUrl: parseEnvVarForVar('COMPONENT_MANAGEMENT_SERVICE_URL').replace(/\/+$/, ''),
21
23
  renderServiceUrl: parseEnvVarForVar('COMPONENT_RENDER_SERVICE_URL').replace(/\/+$/, ''),
24
+ contentServiceUrl: parseEnvVarForVar('CONTENT_API_URL').replace(/\/+$/, ''),
22
25
  ci_buildVersion: parseEnvVarForVar('CI_COMMIT_SHORT_SHA'),
23
26
  ci_buildBranch: parseEnvVarForVar('CI_COMMIT_REF_NAME'),
24
27
  };
@@ -26,6 +29,10 @@ const configObj: Config = {
26
29
  export default configObj;
27
30
 
28
31
  export const managementService = axios.create({
32
+ baseURL: configObj.managementServiceUrl + '/v1',
33
+ });
34
+
35
+ export const managementServiceRoot = axios.create({
29
36
  baseURL: configObj.managementServiceUrl,
30
37
  });
31
38
 
@@ -33,6 +40,10 @@ export const renderService = axios.create({
33
40
  baseURL: configObj.renderServiceUrl,
34
41
  });
35
42
 
43
+ export const contentService = axios.create({
44
+ baseURL: configObj.contentServiceUrl,
45
+ });
46
+
36
47
  export const ci_buildVersion = configObj.ci_buildVersion;
37
48
  export const ci_buildBranch = configObj.ci_buildBranch;
38
49
 
@@ -56,3 +67,19 @@ export async function createFile(filePath: string, sizeInMB: number) {
56
67
  export function removeFile(filePath: string) {
57
68
  fsp.unlink(filePath);
58
69
  }
70
+
71
+ export async function deleteComponentSet(webPath: string) {
72
+ try {
73
+ await managementService.delete(`/component-set/${webPath}`);
74
+ } catch (error) {
75
+ // no ops
76
+ }
77
+ }
78
+
79
+ export async function addComponentSet(componentSet: ComponentSetWebModel) {
80
+ try {
81
+ await managementService.post(`/component-set`, componentSet);
82
+ } catch (error) {
83
+ //no ops
84
+ }
85
+ }
@@ -1,4 +1,11 @@
1
- import { renderService, managementService, ci_buildVersion, ci_buildBranch } from './helper';
1
+ import {
2
+ renderService,
3
+ managementService,
4
+ contentService,
5
+ managementServiceRoot,
6
+ ci_buildVersion,
7
+ ci_buildBranch,
8
+ } from './helper';
2
9
 
3
10
  interface HealthInfo {
4
11
  status: string;
@@ -8,14 +15,56 @@ interface HealthInfo {
8
15
 
9
16
  describe('Verify latest services deployments', () => {
10
17
  it('Should have latest Management API service', async () => {
11
- const response: HealthInfo = (await managementService.get('/health')).data;
18
+ const response: HealthInfo = (await managementServiceRoot.get('/health')).data;
12
19
  expect(response.buildVersion).toBe(ci_buildVersion);
13
20
  expect(response.buildBranch).toBe(ci_buildBranch);
14
21
  });
15
22
 
23
+ it('Should return 200 for Management API docs', async () => {
24
+ const req = await managementService.get('/docs');
25
+ expect(req.status).toBe(200);
26
+ expect(req.headers['content-type']).toEqual('text/html; charset=utf-8');
27
+ });
28
+
29
+ it('Should return 200 for Management API docs.json', async () => {
30
+ const req = await managementService.get('/docs.json');
31
+ expect(req.status).toBe(200);
32
+ expect(req.headers['content-type']).toEqual('application/json; charset=UTF-8');
33
+ });
34
+
16
35
  it('Should have latest Render Runtime service', async () => {
17
36
  const response: HealthInfo = (await renderService.get('/health')).data;
18
37
  expect(response.buildVersion).toBe(ci_buildVersion);
19
38
  expect(response.buildBranch).toBe(ci_buildBranch);
20
39
  });
40
+
41
+ it('Should return 200 for Render Runtime API docs', async () => {
42
+ const req = await renderService.get('/docs');
43
+ expect(req.status).toBe(200);
44
+ expect(req.headers['content-type']).toEqual('text/html; charset=utf-8');
45
+ });
46
+
47
+ it('Should return 200 for Render Runtime API docs.json', async () => {
48
+ const req = await renderService.get('/docs.json');
49
+ expect(req.status).toBe(200);
50
+ expect(req.headers['content-type']).toEqual('application/json; charset=UTF-8');
51
+ });
52
+
53
+ it('Should have latest Content API service', async () => {
54
+ const response: HealthInfo = (await contentService.get('/health')).data;
55
+ expect(response.buildVersion).toBe(ci_buildVersion);
56
+ expect(response.buildBranch).toBe(ci_buildBranch);
57
+ });
58
+
59
+ it('Should return 200 for Content API docs', async () => {
60
+ const req = await contentService.get('/docs');
61
+ expect(req.status).toBe(200);
62
+ expect(req.headers['content-type']).toEqual('text/html; charset=utf-8');
63
+ });
64
+
65
+ it('Should return 200 for Content API docs.json', async () => {
66
+ const req = await contentService.get('/docs.json');
67
+ expect(req.status).toBe(200);
68
+ expect(req.headers['content-type']).toEqual('application/json; charset=UTF-8');
69
+ });
21
70
  });
@@ -1,24 +1,46 @@
1
1
  import { uploadComponentFolder } from '../index';
2
- import configObj, { renderService, managementService, getTestComponents, createFile, removeFile } from './helper';
2
+ import configObj, {
3
+ renderService,
4
+ managementService,
5
+ getTestComponents,
6
+ createFile,
7
+ removeFile,
8
+ addComponentSet,
9
+ deleteComponentSet,
10
+ } from './helper';
3
11
  import color from 'cli-color';
4
12
  import path from 'path';
5
13
  import supertest from 'supertest';
6
14
  import { logger } from '../upload-component-folder';
15
+ import { ComponentSetWebModel } from '@squiz/component-lib';
16
+ import fsp from 'fs/promises';
7
17
 
8
18
  const mockConsoleError = jest.fn();
9
19
  const mockConsoleLog = jest.fn();
10
20
 
11
21
  const orgConsoleError = console.error;
22
+ const webPath = 'set';
23
+ const testFilesDir = path.resolve(__dirname, 'test-files');
24
+
25
+ beforeAll(async () => {
26
+ await fsp.rm(testFilesDir, { force: true, recursive: true });
27
+ await fsp.mkdir(testFilesDir);
28
+ });
12
29
 
13
30
  afterAll(async () => {
14
31
  // clean up the component added by the test
32
+ await deleteComponentSet(webPath);
33
+
15
34
  for (const componentName of getTestComponents()) {
16
35
  try {
17
- await managementService.delete(`/component/${componentName}`);
36
+ await managementService.delete(`/component/smoke-test-components/${componentName}`);
18
37
  } catch {
19
38
  // no op
20
39
  }
21
40
  }
41
+
42
+ // clean up the test componnet files
43
+ await fsp.rm(testFilesDir, { force: true, recursive: true });
22
44
  });
23
45
 
24
46
  describe('Test isolated test cases', () => {
@@ -30,16 +52,26 @@ describe('Test isolated test cases', () => {
30
52
  it('Should fail uploading a component without manifest.json', async () => {
31
53
  mockConsoleError.mockClear();
32
54
  const componentPath = path.join(__dirname, '/__components__/invalid-upload');
33
- await uploadComponentFolder(componentPath, configObj.managementServiceUrl, configObj.renderServiceUrl);
55
+ await uploadComponentFolder(
56
+ componentPath,
57
+ configObj.managementServiceUrl + '/v1',
58
+ configObj.renderServiceUrl,
59
+ testFilesDir,
60
+ );
34
61
  expect(mockConsoleError.mock.calls[0][0]).toEqual(color.red('manifest could not be found'));
35
62
  });
36
63
 
37
64
  it('Should fail uploading a component that has invalid manifest.json', async () => {
38
65
  mockConsoleError.mockClear();
39
66
  const componentPath = path.join(__dirname, '/__components__/invalid-manifest');
40
- await uploadComponentFolder(componentPath, configObj.managementServiceUrl, configObj.renderServiceUrl);
67
+ await uploadComponentFolder(
68
+ componentPath,
69
+ configObj.managementServiceUrl + '/v1',
70
+ configObj.renderServiceUrl,
71
+ testFilesDir,
72
+ );
41
73
  expect(mockConsoleError.mock.calls[0][0]).toEqual(
42
- color.red('/name: pattern must match pattern "^[a-zA-Z0-9_\\-]+$"'),
74
+ color.red('failed validation: /name pattern must match pattern "^[a-zA-Z0-9_\\-]+$"'),
43
75
  );
44
76
  });
45
77
 
@@ -48,7 +80,12 @@ describe('Test isolated test cases', () => {
48
80
  const componentPath = path.join(__dirname, '/__components__/big-package');
49
81
  const filePath = `${componentPath}/105mb-file`;
50
82
  await createFile(filePath, 105); // Higher limit has been used because compression reduces the size if you use closer to 100MB
51
- await uploadComponentFolder(componentPath, configObj.managementServiceUrl, configObj.renderServiceUrl);
83
+ await uploadComponentFolder(
84
+ componentPath,
85
+ configObj.managementServiceUrl + '/v1',
86
+ configObj.renderServiceUrl,
87
+ testFilesDir,
88
+ );
52
89
  expect(mockConsoleError.mock.calls[0][0]).toEqual(
53
90
  color.red(['File size exceeds the maximum limit of 100MB'].join('')),
54
91
  );
@@ -61,6 +98,7 @@ describe('Deploy basic component having a static file', () => {
61
98
  const componentPath = path.join(__dirname, '/__components__/cmp-static-file-test');
62
99
 
63
100
  beforeAll(async () => {
101
+ await deleteComponentSet(webPath);
64
102
  for (const componentName of getTestComponents()) {
65
103
  try {
66
104
  await managementService.delete(`/component/${componentName}`);
@@ -80,33 +118,56 @@ describe('Deploy basic component having a static file', () => {
80
118
  });
81
119
 
82
120
  it('Should upload the component and return a valid url to preview', async () => {
83
- mockConsoleLog.mockClear();
84
- await uploadComponentFolder(componentPath, configObj.managementServiceUrl, configObj.renderServiceUrl);
85
-
86
- const url = /uploaded location: (.*)/.exec(mockConsoleLog.mock.lastCall)?.[1] || '';
87
- const uploadedComponent = '<a href="/r/set/cmp-static-file-test/1.0.0">1.0.0</a>';
88
- const get = await supertest(url).get('/');
121
+ await uploadComponentFolder(
122
+ componentPath,
123
+ configObj.managementServiceUrl + '/v1',
124
+ configObj.renderServiceUrl,
125
+ testFilesDir,
126
+ );
89
127
 
128
+ const uploadedComponent = '<a href="/r/set/smoke-test-components/cmp-static-file-test/1.0.0">1.0.0</a>';
129
+ const get = await supertest(configObj.renderServiceUrl).get('/');
90
130
  expect(get.status).toEqual(200);
91
131
  expect((get as any)?.res?.text).toContain(uploadedComponent);
92
132
  });
93
133
 
94
134
  it('Should fail upload the component with same version', async () => {
95
135
  mockConsoleError.mockClear();
96
- await uploadComponentFolder(componentPath, configObj.managementServiceUrl, configObj.renderServiceUrl);
136
+ await uploadComponentFolder(
137
+ componentPath,
138
+ configObj.managementServiceUrl + '/v1',
139
+ configObj.renderServiceUrl,
140
+ testFilesDir,
141
+ );
97
142
  expect(mockConsoleError.mock.calls[0][0]).toEqual(
98
- color.red('Cannot upload component version, cmp-static-file-test 1.0.0 already exists'),
143
+ color.red('Cannot upload component version, smoke-test-components/cmp-static-file-test 1.0.0 already exists'),
99
144
  );
100
145
  });
101
146
 
102
147
  it('Should render component', async () => {
103
- const response = await renderService.get('/r/set/cmp-static-file-test/1.0.0/?something=hello');
148
+ const componentSet: ComponentSetWebModel = {
149
+ webPath,
150
+ displayName: 'Set',
151
+ description: 'Set description',
152
+ headers: {},
153
+ environmentVariables: {},
154
+ components: {
155
+ 'smoke-test-components/cmp-static-file-test': [{ environmentVariables: {}, version: '1.0.0' }],
156
+ },
157
+ componentVersionRules: {},
158
+ };
159
+
160
+ await addComponentSet(componentSet);
161
+
162
+ const response = await renderService.get(
163
+ '/r/set/smoke-test-components/cmp-static-file-test/1.0.0/?something=hello',
164
+ );
104
165
  expect(response.status).toEqual(200);
105
166
  expect(response.data).toEqual(
106
167
  [
107
168
  '<div>Input: hello</div>',
108
- '<div>cmp-static-file-test 1.0.0 ',
109
- `${configObj.renderServiceUrl}/s/cmp-static-file-test/1.0.0/birthday-cake.png</div>`,
169
+ '<div>smoke-test-components/cmp-static-file-test 1.0.0 ',
170
+ `${configObj.renderServiceUrl}/s/smoke-test-components/cmp-static-file-test/1.0.0/birthday-cake.png</div>`,
110
171
  ].join(''),
111
172
  );
112
173
  });
@@ -1,6 +1,7 @@
1
1
  import { ScanStatus, uploadFile } from '@squiz/virus-scanner-lib';
2
- import { loadManifest, zipDirectory } from '@squiz/component-lib';
3
- import { V1 } from '@squiz/component-lib';
2
+
3
+ import { zipDirectory } from '@squiz/dx-common-lib';
4
+ import { Manifest, ManifestServiceForDev } from '@squiz/component-lib';
4
5
  import fsp from 'fs/promises';
5
6
  import path from 'path';
6
7
  import axios, { AxiosResponse, AxiosError, AxiosInstance } from 'axios';
@@ -13,8 +14,9 @@ export async function uploadComponentFolder(
13
14
  folderPath: string,
14
15
  componentServiceManagementUrl: string,
15
16
  componentRenderServiceUrl: string,
17
+ baseTempDir: string = '',
16
18
  ): Promise<void> {
17
- const tmpDir = path.resolve(await fsp.mkdtemp('cmp-upload'));
19
+ const tmpDir = await fsp.mkdtemp(path.resolve(baseTempDir, 'cmp-upload'));
18
20
 
19
21
  try {
20
22
  const axiosInstance = axios.create({
@@ -58,19 +60,26 @@ export async function uploadComponentFolder(
58
60
  }
59
61
 
60
62
  async function preUploadChecks(folderPath: string, renderService: string) {
61
- const result = await loadManifest(path.join(folderPath, `manifest.json`));
63
+ const service = new ManifestServiceForDev(folderPath, logger);
64
+ const manifestPath = path.join(folderPath, `manifest.json`);
65
+
66
+ const result = await service.readManifest(manifestPath);
67
+ await service.assertManifestIsValid(manifestPath, result.getModel());
68
+
62
69
  if (await checkIfVersionExists(result, renderService)) {
63
- throw new Error(`Cannot upload component version, ${result.name} ${result.version} already exists`);
70
+ throw new Error(`Cannot upload component version, ${result.getName()} ${result.getVersion()} already exists`);
64
71
  }
65
72
  }
66
73
 
67
- async function checkIfVersionExists(inputManifest: V1, renderService: string) {
74
+ async function checkIfVersionExists(inputManifest: Manifest, renderService: string) {
68
75
  const axiosInstance = axios.create({
69
76
  baseURL: renderService,
70
77
  });
71
78
 
72
79
  try {
73
- const response = await axiosInstance.get(`d/${inputManifest.name}/${inputManifest.version}/manifest.json`);
80
+ const response = await axiosInstance.get(
81
+ `d/${inputManifest.getName()}/${inputManifest.getVersion()}/manifest.json`,
82
+ );
74
83
  if (response.status === 200) {
75
84
  return true;
76
85
  }