diff options
Diffstat (limited to 'scripts/package.js')
-rw-r--r-- | scripts/package.js | 221 |
1 files changed, 152 insertions, 69 deletions
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.*/); | |||
13 | const repo = 'https://cdn.jsdelivr.net/gh/getferdi/recipes/recipes/'; | 13 | const 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 |
16 | const compress = (src, dest) => new Promise((resolve, reject) => { | 16 | const 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); |