@applicaster/zapplicaster-cli 16.0.0-rc.8 → 16.0.0-rc.80

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapplicaster-cli",
3
- "version": "16.0.0-rc.8",
3
+ "version": "16.0.0-rc.80",
4
4
  "description": "CLI Tool for the zapp app and Quick Brick project",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/zapp-react-dom-cli-template": "16.0.0-rc.8",
32
- "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-rc.8",
33
- "@applicaster/zapp-react-native-cli-template": "16.0.0-rc.8",
34
- "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-rc.8",
31
+ "@applicaster/zapp-react-dom-cli-template": "16.0.0-rc.80",
32
+ "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-rc.80",
33
+ "@applicaster/zapp-react-native-cli-template": "16.0.0-rc.80",
34
+ "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-rc.80",
35
35
  "axios": "^0.28.0",
36
36
  "camelize": "^1.0.0",
37
37
  "chalk": "^2.3.2",
@@ -8,6 +8,7 @@ exports[`configurator builds the configuration object 1`] = `
8
8
  "appVersionId": "APP_VERSION_ID",
9
9
  "assets_url": "https://assets-secure.applicaster.com/zapp/accounts/5ae06cef8fba0f00084bd3c6/apps/com.quickbricktest/apple_store/1.8/styles/assets_styles.json",
10
10
  "otherParam": "param",
11
+ "vidaa_client_partner_id": undefined,
11
12
  },
12
13
  "destinationPath": "development-app",
13
14
  "deviceTarget": undefined,
@@ -32,6 +33,7 @@ exports[`configurator sets a null path if none is provided and the cli doesn't r
32
33
  "appVersionId": "APP_VERSION_ID",
33
34
  "assets_url": "https://assets-secure.applicaster.com/zapp/accounts/5ae06cef8fba0f00084bd3c6/apps/com.quickbricktest/apple_store/1.8/styles/assets_styles.json",
34
35
  "otherParam": "param",
36
+ "vidaa_client_partner_id": undefined,
35
37
  },
36
38
  "destinationPath": null,
37
39
  "deviceTarget": undefined,
