aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorLibravatar Markus Hatvan <markus_hatvan@aon.at>2021-10-05 16:46:55 +0200
committerLibravatar GitHub <noreply@github.com>2021-10-05 16:46:55 +0200
commit2484c63d77e05fff384cc08b6ea46a29a22a62ec (patch)
tree21b8b74f1788407a4cfb13052160398f13424ac6 /scripts
parentAdd check to ensure that generated file 'user.js' is not present in default p... (diff)
downloadferdium-recipes-2484c63d77e05fff384cc08b6ea46a29a22a62ec.tar.gz
ferdium-recipes-2484c63d77e05fff384cc08b6ea46a29a22a62ec.tar.zst
ferdium-recipes-2484c63d77e05fff384cc08b6ea46a29a22a62ec.zip
chore: repo maintenance (#732)
Diffstat (limited to 'scripts')
-rw-r--r--scripts/create.js13
-rw-r--r--scripts/package.js221
2 files changed, 161 insertions, 73 deletions
diff --git a/scripts/create.js b/scripts/create.js
index 0e785fa..184dc70 100644
--- a/scripts/create.js
+++ b/scripts/create.js
@@ -14,14 +14,19 @@ You can set "Folder name" to "FerdiDev" to use Ferdi's development instance inst
14 14
15pnpm run create WhatsApp FerdiDev 15pnpm run create WhatsApp FerdiDev
16`); 16`);
17 return; 17 throw new Error('Please provide the correct number of args!');
18} 18}
19 19
20const recipeName = process.argv[2]; 20const recipeName = process.argv[2];
21const recipe = recipeName.toLowerCase().replace(/\s/g, '-'); 21const recipe = recipeName.toLowerCase().replace(/\s/g, '-');
22const cleanRecipeId = recipe.replace(/[^a-z]/g, ''); // Clean recipe ID only containing a-z, for usage as the JavaScript class name 22const cleanRecipeId = recipe.replace(/[^a-z]/g, ''); // Clean recipe ID only containing a-z, for usage as the JavaScript class name
23const folderName = process.argv[3] || 'Ferdi'; 23const folderName = process.argv[3] || 'Ferdi';
24const filesThatNeedTextReplace = ['package.json', 'index.js', 'webview.js', 'README.md']; 24const filesThatNeedTextReplace = [
25 'package.json',
26 'index.js',
27 'webview.js',
28 'README.md',
29];
25 30
26(async () => { 31(async () => {
27 // Folder paths 32 // Folder paths
@@ -36,7 +41,7 @@ const filesThatNeedTextReplace = ['package.json', 'index.js', 'webview.js', 'REA
36 const sampleRecipe = path.join(__dirname, 'sample_recipe'); 41 const sampleRecipe = path.join(__dirname, 'sample_recipe');
37 42
38 // Make sure dev recipe folder exists 43 // Make sure dev recipe folder exists
39 if (!(await fs.exists(recipesFolder))) { 44 if (!fs.existsSync(recipesFolder)) {
40 console.log( 45 console.log(
41 `Couldn't find your recipe folder (${recipesFolder}). Is Ferdi installed?`, 46 `Couldn't find your recipe folder (${recipesFolder}). Is Ferdi installed?`,
42 ); 47 );
@@ -44,7 +49,7 @@ const filesThatNeedTextReplace = ['package.json', 'index.js', 'webview.js', 'REA
44 } 49 }
45 await fs.ensureDir(devRecipeFolder); 50 await fs.ensureDir(devRecipeFolder);
46 51
47 if (await fs.exists(newRecipeFolder)) { 52 if (fs.existsSync(newRecipeFolder)) {
48 console.log('⚠️ Recipe already exists'); 53 console.log('⚠️ Recipe already exists');
49 return; 54 return;
50 } 55 }
diff --git a/scripts/package.js b/scripts/package.js
index 0f2ae6f..4fc02f1 100644
--- a/scripts/package.js
+++ b/scripts/package.js
@@ -13,24 +13,28 @@ const pkgVersionChangedMatcher = new RegExp(/\n\+.*version.*/);
13const repo = 'https://cdn.jsdelivr.net/gh/getferdi/recipes/recipes/'; 13const repo = 'https://cdn.jsdelivr.net/gh/getferdi/recipes/recipes/';
14 14
15// Helper: Compress src folder into dest file 15// Helper: Compress src folder into dest file
16const compress = (src, dest) => new Promise((resolve, reject) => { 16const compress = (src, dest) =>
17 targz.compress({ 17 new Promise((resolve, reject) => {
18 src, 18 targz.compress(
19 dest, 19 {
20 tar: { 20 src,
21 // Don't package .DS_Store files and .md files 21 dest,
22 ignore: function(name) { 22 tar: {
23 return path.basename(name) === '.DS_Store' || name.endsWith('.md') 23 // Don't package .DS_Store files and .md files
24 } 24 ignore: function (name) {
25 }, 25 return path.basename(name) === '.DS_Store' || name.endsWith('.md');
26 }, (err) => { 26 },
27 if (err) { 27 },
28 reject(err); 28 },
29 } else { 29 err => {
30 resolve(dest); 30 if (err) {
31 } 31 reject(err);
32 } else {
33 resolve(dest);
34 }
35 },
36 );
32 }); 37 });
33});
34 38
35// Let us work in an async environment 39// Let us work in an async environment
36(async () => { 40(async () => {
@@ -51,20 +55,23 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
51 const git = await simpleGit(repoRoot); 55 const git = await simpleGit(repoRoot);
52 const isGitRepo = await git.checkIsRepo(); 56 const isGitRepo = await git.checkIsRepo();
53 if (!isGitRepo) { 57 if (!isGitRepo) {
54 console.debug("NOT A git repo: will bypass dirty state checks"); 58 console.debug('NOT A git repo: will bypass dirty state checks');
55 } 59 }
56 60
57 const availableRecipes = fs.readdirSync(recipesFolder, { withFileTypes: true }) 61 const availableRecipes = fs
62 .readdirSync(recipesFolder, { withFileTypes: true })
58 .filter(dir => dir.isDirectory()) 63 .filter(dir => dir.isDirectory())
59 .map(dir => dir.name); 64 .map(dir => dir.name);
60 65
61 for(let recipe of availableRecipes) { 66 for (let recipe of availableRecipes) {
62 const recipeSrc = path.join(recipesFolder, recipe); 67 const recipeSrc = path.join(recipesFolder, recipe);
63 const packageJson = path.join(recipeSrc, 'package.json'); 68 const packageJson = path.join(recipeSrc, 'package.json');
64 69
65 // Check that package.json exists 70 // Check that package.json exists
66 if (!await fs.pathExists(packageJson)) { 71 if (!(await fs.pathExists(packageJson))) {
67 console.log(`⚠️ Couldn't package "${recipe}": Folder doesn't contain a "package.json".`); 72 console.log(
73 `⚠️ Couldn't package "${recipe}": Folder doesn't contain a "package.json".`,
74 );
68 unsuccessful++; 75 unsuccessful++;
69 continue; 76 continue;
70 } 77 }
@@ -73,7 +80,9 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
73 const svgIcon = path.join(recipeSrc, 'icon.svg'); 80 const svgIcon = path.join(recipeSrc, 'icon.svg');
74 const hasSvg = await fs.pathExists(svgIcon); 81 const hasSvg = await fs.pathExists(svgIcon);
75 if (!hasSvg) { 82 if (!hasSvg) {
76 console.log(`⚠️ Couldn't package "${recipe}": Recipe doesn't contain an icon SVG`); 83 console.log(
84 `⚠️ Couldn't package "${recipe}": Recipe doesn't contain an icon SVG`,
85 );
77 unsuccessful++; 86 unsuccessful++;
78 continue; 87 continue;
79 } 88 }
@@ -82,7 +91,9 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
82 const svgSize = sizeOf(svgIcon); 91 const svgSize = sizeOf(svgIcon);
83 const svgHasRightSize = svgSize.width === svgSize.height; 92 const svgHasRightSize = svgSize.width === svgSize.height;
84 if (!svgHasRightSize) { 93 if (!svgHasRightSize) {
85 console.log(`⚠️ Couldn't package "${recipe}": Recipe SVG icon isn't a square`); 94 console.log(
95 `⚠️ Couldn't package "${recipe}": Recipe SVG icon isn't a square`,
96 );
86 unsuccessful++; 97 unsuccessful++;
87 continue; 98 continue;
88 } 99 }
@@ -90,61 +101,114 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
90 // Check that user.js does not exist 101 // Check that user.js does not exist
91 const userJs = path.join(recipeSrc, 'user.js'); 102 const userJs = path.join(recipeSrc, 'user.js');
92 if (await fs.pathExists(userJs)) { 103 if (await fs.pathExists(userJs)) {
93 console.log(`⚠️ Couldn't package "${recipe}": Folder contains a "user.js".`); 104 console.log(
105 `⚠️ Couldn't package "${recipe}": Folder contains a "user.js".`,
106 );
94 unsuccessful++; 107 unsuccessful++;
95 continue; 108 continue;
96 } 109 }
97 110
98 // Read package.json 111 // Read package.json
99 const config = await fs.readJson(packageJson) 112 const config = await fs.readJson(packageJson);
100 113
101 // Make sure it contains all required fields 114 // Make sure it contains all required fields
102 if (!config) { 115 if (!config) {
103 console.log(`⚠️ Couldn't package "${recipe}": Could not read or parse "package.json"`); 116 console.log(
117 `⚠️ Couldn't package "${recipe}": Could not read or parse "package.json"`,
118 );
104 unsuccessful++; 119 unsuccessful++;
105 continue; 120 continue;
106 } 121 }
107 let configErrors = []; 122 let configErrors = [];
108 if (!config.id) { 123 if (!config.id) {
109 configErrors.push("The recipe's package.json contains no 'id' field. This field should contain a unique ID made of lowercase letters (a-z), numbers (0-9), hyphens (-), periods (.), and underscores (_)"); 124 configErrors.push(
125 "The recipe's package.json contains no 'id' field. This field should contain a unique ID made of lowercase letters (a-z), numbers (0-9), hyphens (-), periods (.), and underscores (_)",
126 );
127 // eslint-disable-next-line no-useless-escape
110 } else if (!/^[a-zA-Z0-9._\-]+$/.test(config.id)) { 128 } else if (!/^[a-zA-Z0-9._\-]+$/.test(config.id)) {
111 configErrors.push("The recipe's 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 (_)"); 129 configErrors.push(
130 "The recipe's 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 (_)",
131 );
112 } 132 }
113 if (config.id !== recipe) { 133 if (config.id !== recipe) {
114 configErrors.push(`The recipe's id (${config.id}) does not match the folder name (${recipe})`); 134 configErrors.push(
135 `The recipe's id (${config.id}) does not match the folder name (${recipe})`,
136 );
115 } 137 }
116 if (!config.name) { 138 if (!config.name) {
117 configErrors.push("The recipe's package.json contains no 'name' field. This field should contain the name of the service (e.g. 'Google Keep')"); 139 configErrors.push(
140 "The recipe's package.json contains no 'name' field. This field should contain the name of the service (e.g. 'Google Keep')",
141 );
118 } 142 }
119 if (!config.version) { 143 if (!config.version) {
120 configErrors.push("The recipe's package.json contains no 'version' field. This field should contain the a semver-compatible version number for your recipe (e.g. '1.0.0')"); 144 configErrors.push(
145 "The recipe's package.json contains no 'version' field. This field should contain the a semver-compatible version number for your recipe (e.g. '1.0.0')",
146 );
121 } 147 }
122 if (!config.config || typeof config.config !== "object") { 148 if (!config.config || typeof config.config !== 'object') {
123 configErrors.push("The recipe's package.json contains no 'config' object. This field should contain a configuration for your service."); 149 configErrors.push(
150 "The recipe's package.json contains no 'config' object. This field should contain a configuration for your service.",
151 );
124 } 152 }
125 153
126 const topLevelKeys = Object.keys(config); 154 const topLevelKeys = Object.keys(config);
127 topLevelKeys.forEach(key => { 155 topLevelKeys.forEach(key => {
128 if (typeof(config[key]) === 'string') { 156 if (typeof config[key] === 'string') {
129 if (config[key] === '') { 157 if (config[key] === '') {
130 configErrors.push(`The recipe's package.json contains empty value for key: ${key}`); 158 configErrors.push(
159 `The recipe's package.json contains empty value for key: ${key}`,
160 );
131 } 161 }
132 } else if ((key === 'config' || key === 'aliases') && typeof(config[key]) !== 'object') { 162 } else if (
133 configErrors.push(`The recipe's package.json contains unexpected value for key: ${key}`); 163 (key === 'config' || key === 'aliases') &&
164 typeof config[key] !== 'object'
165 ) {
166 configErrors.push(
167 `The recipe's package.json contains unexpected value for key: ${key}`,
168 );
134 } 169 }
135 }); 170 });
136 171
137 const knownTopLevelKeys = ['id', 'name', 'version', 'license', 'repository', 'aliases', 'config']; 172 const knownTopLevelKeys = [
138 const unrecognizedKeys = topLevelKeys.filter(x => !knownTopLevelKeys.includes(x)); 173 'id',
174 'name',
175 'version',
176 'license',
177 'repository',
178 'aliases',
179 'config',
180 ];
181 const unrecognizedKeys = topLevelKeys.filter(
182 x => !knownTopLevelKeys.includes(x),
183 );
139 if (unrecognizedKeys.length > 0) { 184 if (unrecognizedKeys.length > 0) {
140 configErrors.push(`The recipe's package.json contains the following keys that are not recognized: ${unrecognizedKeys}`); 185 configErrors.push(
186 `The recipe's package.json contains the following keys that are not recognized: ${unrecognizedKeys}`,
187 );
141 } 188 }
142 if (config.config && typeof config.config === "object") { 189 if (config.config && typeof config.config === 'object') {
143 const configKeys = Object.keys(config.config); 190 const configKeys = Object.keys(config.config);
144 const knownConfigKeys = ['serviceURL', 'hasTeamId', 'urlInputPrefix', 'urlInputSuffix', 'hasHostedOption', 'hasCustomUrl', 'hasNotificationSound', 'hasDirectMessages', 'hasIndirectMessages', 'allowFavoritesDelineationInUnreadCount', 'message', 'disablewebsecurity']; 191 const knownConfigKeys = [
145 const unrecognizedConfigKeys = configKeys.filter(x => !knownConfigKeys.includes(x)); 192 'serviceURL',
193 'hasTeamId',
194 'urlInputPrefix',
195 'urlInputSuffix',
196 'hasHostedOption',
197 'hasCustomUrl',
198 'hasNotificationSound',
199 'hasDirectMessages',
200 'hasIndirectMessages',
201 'allowFavoritesDelineationInUnreadCount',
202 'message',
203 'disablewebsecurity',
204 ];
205 const unrecognizedConfigKeys = configKeys.filter(
206 x => !knownConfigKeys.includes(x),
207 );
146 if (unrecognizedConfigKeys.length > 0) { 208 if (unrecognizedConfigKeys.length > 0) {
147 configErrors.push(`The recipe's package.json contains the following keys that are not recognized: ${unrecognizedConfigKeys}`); 209 configErrors.push(
210 `The recipe's package.json contains the following keys that are not recognized: ${unrecognizedConfigKeys}`,
211 );
148 } 212 }
149 213
150 // if (config.config.hasCustomUrl !== undefined && config.config.hasHostedOption !== undefined) { 214 // if (config.config.hasCustomUrl !== undefined && config.config.hasHostedOption !== undefined) {
@@ -152,8 +216,13 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
152 // } 216 // }
153 217
154 configKeys.forEach(key => { 218 configKeys.forEach(key => {
155 if (typeof(config.config[key]) === 'string' && config.config[key] === '') { 219 if (
156 configErrors.push(`The recipe's package.json contains empty value for key: ${key}`); 220 typeof config.config[key] === 'string' &&
221 config.config[key] === ''
222 ) {
223 configErrors.push(
224 `The recipe's package.json contains empty value for key: ${key}`,
225 );
157 } 226 }
158 }); 227 });
159 } 228 }
@@ -164,38 +233,50 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
164 // Check for changes in recipe's directory, and if changes are present, then the changes should contain a version bump 233 // Check for changes in recipe's directory, and if changes are present, then the changes should contain a version bump
165 await git.diffSummary(relativeRepoSrc, (err, result) => { 234 await git.diffSummary(relativeRepoSrc, (err, result) => {
166 if (err) { 235 if (err) {
167 configErrors.push(`Got the following error while checking for git changes: ${err}`); 236 configErrors.push(
168 } else if (result && (result.changed !== 0 || result.insertions !== 0 || result.deletions !== 0)) { 237 `Got the following error while checking for git changes: ${err}`,
238 );
239 } else if (
240 result &&
241 (result.changed !== 0 ||
242 result.insertions !== 0 ||
243 result.deletions !== 0)
244 ) {
169 const pkgJsonRelative = path.relative(repoRoot, packageJson); 245 const pkgJsonRelative = path.relative(repoRoot, packageJson);
170 if (!result.files.find(({file}) => file === pkgJsonRelative)) { 246 if (!result.files.find(({ file }) => file === pkgJsonRelative)) {
171 configErrors.push(`Found changes in '${relativeRepoSrc}' without the corresponding version bump in '${pkgJsonRelative}'`); 247 configErrors.push(
248 `Found changes in '${relativeRepoSrc}' without the corresponding version bump in '${pkgJsonRelative}'`,
249 );
172 } else { 250 } else {
173 git.diff(pkgJsonRelative, (_diffErr, diffResult) => { 251 git.diff(pkgJsonRelative, (_diffErr, diffResult) => {
174 if (diffResult && !pkgVersionChangedMatcher.test(diffResult)) { 252 if (diffResult && !pkgVersionChangedMatcher.test(diffResult)) {
175 configErrors.push(`Found changes in '${relativeRepoSrc}' without the corresponding version bump in '${pkgJsonRelative}' (found other changes though)`); 253 configErrors.push(
254 `Found changes in '${relativeRepoSrc}' without the corresponding version bump in '${pkgJsonRelative}' (found other changes though)`,
255 );
176 } 256 }
177 }); 257 });
178 } 258 }
179 } 259 }
180 }); 260 });
181 }; 261 }
182 262
183 if (configErrors.length > 0) { 263 if (configErrors.length > 0) {
184 console.log(`⚠️ Couldn't package "${recipe}": There were errors in the recipe's package.json: 264 console.log(`⚠️ Couldn't package "${recipe}": There were errors in the recipe's package.json:
185 ${configErrors.reduce((str, err) => `${str}\n${err}`)}`); 265 ${configErrors.reduce((str, err) => `${str}\n${err}`)}`);
186 unsuccessful++; 266 unsuccessful++;
187 continue;
188 } 267 }
189 268
190 if (!await fs.exists(path.join(recipeSrc, 'webview.js'))) { 269 if (!fs.existsSync(path.join(recipeSrc, 'webview.js'))) {
191 console.log(`⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "webview.js"`); 270 console.log(
271 `⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "webview.js"`,
272 );
192 unsuccessful++; 273 unsuccessful++;
193 continue;
194 } 274 }
195 if (!await fs.exists(path.join(recipeSrc, 'index.js'))) { 275 if (!fs.existsSync(path.join(recipeSrc, 'index.js'))) {
196 console.log(`⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "index.js"`); 276 console.log(
277 `⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "index.js"`,
278 );
197 unsuccessful++; 279 unsuccessful++;
198 continue;
199 } 280 }
200 281
201 // Package to .tar.gz 282 // Package to .tar.gz
@@ -204,13 +285,13 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
204 // Add recipe to all.json 285 // Add recipe to all.json
205 const isFeatured = featuredRecipes.includes(config.id); 286 const isFeatured = featuredRecipes.includes(config.id);
206 const packageInfo = { 287 const packageInfo = {
207 "featured": isFeatured, 288 featured: isFeatured,
208 "id": config.id, 289 id: config.id,
209 "name": config.name, 290 name: config.name,
210 "version": config.version, 291 version: config.version,
211 "aliases": config.aliases, 292 aliases: config.aliases,
212 "icons": { 293 icons: {
213 "svg": `${repo}${config.id}/icon.svg`, 294 svg: `${repo}${config.id}/icon.svg`,
214 }, 295 },
215 }; 296 };
216 recipeList.push(packageInfo); 297 recipeList.push(packageInfo);
@@ -220,14 +301,16 @@ const compress = (src, dest) => new Promise((resolve, reject) => {
220 recipeList = recipeList.sort((a, b) => { 301 recipeList = recipeList.sort((a, b) => {
221 var textA = a.id.toLowerCase(); 302 var textA = a.id.toLowerCase();
222 var textB = b.id.toLowerCase(); 303 var textB = b.id.toLowerCase();
223 return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; 304 return textA < textB ? -1 : textA > textB ? 1 : 0;
224 }); 305 });
225 await fs.writeJson(allJson, recipeList, { 306 await fs.writeJson(allJson, recipeList, {
226 spaces: 2, 307 spaces: 2,
227 EOL: '\n', 308 EOL: '\n',
228 }); 309 });
229 310
230 console.log(`✅ Successfully packaged and added ${recipeList.length} recipes (${unsuccessful} unsuccessful recipes)`); 311 console.log(
312 `✅ Successfully packaged and added ${recipeList.length} recipes (${unsuccessful} unsuccessful recipes)`,
313 );
231 314
232 if (unsuccessful > 0) { 315 if (unsuccessful > 0) {
233 process.exit(1); 316 process.exit(1);