@applicaster/zapplicaster-cli 16.0.0-alpha.7128076344 → 16.0.0-alpha.7501599877

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-alpha.7128076344",
3
+ "version": "16.0.0-alpha.7501599877",
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-alpha.7128076344",
32
- "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-alpha.7128076344",
33
- "@applicaster/zapp-react-native-cli-template": "16.0.0-alpha.7128076344",
34
- "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-alpha.7128076344",
31
+ "@applicaster/zapp-react-dom-cli-template": "16.0.0-alpha.7501599877",
32
+ "@applicaster/zapp-react-native-android-tv-cli-template": "16.0.0-alpha.7501599877",
33
+ "@applicaster/zapp-react-native-cli-template": "16.0.0-alpha.7501599877",
34
+ "@applicaster/zapp-react-native-tvos-cli-template": "16.0.0-alpha.7501599877",
35
35
  "axios": "^0.28.0",
36
36
  "camelize": "^1.0.0",
37
37
  "chalk": "^2.3.2",
@@ -1,7 +1,8 @@
1
1
  const R = require("ramda");
2
2
  const fs = require("fs");
3
3
  const semver = require("semver");
4
- const { resolve } = require("path");
4
+ const { resolve, join } = require("path");
5
+ const { existsSync } = require("fs");
5
6
  const packageJSON = require("../../../package.json");
6
7
 