@@ -193,6 +193,15 @@ async function appBootstrapper(configuration) {
193
193
  .map(toDayJSLocaleMap)
194
194
  .filter((v) => v !== "en");
195
195
 
196
+ if (
197
+ configuration.buildParams.device_target === "vidaa" &&
198
+ !configuration.buildParams.vidaa_client_partner_id
199
+ ) {
200
+ logger.warn(
201
+ "In order to use the VIDAA SDK you will need to set vidaa_client_partner_id"
202
+ );
203
+ }
204
+
196
205
  configuration.excludedNodeModules = getExcludedNodeModules(plugins);
197
206
  const { filesToRender } = template.files;
198
207
 
@@ -1,9 +1,30 @@
1
1
  const R = require("ramda");
2
2
  const axios = require("axios");
3
+
4
+ const {
5
+ platformIsTV,
6
+ } = require("@applicaster/zapp-react-native-utils/manifestUtils/platformIsTV");
7
+
3
8
  const { getAppVersionParams } = require("../../zapp");
4
9
  const { resolveAppTemplate } = require("../../templates");
5
10
  const { isZappReactNativeRepo } = require("../../settings");
6
11
 
12
+ function resolveVidaaClientPartnerId(buildParams = {}) {
13
+ const fromBuildParams = buildParams.vidaa_client_partner_id;
14
+
15
+ if (typeof fromBuildParams === "string" && fromBuildParams.trim()) {
16
+ return fromBuildParams.trim();
17
+ }
18
+
19
+ const fromEnv = process.env.vidaa_client_partner_id;
20
+
21
+ if (typeof fromEnv === "string" && fromEnv.trim()) {
22
+ return fromEnv.trim();
23
+ }
24
+
25
+ return undefined;
26
+ }
27
+
7
28
  const store_map = {
8
29
  amazon: "amazon",
9
30
  google_play: "android",
@@ -11,6 +32,7 @@ const store_map = {
11
32
  samsung_app_store: "samsung_tv",
12
33
  lg_content_store: "lg_tv",
13
34
  vizio_app_store: "vizio",
35
+ vidaa_app_store: "vidaa",
14
36
  };
15
37
 
16
38
  /**
@@ -21,7 +43,7 @@ const store_map = {
21
43
  */
22
44
  async function retrievesSpinnerColor(url, platform) {
23
45
  try {
24
- if (!R.contains(platform, ["samsung_tv", "lg_tv", "vizio"])) {
46
+ if (!platformIsTV(platform)) {
25
47
  return;
26
48
  }
27
49
 
@@ -72,6 +94,9 @@ async function configurator({ cliArgs, cliOptions }) {
72
94
 
73
95
  const buildParams = await getAppVersionParams(appVersionId);
74
96
 
97
+ buildParams.vidaa_client_partner_id =
98
+ resolveVidaaClientPartnerId(buildParams);
99
+
75
100
  delete buildParams.styles_url; // deprecated
76
101
 
77
102
  const platform = resolvePlatform(buildParams.assets_url);
@@ -133,6 +133,12 @@ const defaultFonts = {
133
133
  "Miso-Bold.ttf",
134
134
  ...commonFonts,
135
135
  ],
136
+ vidaa: [
137
+ "Miso-Light.ttf",
138
+ "Miso-Regular.ttf",
139
+ "Miso-Bold.ttf",
140
+ ...commonFonts,
141
+ ],
136
142
  vizio: [
137
143
  "Miso-Light.ttf",
138
144
  "Miso-Regular.ttf",
@@ -49,6 +49,7 @@ function renderManifest({
49
49
 
50
50
  const validPlatforms = [
51
51
  "lg_tv",
52
+ "vidaa",
52
53
  "samsung_tv",
53
54
  "vizio",
54
55
  "android_tv_for_quickbrick",
@@ -0,0 +1,58 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`reloadConfig runs the reload config task 1`] = `
4
+ [
5
+ [
6
+ "reload configuration",
7
+ "We're reloading the configuration for your app",
8
+ ],
9
+ ]
10
+ `;
11
+
12
+ exports[`reloadConfig runs the reload config task 2`] = `[]`;
13
+
14
+ exports[`reloadConfig runs the reload config task 3`] = `[]`;
15
+
16
+ exports[`reloadConfig runs the reload config task 4`] = `[]`;
17
+
18
+ exports[`reloadConfig runs the reload config task 5`] = `
19
+ [
20
+ [
21
+ "checking prerequisites...",
22
+ ],
23
+ [
24
+ "Gathering configuration",
25
+ ],
26
+ [
27
+ "Creating workspace",
28
+ ],
29
+ [
30
+ "Creating runtime URLs configuration file",
31
+ ],
32
+ [
33
+ "Retrieving configuration files",
34
+ ],
35
+ ]
36
+ `;
37
+
38
+ exports[`reloadConfig runs the reload config task 6`] = `
39
+ [
40
+ [
41
+ "All good - moving on",
42
+ ],
43
+ [
44
+ "configuration retrieved",
45
+ ],
46
+ [
47
+ "Workspace created !",
48
+ ],
49
+ [
50
+ "Runtime configuration URLs file created",
51
+ ],
52
+ [
53
+ "Configuration files retrieved !",
54
+ ],
55
+ ]
56
+ `;
57
+
58
+ exports[`reloadConfig runs the reload config task 7`] = `[]`;
@@ -0,0 +1,86 @@
1
+ const { reloadConfig } = require("../index");
2
+
3
+ const appVersionId = "e7a17c6f-4113-41ac-9450-6281d22153a1";
4
+
5
+ const options = {
6
+ appVersionId,
7
+ openSdk: true,
8
+ config: "some_file",
9
+ };
10
+
11
+ jest.mock("../../../logger", () => ({
12
+ welcome: jest.fn(),
13
+ log: jest.fn(),
14
+ error: jest.fn(),
15
+ warn: jest.fn(),
16
+ startStep: jest.fn(),
17
+ endStep: jest.fn(),
18
+ success: jest.fn(),
19
+ }));
20
+
21
+ jest.mock("../../prepareWorkspace/prerequisitesChecker.js", () => ({
22
+ prerequisitesChecker: jest.fn(() => true),
23
+ }));
24
+
25
+ const mockConfig = { appVersionId, buildParams: {} };
26
+
27
+ jest.mock("../../prepareWorkspace/configurator.js", () => ({
28
+ configurator: jest.fn(() => mockConfig),
29
+ }));
30
+
31
+ jest.mock("../../prepareWorkspace/workspaceCreator.js", () => ({
32
+ workspaceCreator: jest.fn(),
33
+ }));
34
+
35
+ jest.mock("../../prepareWorkspace/configFilesRetriever.js", () => ({
36
+ configFilesRetriever: jest.fn(),
37
+ }));
38
+
39
+ jest.mock("../../prepareWorkspace/runtimeConfigurationUrlsCreator.js", () => ({
40
+ runtimeConfigurationUrlsCreator: jest.fn(),
41
+ }));
42
+
43
+ const {
44
+ prerequisitesChecker,
45
+ } = require("../../prepareWorkspace/prerequisitesChecker");
46
+
47
+ const { configurator } = require("../../prepareWorkspace/configurator");
48
+ const { workspaceCreator } = require("../../prepareWorkspace/workspaceCreator");
49
+
50
+ const {
51
+ configFilesRetriever,
52
+ } = require("../../prepareWorkspace/configFilesRetriever");
53
+
54
+ const {
55
+ runtimeConfigurationUrlsCreator,
56
+ } = require("../../prepareWorkspace/runtimeConfigurationUrlsCreator");
57
+
58
+ const logger = require("../../../logger");
59
+
60
+ describe("reloadConfig", () => {
61
+ it("runs the reload config task", async () => {
62
+ await reloadConfig(appVersionId, options);
63
+
64
+ expect(logger.welcome.mock.calls).toMatchSnapshot();
65
+ expect(logger.log.mock.calls).toMatchSnapshot();
66
+ expect(logger.error.mock.calls).toMatchSnapshot();
67
+ expect(logger.warn.mock.calls).toMatchSnapshot();
68
+ expect(logger.startStep.mock.calls).toMatchSnapshot();
69
+ expect(logger.endStep.mock.calls).toMatchSnapshot();
70
+ expect(logger.success.mock.calls).toMatchSnapshot();
71
+
72
+ expect(prerequisitesChecker).toHaveBeenCalledWith({
73
+ cliArgs: [appVersionId],
74
+ cliOptions: options,
75
+ });
76
+
77
+ expect(configurator).toHaveBeenCalledWith({
78
+ cliArgs: [appVersionId],
79
+ cliOptions: options,
80
+ });
81
+
82
+ expect(workspaceCreator).toHaveBeenCalledWith(mockConfig);
83
+ expect(runtimeConfigurationUrlsCreator).toHaveBeenCalledWith(mockConfig);
84
+ expect(configFilesRetriever).toHaveBeenCalledWith(mockConfig);
85
+ });
86
+ });
@@ -27,18 +27,18 @@ const reloadConfigTask = {
27
27
  error: "Could not create workspace",
28
28
  completion: "Workspace created !",
29
29
  },
30
- {
31
- start: "Retrieving configuration files",
32
- run: configFilesRetriever,
33
- error: "Could not retrieve configuration files",
34
- completion: "Configuration files retrieved !",
35
- },
36
30
  {
37
31
  start: "Creating runtime URLs configuration file",
38
32
  run: runtimeConfigurationUrlsCreator,
39
33
  error: "Could not create runtime_configuration_urls.json",
40
34
  completion: "Runtime configuration URLs file created",
41
35
  },
36
+ {
37
+ start: "Retrieving configuration files",
38
+ run: configFilesRetriever,
39
+ error: "Could not retrieve configuration files",
40
+ completion: "Configuration files retrieved !",
41
+ },
42
42
  ],
43
43
  };
44
44
 
@@ -1,6 +1,7 @@
1
1
  const { saveRemoteJsonToFile, saveAssets, saveFontFiles } = require("../index");
2
2
  const nock = require("nock");
3
3
  const R = require("ramda");
4
+ const fs = require("fs");
4
5
  const logger = require("../../logger");
5
6
 
6
7
  const getFileName = R.compose(R.last, R.split("/"));
@@ -109,9 +110,37 @@ describe("saveAssets", () => {
109
110
 
110
111
  afterEach(() => {
111
112
  clearAllMocks();
113
+
114
+ ["existsSync", "openSync", "closeSync", "readSync", "readFileSync"].forEach(
115
+ (method) => {
116
+ if (fs[method] && fs[method].mockRestore) {
117
+ fs[method].mockRestore();
118
+ }
119
+ }
120
+ );
112
121
  });
113
122
 
123
+ function mockDownloadedFile({ isZip, content } = {}) {
124
+ jest.spyOn(fs, "existsSync").mockReturnValue(true);
125
+ jest.spyOn(fs, "openSync").mockReturnValue(1);
126
+ jest.spyOn(fs, "closeSync").mockImplementation(() => ({}));
127
+
128
+ jest.spyOn(fs, "readSync").mockImplementation((fd, buffer) => {
129
+ const header = isZip
130
+ ? Buffer.from([0x50, 0x4b, 0x03, 0x04])
131
+ : Buffer.from("<?xm");
132
+
133
+ header.copy(buffer);
134
+
135
+ return 4;
136
+ });
137
+
138
+ jest.spyOn(fs, "readFileSync").mockReturnValue(content || "");
139
+ }
140
+
114
141
  it("downloads the files with cURL, and unzips the assets", () => {
142
+ mockDownloadedFile({ isZip: true });
143
+
115
144
  saveAssets({ destinationPath, platform }, assetsUrl);
116
145
 
117
146
  expect(loggerSpy).toHaveBeenCalledWith(`saving assets from ${assetsUrl}`);
@@ -122,6 +151,30 @@ describe("saveAssets", () => {
122
151
  filePath
123
152
  );
124
153
  });
154
+
155
+ it("throws a clear error and skips unzip when the bundle is not a zip", () => {
156
+ mockDownloadedFile({
157
+ isZip: false,
158
+ content:
159
+ '<?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code></Error>',
160
+ });
161
+
162
+ expect(() => saveAssets({ destinationPath, platform }, assetsUrl)).toThrow(
163
+ /AccessDenied/
164
+ );
165
+
166
+ expect(shell.unzip).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it("throws a clear error when the bundle was not downloaded", () => {
170
+ jest.spyOn(fs, "existsSync").mockReturnValue(false);
171
+
172
+ expect(() => saveAssets({ destinationPath, platform }, assetsUrl)).toThrow(
173
+ /was not downloaded/
174
+ );
175
+
176
+ expect(shell.unzip).not.toHaveBeenCalled();
177
+ });
125
178
  });
126
179
 
127
180
  describe("saveFontFiles", () => {
@@ -1,4 +1,5 @@
1
1
  const R = require("ramda");
2
+ const fs = require("fs");
2
3
  const axios = require("axios");
3
4
  const { resolve } = require("path");
4
5
  const { curlFile, curlMultipleFiles, unzip } = require("../shell");
@@ -12,6 +13,64 @@ const getFileName = R.ifElse(
12
13
  R.prop("name")
13
14
  );
14
15
 
16
+ // Local file signature for a zip archive: "PK\x03\x04"
17
+ const ZIP_SIGNATURE = "504b0304";
18
+
19
+ /**
20
+ * checks whether a file on disk is a real zip archive by reading its magic bytes.
21
+ * @param {String} file path to the file to inspect
22
+ * @returns {Boolean} true if the file starts with the zip local file signature
23
+ */
24
+ function isZipFile(file) {
25
+ const fd = fs.openSync(file, "r");
26
+
27
+ try {
28
+ const header = Buffer.alloc(4);
29
+ const bytesRead = fs.readSync(fd, header, 0, 4, 0);
30
+
31
+ return bytesRead === 4 && header.toString("hex") === ZIP_SIGNATURE;
32
+ } finally {
33
+ fs.closeSync(fd);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * verifies that the downloaded assets bundle is a real zip archive.
39
+ * The assets CDN responds with a small XML/HTML body (e.g. S3 "AccessDenied")
40
+ * when the bundle is missing or not yet published. Because the download uses
41
+ * `curl -O` without `-f`, that error body is saved as assets.zip and the process
42
+ * continues, only to crash later in webpack with a confusing parse error. This
43
+ * check fails fast in the prepare step with an actionable message instead.
44
+ * @param {String} file path to the downloaded assets bundle
45
+ * @param {String} assetsUrl original url, surfaced in the error message
46
+ */
47
+ function assertValidAssetsBundle(file, assetsUrl) {
48
+ if (!fs.existsSync(file)) {
49
+ throw new Error(
50
+ `Assets bundle was not downloaded from ${assetsUrl}. ` +
51
+ "Verify the app has a published build with generated assets for this platform."
52
+ );
53
+ }
54
+
55
+ if (isZipFile(file)) {
56
+ return;
57
+ }
58
+
59
+ const preview = fs
60
+ .readFileSync(file, "utf8")
61
+ .replace(/\s+/g, " ")
62
+ .trim()
63
+ .slice(0, 300);
64
+
65
+ throw new Error(
66
+ `Invalid assets bundle downloaded from ${assetsUrl} - expected a zip archive ` +
67
+ `but received non-zip content:\n "${preview}"\n` +
68
+ "This usually means the assets bundle has not been generated/published for " +
69
+ 'this platform, or the URL is not accessible (e.g. S3 "AccessDenied"). ' +
70
+ "Publish a build with assets for this platform before preparing the workspace."
71
+ );
72
+ }
73
+
15
74
  /**
16
75
  * saves a remote json file in the app's configuration folder.
17
76
  * curried function of the shape saveRemoteJsonToFile(configuration)(fileUrl)
@@ -65,7 +124,12 @@ function saveAssets({ destinationPath, platform }, assetsUrl) {
65
124
  });
66
125
 
67
126
  curlFile(assetsUrl, filePath);
68
- unzip(resolve(filePath, fileName), filePath);
127
+
128
+ const downloadedFile = resolve(filePath, fileName);
129
+
130
+ assertValidAssetsBundle(downloadedFile, assetsUrl);
131
+
132
+ unzip(downloadedFile, filePath);
69
133
  }
70
134
 
71
135
  /**
package/src/file/index.js CHANGED
@@ -61,8 +61,13 @@ function destinationFileNameMapper(destinationFileName) {
61
61
  /**
62
62
  * Copies an array of files from a folder to another
63
63
  *
64
+ * Each entry can either be a string (same relative path for source and
65
+ * destination) or an object `{ source, destination }` when a file should be
66
+ * read from one relative path and written to another. The latter lets several
67
+ * platforms reuse a single set of bundled assets without duplicating them.
68
+ *
64
69
  * @param {String} source folder of the files
65
- * @param {String[]} files list of the files
70
+ * @param {(String|{source: String, destination: String})[]} files list of the files
66
71
  * @param {String} destination path
67
72
  * @returns {Promise[]} array of promises with the status of the copied files
68
73
  */
@@ -72,14 +77,15 @@ function copyFiles(source, filesOrFilesGetter, destination, config) {
72
77
  ? filesOrFilesGetter(config)
73
78
  : filesOrFilesGetter;
74
79
 
75
- return R.map(
76
- (file) =>
77
- copyFileSync(
78
- join(source, file),
79
- destinationFileNameMapper(join(destination, file))
80
- ),
81
- files
82
- );
80
+ return R.map((file) => {
81
+ const sourceFile = typeof file === "string" ? file : file.source;
82
+ const destinationFile = typeof file === "string" ? file : file.destination;
83
+
84
+ return copyFileSync(
85
+ join(source, sourceFile),
86
+ destinationFileNameMapper(join(destination, destinationFile))
87
+ );
88
+ }, files);
83
89
  }
84
90
 
85
91
  module.exports = {
@@ -32,4 +32,4 @@ exports[`resolveAppTemplate retrieves the template from the provided path if pro
32
32
  }
33
33
  `;
34
34
 
35
- exports[`resolveAppTemplate throws if resolving a template for an unknown platform 1`] = `"could not find template for foo - available templates: ios,android,samsung_tv,lg_tv,vizio,apple_tv,android_tv,amazon"`;
35
+ exports[`resolveAppTemplate throws if resolving a template for an unknown platform 1`] = `"could not find template for foo - available templates: ios,android,samsung_tv,lg_tv,vidaa,vizio,apple_tv,android_tv,amazon"`;
@@ -27,6 +27,7 @@ const templatesMap = {
27
27
  android: "@applicaster/zapp-react-native-cli-template",
28
28
  samsung_tv: "@applicaster/zapp-react-dom-cli-template",
29
29
  lg_tv: "@applicaster/zapp-react-dom-cli-template",
30
+ vidaa: "@applicaster/zapp-react-dom-cli-template",
30
31
  vizio: "@applicaster/zapp-react-dom-cli-template",
31
32
  apple_tv: "@applicaster/zapp-react-native-tvos-cli-template",
32
33
  android_tv: "@applicaster/zapp-react-native-android-tv-cli-template",