aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/api/package.js
blob: c64132faaa8ff14a66254c47b66ff4655b74de25 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/**
 * Package recipe into tar.gz file
 */
const targz = require('targz');
const fs = require('fs-extra');
const path = require('path');
const sizeOf = require('image-size');
const semver = require('semver');

// Publicly availible link to this repository's uncompressed folder
// Used for generating public icon URLs
const repo = 'https://cdn.jsdelivr.net/gh/getferdi/recipes/uncompressed/';

// Helper: Compress src folder into dest file
const compress = (src, dest) => new Promise((resolve, reject) => {
  targz.compress({
    src,
    dest,
    tar: {
      // Don't package .DS_Store files
      ignore: function(name) {
          return path.basename(name) === '.DS_Store'
      }
    },
  }, (err) => {
    if (err) {
      reject(err);
    } else {
      resolve(dest);
    }
  });
});
// Let us work in an async environment
module.exports = async () => {
  // Create paths to important files
  const recipeSrc = path.join(__dirname, '../recipe_src');
  const packageJson = path.join(recipeSrc, 'package.json');
  const svgIcon = path.join(recipeSrc, 'icon.svg');
  const pngIcon = path.join(recipeSrc, 'icon.png');
  const allJson = path.join(__dirname, '../../', 'all.json');
  let all = await fs.readJson(allJson);

  let errorMessages = []

  // Check that package.json exists
  if (!await fs.pathExists(packageJson)) {
    errorMessages.push(`⚠️ It looks like your recipe is missing the "package.json" file.
    ↪ Please add your recipe to ${recipeSrc} and make sure that folder contains a "package.json".
    ℹ For more information on how to add your recipe visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md`);
  }

  // Check that icons exist
  const hasSvg = await fs.pathExists(svgIcon);
  if (!hasSvg) {
    errorMessages.push(`⚠️ It looks like your recipe is missing the "icon.svg" file.
    ↪ Please make sure your recipe contains an icon.svg file.
    ℹ For more information about recipe icons visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#icons`);  
  }
  
  const hasPng = await fs.pathExists(pngIcon);
  if (!hasPng) {
    errorMessages.push(`⚠️ It looks like your recipe is missing the "icon.png" file.
    ↪ Please make sure your recipe contains an icon.png file.
    ↪ Please also make sure that your PNG icon is 1024x1024px in size.
    ℹ For more information about recipe icons visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#icons`);
  }

  // Check that icons have the right dimensions
  if (hasSvg) {
    const svgSize = sizeOf(svgIcon);
    const svgHasRightSize = svgSize.width === svgSize.height;
    if (!svgHasRightSize) {
      errorMessages.push(`⚠️ It looks like your "icon.svg" is not a square.
      ↪ Please make sure that your "icon.svg" has the right dimensions to make a square- width and height should be the same.
      ℹ You can use software like Photoshop, GIMP or Photopea (https://www.photopea.com/) to resize your icons.
      ℹ For more information about recipe icons visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#icons`);
    }
  }

  if (hasPng) {
    const pngSize = sizeOf(pngIcon);
    const pngHasRightSize = pngSize.width === 1024 && pngSize.height === 1024;
    if (hasPng && !pngHasRightSize) {
      errorMessages.push(`⚠️ it looks like your "icon.png" is not 1024x1024 in size.
      ↪ Please make sure that your "icon.png" has the right dimeensions of 1024x1024px.
      ℹ You can use software like Photoshop, GIMP or Photopea (https://www.photopea.com/) to resize your icons.
      ℹ For more information about recipe icons visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#icons`);
    }
  }

  // Read package.json
  const config = await fs.readJson(packageJson)

  // Make sure it contains all required fields
  if (!config) {
    errorMessages.push(`⚠️ It looks like your "package.json" file could not read or parsed.
    ↪ Please make sure your "package.json" contains valid JSON.
    ℹ You can use a JSON Validator like JSONLint: https://jsonlint.com/
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  }
  
  if (!config.id) {
    errorMessages.push(`⚠️ It looks like your "package.json" does not contain an "id" field.
    ↪ Please make sure the "id" field contains a unique ID made of lowercase letters (a-z), numbers (0-9), hyphens (-), periods (.), and underscores (_)
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  } else if (!/^[a-z._\-]+$/.test(config.id)) {
    errorMessages.push(`⚠️ It looks like your "package.json" defines an invalid recipe ID.
    ↪ Please make sure the "id" field only contains lowercase letters (a-z), numbers (0-9), hyphens (-), periods (.), and underscores (_)
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  }
  if (!config.name) {
    errorMessages.push(`⚠️ It looks like your "package.json" does not contain a "name" field.
    ↪ Please make sure the "name" field contains the name of the service (e.g. "Google Keep")
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  }
  if (!config.version) {
    errorMessages.push(`⚠️ It looks like your "package.json" does not contain a "version" field.
    ↪ Please make sure the "version" field contains a semver-compatible version number for your recipe (e.g. "1.0.0")
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  }
  if (!config.config || typeof config.config !== "object") {
    errorMessages.push(`⚠️ It looks like your "package.json" does not contain a "config" object.
    ↪ Please make sure the "config" object contains a configuration for your service.
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  } else if (!config.config.serviceURL) {
    errorMessages.push(`⚠️ It looks like your "package.json" does not contain a "config" object without a "serviceURL" field.
    ↪ Please make sure the "serviceURL" contains the URL of your service.
    ℹ For more information about the package.json file visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md`);
  }

  // Index of the current recipe in all.json
  const packageIndex = all.findIndex(e => e.id === config.id)

  if (packageIndex !== -1) {
    const currentVersion = config.version;
    const repoVersion = all[packageIndex].version;

  if (semver.gte(repoVersion, currentVersion)) {
    errorMessages.push(`⚠️ It looks like your recipe is using the same version number as the current recipe.
    ↪ Please make sure to increase the version number inside your "package.json" everytime you want to repackage (e.g. '1.0.0' to '1.0.1').
    ↪ If you don't increase your version number, Ferdi cannot detect that you have made changes to the recipe.
    ℹ For more information about versioning of recipes visit: https://github.com/getferdi/recipes/blob/master/docs/configuration.md#config-flags`);
    }
  }

  if (!await fs.exists(path.join(recipeSrc, 'webview.js'))) {
    errorMessages.push(`⚠️ It looks like your recipe doesn't contain a "webview.js" file.
    ↪ Please make sure to create that file and add your features to it.
    ℹ For more information about the webview.js file visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#webviewjs and https://github.com/getferdi/recipes/blob/master/docs/frontend_api.md`);
  }
  if (!await fs.exists(path.join(recipeSrc, 'index.js'))) {
    errorMessages.push(`⚠️ It looks like your recipe doesn't contain a "index.js" file.
    ↪ Please make sure to create that file and add your features to it. For most recipes it is enough to simply add the basic template found at https://github.com/getferdi/recipes/blob/master/docs/integration.md#indexjs
    ℹ For more information about the webview.js file visit: https://github.com/getferdi/recipes/blob/master/docs/integration.md#indexjs and https://github.com/getferdi/recipes/blob/master/docs/backend_api.md`);
  }

  if (errorMessages.length > 0) {
    console.log(`❌ Could not add your recipe, the following ${errorMessages.length} error(s) were found:
${errorMessages.reduce((str, err) => `${str}\n${err}`)}
 For more information, visit: https://github.com/getferdi/recipes/tree/master/docs`);
    return;
  }

  // Package to .tar.gz
  console.log(`[Info] Packaging ${config.id}...`);
  compress(recipeSrc, path.join(__dirname, '../../', 'archives', `${config.id}.tar.gz`));

  // Copy recipe src folder to /uncompressed/:id folder
  console.log('[Info] Copying to uncompressed recipes');
  await fs.copy('recipe_src', path.join(__dirname, '../../', 'uncompressed', `${config.id}`));

  // Add recipe to all.json
  console.log('[Info] Adding to all.json');
  const isFeatured = packageIndex !== -1 ? all[packageIndex].featured : false;
  const packageInfo = {
    "author": config.author || '',
    "featured": isFeatured,
    "id": config.id,
    "name": config.name,
    "version": config.version || '1.0.0',
    "icons": {
      "png": `${repo}${config.id}/icon.png`,
      "svg": `${repo}${config.id}/icon.svg`,
    },
  };
  // Check if package ID already exists
  if (packageIndex !== -1) {
    console.log('[Info] Recipe with ID already exists - overwriting');
    all[packageIndex] = packageInfo;
  } else {
    console.log('[Info] No recipe with ID found - creating new.');
    all.push(packageInfo);
  }

  // Sort package list alphabetically
  all = all.sort((a, b) => {
    var textA = a.id.toLowerCase();
    var textB = b.id.toLowerCase();
    return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
  });
  await fs.writeJson(allJson, all, {
    spaces: 2,
    EOL: '\n',
  });

  console.log(`✅ Successfully packaged and added new recipe "${config.id}"`);
};