@applicaster/zapplicaster-cli 16.0.0-rc.25 → 16.0.0-rc.26

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.25",
3
+ "version": "16.0.0-rc.26",
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.25",
32
- "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-rc.25",
33
- "@applicaster/zapp-react-native-cli-template": "16.0.0-rc.25",
34
- "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-rc.25",
31
+ "@applicaster/zapp-react-dom-cli-template": "16.0.0-rc.26",
32
+ "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-rc.26",
33
+ "@applicaster/zapp-react-native-cli-template": "16.0.0-rc.26",
34
+ "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-rc.26",
35
35
  "axios": "^0.28.0",
36
36
  "camelize": "^1.0.0",
37
37
  "chalk": "^2.3.2",
@@ -1,5 +1,10 @@
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");
@@ -11,6 +16,7 @@ const store_map = {
11
16
  samsung_app_store: "samsung_tv",
12
17
  lg_content_store: "lg_tv",
13
18
  vizio_app_store: "vizio",
19
+ vidaa_app_store: "vidaa",
14
20
  };
15
21
 
16
22
  /**
@@ -21,7 +27,7 @@ const store_map = {
21
27
  */
22
28
  async function retrievesSpinnerColor(url, platform) {
23
29
  try {
24
- if (!R.contains(platform, ["samsung_tv", "lg_tv", "vizio"])) {
30
+ if (!platformIsTV(platform)) {
25
31
  return;
26
32
  }
27
33
 
@@ -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",
@@ -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",