7
8
  const {
@@ -30,6 +31,36 @@ const {
30
31
 
31
32
  const { buildMobileApp } = require("./buildMobile");
32
33
 
34
+ async function patchPackages() {
35
+ if (existsSync(join(process.cwd(), "quick_brick", "patch_rn.js"))) {
36
+ exec(`node ${process.cwd()}/quick_brick/patch_rn.js`);
37
+ } else if (
38
+ existsSync(join(process.cwd(), "development-app", "patch_rn.js"))
39
+ ) {
40
+ exec(`node ${process.cwd()}/development-app/patch_rn.js`);
41
+ } else if (
42
+ existsSync(
43
+ join(
44
+ process.cwd(),
45
+ "packages",
46
+ "zapp-react-dom-development-app",
47
+ "patch_rn.js"
48
+ )
49
+ )
50
+ ) {
51
+ const pathToFile = join(
52
+ process.cwd(),
53
+ "packages",
54
+ "zapp-react-dom-development-app",
55
+ "patch_rn.js"
56
+ );
57
+
58
+ exec(`node ${pathToFile}`);
59
+ } else {
60
+ logger.log("Cannot patch packages, can't find the patch_rn.js file");
61
+ }
62
+ }
63
+
33
64
  /**
34
65
  * fallback to en for missing dayjslocalization
35
66
  * converts the locale code to a dayjs compatible locale code
@@ -101,12 +132,7 @@ async function appBootstrapper(configuration) {
101
132
 
102
133
  const { templatePath, filesToCopy } = template.files;
103
134
 
104
- await copyFiles(
105
- templatePath,
106
- filesToCopy,
107
- quickBrickProjectPath,
108
- configuration
109
- );
135
+ copyFiles(templatePath, filesToCopy, quickBrickProjectPath, configuration);
110
136
 
111
137
  logger.log("injecting dependencies...");
112
138
  const { dependencies, scripts } = template;
@@ -190,6 +216,8 @@ async function appBootstrapper(configuration) {
190
216
  const quickBrickVersion = packageJSON.version;
191
217
  checkAllPluginsSatisfiesQuickBrickVersion({ quickBrickVersion, plugins });
192
218
 
219
+ await patchPackages();
220
+
193
221
  if (buildMobile) {
194
222
  await buildMobileApp(configuration);
195
223
  }
@@ -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
 
@@ -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",
@@ -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
  /**
@@ -5,27 +5,10 @@ exports[`copyFiles when destination path has no package.json doesn't skips the c
5
5
  [
6
6
  "some/source/path/foo.json",
7
7
  "some/destination/Path/foo.json",
8
- [Function],
9
8
  ],
10
9
  [
11
10
  "some/source/path/package.json",
12
11
  "some/destination/Path/package.json",
13
- [Function],
14
- ],
15
- ]
16
- `;
17
-
18
- exports[`copyFiles when there is no package.json to copy copies the dir 1`] = `
19
- [
20
- [
21
- "some/source/path/files/file1.txt",
22
- "some/destination/Path/files/file1.txt",
23
- [Function],
24
- ],
25
- [
26
- "some/source/path/files/file2.txt",
27
- "some/destination/Path/files/file2.txt",
28
- [Function],
29
12
  ],
30
13
  ]
31
14
  `;
@@ -35,12 +18,10 @@ exports[`copyFiles when there is no package.json to copy copies the files 1`] =
35
18
  [
36
19
  "some/source/path/foo.json",
37
20
  "some/destination/Path/foo.json",
38
- [Function],
39
21
  ],
40
22
  [
41
23
  "some/source/path/bar.json",
42
24
  "some/destination/Path/bar.json",
43
- [Function],
44
25
  ],
45
26
  ]
46
27
  `;
@@ -38,27 +38,14 @@ describe("copyFiles", () => {
38
38
  fs.writeFile("foo.json", { foo: "bar" }, jest.fn());
39
39
  fs.writeFile("bar.json", { bar: "baz" }, jest.fn());
40
40
  fs.writeFile("qux.json", { qux: 42 }, jest.fn());
41
- fs.copyFile.mockClear();
42
- fs.readdir.mockClear();
41
+ fs.copyFileSync.mockClear();
43
42
  });
44
43
 
45
44
  describe("when there is no package.json to copy", () => {
46
- it("copies the files", async () => {
47
- await copyFiles(sourcePath, ["foo.json", "bar.json"], destinationPath);
48
- expect(fs.copyFile).toHaveBeenCalledTimes(2);
49
- expect(fs.copyFile.mock.calls).toMatchSnapshot();
50
- });
51
-
52
- it("copies the dir", async () => {
53
- // Mock fs.readdir to return some files when the directory is read
54
- fs.readdir.mockImplementationOnce((path, callback) => {
55
- callback(null, ["file1.txt", "file2.txt"]);
56
- });
57
-
58
- await copyFiles(sourcePath, ["files/"], destinationPath);
59
-
60
- expect(fs.copyFile).toHaveBeenCalledTimes(2);
61
- expect(fs.copyFile.mock.calls).toMatchSnapshot();
45
+ it("copies the files", () => {
46
+ copyFiles(sourcePath, ["foo.json", "bar.json"], destinationPath);
47
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(2);
48
+ expect(fs.copyFileSync.mock.calls).toMatchSnapshot();
62
49
  });
63
50
  });
64
51
 
@@ -73,40 +60,29 @@ describe("copyFiles", () => {
73
60
  fs.__resetFiles();
74
61
  });
75
62
 
76
- it("overrides it", async () => {
63
+ it("overrides it", () => {
77
64
  const sourcePath = "some/source/path";
78
65
  const destinationPath = "some/destination/Path";
79
66
 
80
- await copyFiles(
81
- sourcePath,
82
- ["foo.json", "package.json"],
83
- destinationPath
84
- );
85
-
86
- expect(fs.copyFile).toHaveBeenCalledTimes(2);
67
+ copyFiles(sourcePath, ["foo.json", "package.json"], destinationPath);
68
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(2);
87
69
 
88
- expect(fs.copyFile).toHaveBeenCalledWith(
70
+ expect(fs.copyFileSync).toHaveBeenCalledWith(
89
71
  `${sourcePath}/foo.json`,
90
- `${destinationPath}/foo.json`,
91
- expect.any(Function)
72
+ `${destinationPath}/foo.json`
92
73
  );
93
74
  });
94
75
  });
95
76
 
96
77
  describe("when destination path has no package.json", () => {
97
- it("doesn't skips the copy", async () => {
78
+ it("doesn't skips the copy", () => {
98
79
  const sourcePath = "some/source/path";
99
80
  const destinationPath = "some/destination/Path";
100
81
 
101
- await copyFiles(
102
- sourcePath,
103
- ["foo.json", "package.json"],
104
- destinationPath
105
- );
106
-
107
- expect(fs.copyFile).toHaveBeenCalledTimes(2);
82
+ copyFiles(sourcePath, ["foo.json", "package.json"], destinationPath);
83
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(2);
108
84
 
109
- expect(fs.copyFile.mock.calls).toMatchSnapshot();
85
+ expect(fs.copyFileSync.mock.calls).toMatchSnapshot();
110
86
  expect(logger.warn).not.toHaveBeenCalled();
111
87
  });
112
88
  });
@@ -124,7 +100,7 @@ describe("copyFolder", () => {
124
100
  });
125
101
 
126
102
  beforeEach(() => {
127
- fs.copyFile.mockClear();
103
+ fs.copyFileSync.mockClear();
128
104
  });
129
105
 
130
106
  it("copies the file in the folder", async () => {
@@ -134,7 +110,7 @@ describe("copyFolder", () => {
134
110
  await copyFolder(sourcePath, destinationPath);
135
111
 
136
112
  expect(fs.readdir).toHaveBeenCalledWith(sourcePath, expect.any(Function));
137
- expect(fs.copyFile).toHaveBeenCalledTimes(2);
113
+ expect(fs.copyFileSync).toHaveBeenCalledTimes(2);
138
114
  });
139
115
 
140
116
  afterAll(() => {
package/src/file/index.js CHANGED
@@ -5,16 +5,7 @@ const R = require("ramda");
5
5
  * @module @applicaster/zapplicaster-cli/util/file
6
6
  */
7
7
 
8
- const {
9
- writeFile,
10
- readFile,
11
- readdir,
12
- copyFile,
13
- lstatSync,
14
- mkdirSync,
15
- existsSync,
16
- } = require("fs");
17
-
8
+ const { writeFile, readFile, readdir, copyFile, copyFileSync } = require("fs");
18
9
  const { promisify } = require("util");
19
10
  const { join } = require("path");
20
11
 
@@ -70,49 +61,31 @@ function destinationFileNameMapper(destinationFileName) {
70
61
  /**
71
62
  * Copies an array of files from a folder to another
72
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
+ *
73
69
  * @param {String} source folder of the files
74
- * @param {String[]} files list of the files
70
+ * @param {(String|{source: String, destination: String})[]} files list of the files
75
71
  * @param {String} destination path
76
72
  * @returns {Promise[]} array of promises with the status of the copied files
77
73
  */
78
- async function copyFiles(source, filesOrFilesGetter, destination, config) {
74
+ function copyFiles(source, filesOrFilesGetter, destination, config) {
79
75
  const files =
80
76
  typeof filesOrFilesGetter === "function"
81
77
  ? filesOrFilesGetter(config)
82
78
  : filesOrFilesGetter;
83
79
 
84
- return Promise.all(
85
- R.map(async (file) => {
86
- let sourcePath, destinationPath;
87
-
88
- try {
89
- if (Array.isArray(file)) {
90
- sourcePath = join(source, file[0]);
91
-
92
- destinationPath = destinationFileNameMapper(
93
- join(destination, file[1])
94
- );
95
- } else {
96
- sourcePath = join(source, file);
97
- destinationPath = destinationFileNameMapper(join(destination, file));
98
- }
99
-
100
- if (lstatSync(sourcePath).isDirectory()) {
101
- if (!existsSync(destinationPath)) {
102
- mkdirSync(destinationPath, { recursive: true });
103
- }
104
-
105
- return await copyFolder(sourcePath, destinationPath);
106
- }
80
+ return R.map((file) => {
81
+ const sourceFile = typeof file === "string" ? file : file.source;
82
+ const destinationFile = typeof file === "string" ? file : file.destination;
107
83
 
108
- return await copyFileAsync(sourcePath, destinationPath);
109
- } catch (error) {
110
- throw new Error(
111
- `Failed to copy "${sourcePath}" to "${destinationPath}": ${error.message}`
112
- );
113
- }
114
- }, files)
115
- );
84
+ return copyFileSync(
85
+ join(source, sourceFile),
86
+ destinationFileNameMapper(join(destination, destinationFile))
87
+ );
88
+ }, files);
116
89
  }
117
90
 
118
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",
@@ -2,10 +2,6 @@ const fs = jest.genMockFromModule("fs");
2
2
  const _fs = jest.requireActual("fs");
3
3
  const R = require("ramda");
4
4
 
5
- function mkdirSync() {
6
- return undefined;
7
- }
8
-
9
5
  function readdirSync(folder) {
10
6
  return _fs.readdirSync(folder);
11
7
  }
@@ -81,16 +77,10 @@ function __resetFiles() {
81
77
  fs.__writenFiles = [];
82
78
  }
83
79
 
84
- fs.lstatSync = (_path) => ({
85
- isDirectory: () => _path.endsWith("/"),
86
- isFile: () => _path.endsWith("/") === false,
87
- });
88
-
89
80
  fs.__writenFiles = [];
90
81
  fs.__resetFiles = __resetFiles;
91
82
  fs.__addMockedFiles = __addMockedFiles;
92
83
  fs.readdirSync = readdirSync;
93
- fs.mkdirSync = mkdirSync;
94
84
  fs.readdir = jest.fn(readdir);
95
85
  fs.writeFile = jest.fn(writeFile);
96
86
  fs.readFile = jest.fn(readFile);