diff options
Diffstat (limited to 'scripts/package.js')
-rw-r--r-- | scripts/package.js | 177 |
1 files changed, 173 insertions, 4 deletions
diff --git a/scripts/package.js b/scripts/package.js index d650121..82dfc81 100644 --- a/scripts/package.js +++ b/scripts/package.js | |||
@@ -1,8 +1,177 @@ | |||
1 | /** | 1 | /** |
2 | * Package recipe into tar.gz file | 2 | * Package all recipes |
3 | */ | 3 | */ |
4 | require('./api/require-depts')(); | 4 | const targz = require('targz'); |
5 | const fs = require('fs-extra'); | ||
6 | const path = require('path'); | ||
7 | const sizeOf = require('image-size'); | ||
5 | 8 | ||
6 | const packageRecipe = require('./api/package'); | 9 | // Publicly availible link to this repository's recipe folder |
10 | // Used for generating public icon URLs | ||
11 | const repo = 'https://cdn.jsdelivr.net/gh/getferdi/recipes/recipes/'; | ||
7 | 12 | ||
8 | packageRecipe(); \ No newline at end of file | 13 | // Helper: Compress src folder into dest file |
14 | const compress = (src, dest) => new Promise((resolve, reject) => { | ||
15 | targz.compress({ | ||
16 | src, | ||
17 | dest, | ||
18 | tar: { | ||
19 | // Don't package .DS_Store files | ||
20 | ignore: function(name) { | ||
21 | return path.basename(name) === '.DS_Store' | ||
22 | } | ||
23 | }, | ||
24 | }, (err) => { | ||
25 | if (err) { | ||
26 | reject(err); | ||
27 | } else { | ||
28 | resolve(dest); | ||
29 | } | ||
30 | }); | ||
31 | }); | ||
32 | |||
33 | // Let us work in an async environment | ||
34 | (async () => { | ||
35 | // Create paths to important files | ||
36 | const recipesFolder = path.join(__dirname, '../recipes'); | ||
37 | const outputFolder = path.join(__dirname, '../archives'); | ||
38 | const allJson = path.join(__dirname, '../all.json'); | ||
39 | const featuredFile = path.join(__dirname, '../featured.json'); | ||
40 | const featuredRecipes = await fs.readJSON(featuredFile); | ||
41 | let recipeList = []; | ||
42 | let unsuccessful = 0; | ||
43 | |||
44 | await fs.ensureDir(outputFolder); | ||
45 | await fs.emptyDir(outputFolder); | ||
46 | await fs.remove(allJson); | ||
47 | |||
48 | const availableRecipes = fs.readdirSync(recipesFolder, { withFileTypes: true }) | ||
49 | .filter(dir => dir.isDirectory()) | ||
50 | .map(dir => dir.name); | ||
51 | |||
52 | for(let recipe of availableRecipes) { | ||
53 | const recipeSrc = path.join(recipesFolder, recipe); | ||
54 | const packageJson = path.join(recipeSrc, 'package.json'); | ||
55 | const svgIcon = path.join(recipeSrc, 'icon.svg'); | ||
56 | const pngIcon = path.join(recipeSrc, 'icon.png'); | ||
57 | |||
58 | // Check that package.json exists | ||
59 | if (!await fs.pathExists(packageJson)) { | ||
60 | console.log(`⚠️ Couldn't package "${recipe}": Folder doesn't contain a "package.json".`); | ||
61 | unsuccessful++; | ||
62 | continue; | ||
63 | } | ||
64 | |||
65 | // Check that icons exist | ||
66 | const hasSvg = await fs.pathExists(svgIcon); | ||
67 | const hasPng = await fs.pathExists(pngIcon); | ||
68 | if (!hasSvg && !hasPng) { | ||
69 | console.log(`⚠️ Couldn't package "${recipe}": Recipe doesn't contain an icon SVG and PNG`); | ||
70 | unsuccessful++; | ||
71 | } else if (!hasSvg) { | ||
72 | console.log(`⚠️ Couldn't package "${recipe}": Recipe doesn't contain an icon SVG`); | ||
73 | unsuccessful++; | ||
74 | continue; | ||
75 | } else if (!hasPng) { | ||
76 | console.log(`⚠️ Couldn't package "${recipe}": Recipe doesn't contain an icon PNG`); | ||
77 | unsuccessful++; | ||
78 | continue; | ||
79 | } | ||
80 | |||
81 | // Check icons sizes | ||
82 | const svgSize = sizeOf(svgIcon); | ||
83 | const svgHasRightSize = svgSize.width === svgSize.height; | ||
84 | if (!svgHasRightSize) { | ||
85 | console.log(`⚠️ Couldn't package "${recipe}": Recipe SVG icon isn't a square`); | ||
86 | unsuccessful++; | ||
87 | continue; | ||
88 | } | ||
89 | |||
90 | const pngSize = sizeOf(pngIcon); | ||
91 | const pngHasRightSize = pngSize.width === 1024 && pngSize.height === 1024; | ||
92 | if (!pngHasRightSize) { | ||
93 | console.log(`⚠️ Couldn't package "${recipe}": Recipe PNG icon dimensions should be 1024x1024`); | ||
94 | unsuccessful++; | ||
95 | continue; | ||
96 | } | ||
97 | |||
98 | // Read package.json | ||
99 | const config = await fs.readJson(packageJson) | ||
100 | |||
101 | // Make sure it contains all required fields | ||
102 | if (!config) { | ||
103 | console.log(`⚠️ Couldn't package "${recipe}": Could not read or parse "package.json"`); | ||
104 | unsuccessful++; | ||
105 | continue; | ||
106 | } | ||
107 | let configErrors = []; | ||
108 | 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 (_)"); | ||
110 | } 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 (_)"); | ||
112 | } | ||
113 | if (!config.name) { | ||
114 | configErrors.push("The recipe's package.json contains no 'name' field. This field should contain the name of the service (e.g. 'Google Keep')"); | ||
115 | } | ||
116 | if (!config.version) { | ||
117 | 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')"); | ||
118 | } | ||
119 | if (!config.config || typeof config.config !== "object") { | ||
120 | configErrors.push("The recipe's package.json contains no 'config' object. This field should contain a configuration for your service."); | ||
121 | } | ||
122 | |||
123 | if (configErrors.length > 0) { | ||
124 | console.log(`⚠️ Couldn't package "${recipe}": There were errors in the recipe's package.json: | ||
125 | ${configErrors.reduce((str, err) => `${str}\n${err}`)}`); | ||
126 | unsuccessful++; | ||
127 | continue; | ||
128 | } | ||
129 | |||
130 | if (!await fs.exists(path.join(recipeSrc, 'webview.js'))) { | ||
131 | console.log(`⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "webview.js"`); | ||
132 | unsuccessful++; | ||
133 | continue; | ||
134 | } | ||
135 | if (!await fs.exists(path.join(recipeSrc, 'index.js'))) { | ||
136 | console.log(`⚠️ Couldn't package "${recipe}": The recipe doesn't contain a "index.js"`); | ||
137 | unsuccessful++; | ||
138 | continue; | ||
139 | } | ||
140 | |||
141 | // Package to .tar.gz | ||
142 | compress(recipeSrc, path.join(outputFolder, `${config.id}.tar.gz`)); | ||
143 | |||
144 | // Add recipe to all.json | ||
145 | const isFeatured = featuredRecipes.includes(config.id); | ||
146 | const packageInfo = { | ||
147 | "author": config.author || '', | ||
148 | "featured": isFeatured, | ||
149 | "id": config.id, | ||
150 | "name": config.name, | ||
151 | "version": config.version || '1.0.0', | ||
152 | "icons": { | ||
153 | "png": `${repo}${config.id}/icon.png`, | ||
154 | "svg": `${repo}${config.id}/icon.svg`, | ||
155 | }, | ||
156 | }; | ||
157 | recipeList.push(packageInfo); | ||
158 | } | ||
159 | |||
160 | |||
161 | // Sort package list alphabetically | ||
162 | recipeList = recipeList.sort((a, b) => { | ||
163 | var textA = a.id.toLowerCase(); | ||
164 | var textB = b.id.toLowerCase(); | ||
165 | return (textA < textB) ? -1 : (textA > textB) ? 1 : 0; | ||
166 | }); | ||
167 | await fs.writeJson(allJson, recipeList, { | ||
168 | spaces: 2, | ||
169 | EOL: '\n', | ||
170 | }); | ||
171 | |||
172 | console.log(`✅ Successfully packaged and added ${recipeList.length} recipes (${unsuccessful} unsuccessful recipes)`); | ||
173 | |||
174 | if (unsuccessful > 0) { | ||
175 | process.exit(1); | ||
176 | } | ||
177 | })(); | ||