@sanity/plugin-kit 0.0.1-studio-v3.1

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.
Files changed (194) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +398 -0
  3. package/assets/splat/LICENSE +21 -0
  4. package/assets/splat/editorconfig +13 -0
  5. package/assets/splat/eslint.config.js +5 -0
  6. package/assets/splat/gitignore +55 -0
  7. package/assets/splat/npmignore +9 -0
  8. package/assets/splat/prettierrc.js +6 -0
  9. package/assets/splat/sanity.json +8 -0
  10. package/assets/splat/template-tsconfig.json +23 -0
  11. package/assets/splat/v2-incompatible.js.template +11 -0
  12. package/lib/package.json +127 -0
  13. package/lib/src/actions/init.d.ts +65 -0
  14. package/lib/src/actions/init.js +83 -0
  15. package/lib/src/actions/init.js.map +1 -0
  16. package/lib/src/actions/link-watch.d.ts +3 -0
  17. package/lib/src/actions/link-watch.js +69 -0
  18. package/lib/src/actions/link-watch.js.map +1 -0
  19. package/lib/src/actions/splat.d.ts +26 -0
  20. package/lib/src/actions/splat.js +296 -0
  21. package/lib/src/actions/splat.js.map +1 -0
  22. package/lib/src/actions/verify/types.d.ts +77 -0
  23. package/lib/src/actions/verify/types.js +3 -0
  24. package/lib/src/actions/verify/types.js.map +1 -0
  25. package/lib/src/actions/verify/validations.d.ts +28 -0
  26. package/lib/src/actions/verify/validations.js +379 -0
  27. package/lib/src/actions/verify/validations.js.map +1 -0
  28. package/lib/src/actions/verify/verify-common.d.ts +43 -0
  29. package/lib/src/actions/verify/verify-common.js +88 -0
  30. package/lib/src/actions/verify/verify-common.js.map +1 -0
  31. package/lib/src/actions/verify-package.d.ts +5 -0
  32. package/lib/src/actions/verify-package.js +72 -0
  33. package/lib/src/actions/verify-package.js.map +1 -0
  34. package/lib/src/actions/verify-studio.d.ts +5 -0
  35. package/lib/src/actions/verify-studio.js +55 -0
  36. package/lib/src/actions/verify-studio.js.map +1 -0
  37. package/lib/src/actions/verify.d.ts +0 -0
  38. package/lib/src/actions/verify.js +330 -0
  39. package/lib/src/actions/verify.js.map +1 -0
  40. package/lib/src/cli.d.ts +2 -0
  41. package/lib/src/cli.js +86 -0
  42. package/lib/src/cli.js.map +1 -0
  43. package/lib/src/cmds/index.d.ts +8 -0
  44. package/lib/src/cmds/index.js +12 -0
  45. package/lib/src/cmds/index.js.map +1 -0
  46. package/lib/src/cmds/init.d.ts +4 -0
  47. package/lib/src/cmds/init.js +90 -0
  48. package/lib/src/cmds/init.js.map +1 -0
  49. package/lib/src/cmds/link-watch.d.ts +4 -0
  50. package/lib/src/cmds/link-watch.js +49 -0
  51. package/lib/src/cmds/link-watch.js.map +1 -0
  52. package/lib/src/cmds/splat.d.ts +4 -0
  53. package/lib/src/cmds/splat.js +63 -0
  54. package/lib/src/cmds/splat.js.map +1 -0
  55. package/lib/src/cmds/verify-package.d.ts +4 -0
  56. package/lib/src/cmds/verify-package.js +38 -0
  57. package/lib/src/cmds/verify-package.js.map +1 -0
  58. package/lib/src/cmds/verify-studio.d.ts +4 -0
  59. package/lib/src/cmds/verify-studio.js +38 -0
  60. package/lib/src/cmds/verify-studio.js.map +1 -0
  61. package/lib/src/cmds/verify.d.ts +0 -0
  62. package/lib/src/cmds/verify.js +42 -0
  63. package/lib/src/cmds/verify.js.map +1 -0
  64. package/lib/src/cmds/version.d.ts +4 -0
  65. package/lib/src/cmds/version.js +55 -0
  66. package/lib/src/cmds/version.js.map +1 -0
  67. package/lib/src/configs/buildExtensions.d.ts +1 -0
  68. package/lib/src/configs/buildExtensions.js +5 -0
  69. package/lib/src/configs/buildExtensions.js.map +1 -0
  70. package/lib/src/configs/default-source.d.ts +3 -0
  71. package/lib/src/configs/default-source.js +70 -0
  72. package/lib/src/configs/default-source.js.map +1 -0
  73. package/lib/src/configs/merged-packages.d.ts +1 -0
  74. package/lib/src/configs/merged-packages.js +24 -0
  75. package/lib/src/configs/merged-packages.js.map +1 -0
  76. package/lib/src/configs/uselessFiles.d.ts +1 -0
  77. package/lib/src/configs/uselessFiles.js +33 -0
  78. package/lib/src/configs/uselessFiles.js.map +1 -0
  79. package/lib/src/constants.d.ts +11 -0
  80. package/lib/src/constants.js +15 -0
  81. package/lib/src/constants.js.map +1 -0
  82. package/lib/src/dependencies/find.d.ts +0 -0
  83. package/lib/src/dependencies/find.js +195 -0
  84. package/lib/src/dependencies/find.js.map +1 -0
  85. package/lib/src/dependencies/import-linter.d.ts +3 -0
  86. package/lib/src/dependencies/import-linter.js +112 -0
  87. package/lib/src/dependencies/import-linter.js.map +1 -0
  88. package/lib/src/index.d.ts +2 -0
  89. package/lib/src/index.js +6 -0
  90. package/lib/src/index.js.map +1 -0
  91. package/lib/src/npm/manager.d.ts +7 -0
  92. package/lib/src/npm/manager.js +62 -0
  93. package/lib/src/npm/manager.js.map +1 -0
  94. package/lib/src/npm/package.d.ts +8 -0
  95. package/lib/src/npm/package.js +288 -0
  96. package/lib/src/npm/package.js.map +1 -0
  97. package/lib/src/npm/publish.d.ts +1 -0
  98. package/lib/src/npm/publish.js +14 -0
  99. package/lib/src/npm/publish.js.map +1 -0
  100. package/lib/src/npm/resolveLatestVersions.d.ts +3 -0
  101. package/lib/src/npm/resolveLatestVersions.js +35 -0
  102. package/lib/src/npm/resolveLatestVersions.js.map +1 -0
  103. package/lib/src/sanity/manifest.d.ts +48 -0
  104. package/lib/src/sanity/manifest.js +263 -0
  105. package/lib/src/sanity/manifest.js.map +1 -0
  106. package/lib/src/sharedFlags.d.ts +15 -0
  107. package/lib/src/sharedFlags.js +17 -0
  108. package/lib/src/sharedFlags.js.map +1 -0
  109. package/lib/src/util/command-parser.d.ts +9 -0
  110. package/lib/src/util/command-parser.js +41 -0
  111. package/lib/src/util/command-parser.js.map +1 -0
  112. package/lib/src/util/errorToUndefined.d.ts +1 -0
  113. package/lib/src/util/errorToUndefined.js +11 -0
  114. package/lib/src/util/errorToUndefined.js.map +1 -0
  115. package/lib/src/util/files.d.ts +36 -0
  116. package/lib/src/util/files.js +253 -0
  117. package/lib/src/util/files.js.map +1 -0
  118. package/lib/src/util/log.d.ts +14 -0
  119. package/lib/src/util/log.js +36 -0
  120. package/lib/src/util/log.js.map +1 -0
  121. package/lib/src/util/prompt.d.ts +13 -0
  122. package/lib/src/util/prompt.js +75 -0
  123. package/lib/src/util/prompt.js.map +1 -0
  124. package/lib/src/util/readme.d.ts +5 -0
  125. package/lib/src/util/readme.js +73 -0
  126. package/lib/src/util/readme.js.map +1 -0
  127. package/lib/src/util/request.d.ts +1 -0
  128. package/lib/src/util/request.js +19 -0
  129. package/lib/src/util/request.js.map +1 -0
  130. package/lib/src/util/user.d.ts +10 -0
  131. package/lib/src/util/user.js +106 -0
  132. package/lib/src/util/user.js.map +1 -0
  133. package/lib/test/cli.test.d.ts +1 -0
  134. package/lib/test/cli.test.js +64 -0
  135. package/lib/test/cli.test.js.map +1 -0
  136. package/lib/test/fixture-utils.d.ts +25 -0
  137. package/lib/test/fixture-utils.js +67 -0
  138. package/lib/test/fixture-utils.js.map +1 -0
  139. package/lib/test/init-verify-build.test.d.ts +1 -0
  140. package/lib/test/init-verify-build.test.js +75 -0
  141. package/lib/test/init-verify-build.test.js.map +1 -0
  142. package/lib/test/init.test.d.ts +1 -0
  143. package/lib/test/init.test.js +137 -0
  144. package/lib/test/init.test.js.map +1 -0
  145. package/lib/test/run-test-command.d.ts +1 -0
  146. package/lib/test/run-test-command.js +6 -0
  147. package/lib/test/run-test-command.js.map +1 -0
  148. package/lib/test/verify-package.test.d.ts +1 -0
  149. package/lib/test/verify-package.test.js +81 -0
  150. package/lib/test/verify-package.test.js.map +1 -0
  151. package/lib/test/version.test.d.ts +1 -0
  152. package/lib/test/version.test.js +48 -0
  153. package/lib/test/version.test.js.map +1 -0
  154. package/package.json +127 -0
  155. package/src/actions/init.ts +104 -0
  156. package/src/actions/link-watch.ts +74 -0
  157. package/src/actions/splat.ts +366 -0
  158. package/src/actions/verify/types.ts +84 -0
  159. package/src/actions/verify/validations.ts +401 -0
  160. package/src/actions/verify/verify-common.ts +92 -0
  161. package/src/actions/verify-package.ts +87 -0
  162. package/src/actions/verify-studio.ts +55 -0
  163. package/src/actions/verify.ts +328 -0
  164. package/src/cli.ts +77 -0
  165. package/src/cmds/index.ts +9 -0
  166. package/src/cmds/init.ts +85 -0
  167. package/src/cmds/link-watch.ts +51 -0
  168. package/src/cmds/splat.ts +59 -0
  169. package/src/cmds/verify-package.ts +36 -0
  170. package/src/cmds/verify-studio.ts +36 -0
  171. package/src/cmds/verify.ts +40 -0
  172. package/src/cmds/version.ts +67 -0
  173. package/src/configs/buildExtensions.ts +1 -0
  174. package/src/configs/default-source.ts +68 -0
  175. package/src/configs/merged-packages.ts +20 -0
  176. package/src/configs/uselessFiles.ts +29 -0
  177. package/src/constants.ts +13 -0
  178. package/src/dependencies/find.ts +193 -0
  179. package/src/dependencies/import-linter.ts +103 -0
  180. package/src/index.ts +4 -0
  181. package/src/npm/manager.ts +44 -0
  182. package/src/npm/package.ts +370 -0
  183. package/src/npm/publish.ts +9 -0
  184. package/src/npm/resolveLatestVersions.ts +26 -0
  185. package/src/sanity/manifest.ts +340 -0
  186. package/src/sharedFlags.ts +14 -0
  187. package/src/util/command-parser.ts +31 -0
  188. package/src/util/errorToUndefined.ts +7 -0
  189. package/src/util/files.ts +249 -0
  190. package/src/util/log.ts +44 -0
  191. package/src/util/prompt.ts +70 -0
  192. package/src/util/readme.ts +72 -0
  193. package/src/util/request.ts +13 -0
  194. package/src/util/user.ts +110 -0
@@ -0,0 +1,340 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import util from 'util'
4
+ import pkg from '../../package.json'
5
+ import {buildExtensions} from '../configs/buildExtensions'
6
+ import {hasSourceFile, hasCompiledFile, readJsonFile, fileExists} from '../util/files'
7
+ import {errorToUndefined} from '../util/errorToUndefined'
8
+
9
+ const stat = util.promisify(fs.stat)
10
+ const readFile = util.promisify(fs.readFile)
11
+
12
+ const allowedPartProps = ['name', 'implements', 'path', 'description']
13
+ const disallowedPluginProps = ['api', 'project', 'plugins', 'env']
14
+
15
+ export interface SanityV2Manifest {
16
+ root?: boolean
17
+ name: string
18
+ paths: ManifestPaths
19
+ parts?: {
20
+ path: string
21
+ }[]
22
+ }
23
+
24
+ export interface ManifestPaths {
25
+ basePath: string
26
+ compiled?: string
27
+ source?: string
28
+ }
29
+
30
+ export interface ManifestOptions {
31
+ isPlugin?: boolean
32
+ validate?: boolean
33
+ pluginName?: string
34
+ basePath: string
35
+ verifySourceParts?: boolean
36
+ verifyCompiledParts?: boolean
37
+ paths?: ManifestPaths
38
+ flags?: Record<string, any>
39
+ }
40
+
41
+ export async function getPaths(options: ManifestOptions) {
42
+ const {basePath} = options
43
+ const manifest = await readManifest(options)
44
+ if (!manifest.paths) {
45
+ return null
46
+ }
47
+
48
+ return absolutifyPaths(manifest.paths, basePath)
49
+ }
50
+
51
+ function absolutifyPaths(paths: ManifestPaths | undefined, basePath: string) {
52
+ const getPath = (relative?: string) =>
53
+ relative ? path.resolve(path.join(basePath, relative)) : undefined
54
+ return paths
55
+ ? {
56
+ basePath,
57
+ compiled: getPath(paths.compiled),
58
+ source: getPath(paths.source),
59
+ }
60
+ : {basePath}
61
+ }
62
+
63
+ export async function readManifest(options: ManifestOptions) {
64
+ const {basePath, validate = true} = options
65
+ const manifestPath = path.normalize(path.join(basePath, 'sanity.json'))
66
+
67
+ let content
68
+ try {
69
+ content = await readFile(manifestPath, 'utf8')
70
+ } catch (err: any) {
71
+ if (err.code === 'ENOENT') {
72
+ throw new Error(
73
+ `No sanity.json found. sanity.json is required for plugins to function. Use \`${pkg.binname} init\` for a new plugin, or create an empty \`sanity.json\` with an empty object (\`{}\`) for existing ones.`
74
+ )
75
+ }
76
+
77
+ throw new Error(`Failed to read "${manifestPath}": ${err.message}`)
78
+ }
79
+
80
+ let parsed
81
+ try {
82
+ parsed = JSON.parse(content)
83
+ } catch (err: any) {
84
+ throw new Error(`Error parsing "${manifestPath}": ${err.message}`)
85
+ }
86
+
87
+ if (validate) {
88
+ await validateManifest(parsed, options)
89
+ }
90
+
91
+ return parsed
92
+ }
93
+
94
+ export async function validateManifest(manifest: SanityV2Manifest, opts: ManifestOptions) {
95
+ const options = {isPlugin: true, ...opts}
96
+
97
+ if (!isObject(manifest)) {
98
+ throw new Error(`Invalid sanity.json: Root must be an object`)
99
+ }
100
+
101
+ if (options.isPlugin) {
102
+ await validatePluginManifest(manifest, options)
103
+ } else {
104
+ await validateProjectManifest(manifest)
105
+ }
106
+
107
+ if ('root' in manifest && typeof manifest.root !== 'boolean') {
108
+ throw new Error(`Invalid sanity.json: "root" property must be a boolean if declared`)
109
+ }
110
+
111
+ await validateParts(manifest, {
112
+ ...options,
113
+ paths: absolutifyPaths(manifest.paths, options.basePath),
114
+ })
115
+ }
116
+
117
+ function validateProjectManifest(manifest: SanityV2Manifest) {
118
+ if ('paths' in manifest) {
119
+ throw new Error(`Invalid sanity.json: "paths" property has no meaning in a project manifest`)
120
+ }
121
+ }
122
+
123
+ export async function validatePluginManifest(
124
+ manifest: SanityV2Manifest,
125
+ options: {basePath: string}
126
+ ) {
127
+ const disallowed = Object.keys(manifest)
128
+ .filter((key) => disallowedPluginProps.includes(key))
129
+ .map((key) => `"${key}"`)
130
+
131
+ if (disallowed.length > 0) {
132
+ const plural = disallowed.length > 1 ? 's' : ''
133
+ const joined = disallowed.join(', ')
134
+ throw new Error(
135
+ `Invalid sanity.json: Key${plural} ${joined} ${
136
+ plural ? 'are' : 'is'
137
+ } not allowed in a plugin manifest`
138
+ )
139
+ }
140
+
141
+ if (manifest.root) {
142
+ throw new Error(`Invalid sanity.json: "root" cannot be truthy in a plugin manifest`)
143
+ }
144
+
145
+ await validatePaths(manifest, options)
146
+ }
147
+
148
+ export async function validatePaths(manifest: SanityV2Manifest, options: {basePath: string}) {
149
+ if (!('paths' in manifest)) {
150
+ return
151
+ }
152
+
153
+ if (!isObject(manifest.paths)) {
154
+ throw new Error(`Invalid sanity.json: "paths" must be an object if declared`)
155
+ }
156
+
157
+ if (typeof manifest.paths.compiled !== 'string') {
158
+ throw new Error(
159
+ `Invalid sanity.json: "paths" must have a (string) "compiled" property if declared`
160
+ )
161
+ }
162
+
163
+ if (typeof manifest.paths.source !== 'string') {
164
+ throw new Error(
165
+ `Invalid sanity.json: "paths" must have a (string) "source" property if declared`
166
+ )
167
+ }
168
+
169
+ const sourcePath = path.resolve(options.basePath, manifest.paths.source)
170
+ let srcStats
171
+ try {
172
+ srcStats = await stat(sourcePath)
173
+ } catch (err: any) {
174
+ if (err.code === 'ENOENT') {
175
+ throw new Error(`sanity.json references "source" path which does not exist: "${sourcePath}"`)
176
+ }
177
+ }
178
+
179
+ if (!srcStats?.isDirectory()) {
180
+ throw new Error(
181
+ `sanity.json references "source" path which is not a directory: "${sourcePath}"`
182
+ )
183
+ }
184
+ }
185
+
186
+ async function validateParts(manifest: SanityV2Manifest, options: ManifestOptions) {
187
+ if (!('parts' in manifest)) {
188
+ return
189
+ }
190
+
191
+ if (!Array.isArray(manifest.parts)) {
192
+ throw new Error(`Invalid sanity.json: "parts" must be an array if declared`)
193
+ }
194
+
195
+ let i = 0
196
+ for (const part of manifest.parts) {
197
+ await validatePart(part, i, options)
198
+ i++
199
+ }
200
+ }
201
+
202
+ async function validatePart(part: Record<string, any>, index: number, options: ManifestOptions) {
203
+ if (!isObject(part)) {
204
+ throw new Error(`Invalid sanity.json: "parts[${index}]" must be an object`)
205
+ }
206
+
207
+ validateAllowedPartKeys(part, index)
208
+ validatePartStringValues(part, index)
209
+ validatePartNames(part, index, options)
210
+ await validatePartFiles(part, index, options)
211
+ }
212
+
213
+ async function validatePartFiles(
214
+ part: {path?: string} | undefined,
215
+ index: number,
216
+ options: ManifestOptions
217
+ ) {
218
+ const {verifyCompiledParts, verifySourceParts, paths} = options
219
+ if (!part?.path) {
220
+ return
221
+ }
222
+
223
+ const ext = path.extname(part.path)
224
+ if (paths?.source && ext && ext !== '.js' && buildExtensions.includes(ext)) {
225
+ throw new Error(
226
+ `Invalid sanity.json: Part path has extension which is not applicable after compiling. ${ext} becomes .js after compiling. Specify filename without extension (${path.basename(
227
+ part.path
228
+ )}) (parts[${index}])`
229
+ )
230
+ }
231
+
232
+ if (!verifySourceParts && !verifyCompiledParts) {
233
+ return
234
+ }
235
+
236
+ const [srcExists, libExists] = await Promise.all([
237
+ hasSourceFile(part.path, paths),
238
+ verifyCompiledParts && hasCompiledFile(part.path, paths),
239
+ ])
240
+
241
+ if (!srcExists) {
242
+ throw new Error(
243
+ `Invalid sanity.json: Part path references file that does not exist in source directory (${
244
+ paths?.source || paths?.basePath
245
+ }) (parts[${index}])`
246
+ )
247
+ }
248
+
249
+ if (verifyCompiledParts && !libExists) {
250
+ throw new Error(
251
+ `Invalid sanity.json: Part path references file ("${part.path}") that does not exist in compiled directory (${paths?.compiled}) (parts[${index}])`
252
+ )
253
+ }
254
+ }
255
+
256
+ function validatePartNames(
257
+ part: {name?: string; implements?: string} | undefined,
258
+ index: number,
259
+ options: ManifestOptions
260
+ ) {
261
+ const pluginName = options.pluginName ? options.pluginName.replace(/^sanity-plugin-/, '') : ''
262
+ if (!part?.name || !part?.name?.startsWith(`part:${pluginName}/`)) {
263
+ throw new Error(
264
+ `Invalid sanity.json: "name" must be prefixed with "part:${pluginName}/" - got "${part?.name}" (parts[${index}])`
265
+ )
266
+ }
267
+
268
+ if (!part?.implements?.startsWith('part:')) {
269
+ throw new Error(
270
+ `Invalid sanity.json: "implements" must be prefixed with "part:" - got "${part?.implements}" (parts[${index}])`
271
+ )
272
+ }
273
+ }
274
+
275
+ function validateAllowedPartKeys(part: Record<string, any>, index: number) {
276
+ const disallowed = Object.keys(part)
277
+ .filter((key) => !allowedPartProps.includes(key))
278
+ .map((key) => `"${key}"`)
279
+
280
+ if (disallowed.length > 0) {
281
+ const plural = disallowed.length > 1 ? 's' : ''
282
+ const joined = disallowed.join(', ')
283
+ throw new Error(
284
+ `Invalid sanity.json: Key${plural} ${joined} ${
285
+ plural ? 'are' : 'is'
286
+ } not allowed in a part declaration (parts[${index}])`
287
+ )
288
+ }
289
+ }
290
+
291
+ function validatePartStringValues(part: Record<string, any>, index: number) {
292
+ const nonStrings = Object.keys(part)
293
+ .filter((key) => typeof part[key] !== 'string')
294
+ .map((key) => `"${key}"`)
295
+
296
+ if (nonStrings.length > 0) {
297
+ const plural = nonStrings.length > 1 ? 's' : ''
298
+ const joined = nonStrings.join(', ')
299
+ throw new Error(
300
+ `Invalid sanity.json: Key${plural} ${joined} should be of type string (parts[${index}])`
301
+ )
302
+ }
303
+ }
304
+
305
+ function isObject(obj: any) {
306
+ return !Array.isArray(obj) && obj !== null && typeof obj === 'object'
307
+ }
308
+
309
+ export function getReferencesPartPaths(manifest: SanityV2Manifest, basePath: string) {
310
+ const {paths, parts = []} = manifest
311
+ const compiledPath = path.resolve(basePath, paths?.compiled || '')
312
+
313
+ return parts
314
+ .filter((part) => part.path)
315
+ .map((part) => part.path)
316
+ .map((partPath) => (path.extname(partPath) === '' ? `${partPath}.js` : partPath))
317
+ .map((partPath) =>
318
+ path.isAbsolute(partPath)
319
+ ? partPath // Not sure if this ever happens, but :shrugs:
320
+ : path.resolve(compiledPath, partPath)
321
+ )
322
+ }
323
+
324
+ export async function hasSanityJson(basePath: string) {
325
+ const file = await readJsonFile<{root?: boolean}>(path.join(basePath, 'sanity.json')).catch(
326
+ errorToUndefined
327
+ )
328
+ return {exists: Boolean(file), isRoot: Boolean(file && file.root)}
329
+ }
330
+
331
+ export async function findStudioV3Config(basePath: string) {
332
+ const jsFile = 'sanity.config.js'
333
+ const jsExists = await fileExists(path.join(basePath, jsFile))
334
+ if (jsExists) {
335
+ return {v3ConfigFile: jsFile}
336
+ }
337
+ const tsFile = 'sanity.config.ts'
338
+ const tsExists = await fileExists(path.join(basePath, tsFile))
339
+ return {v3ConfigFile: tsExists ? tsFile : undefined}
340
+ }
@@ -0,0 +1,14 @@
1
+ export default {
2
+ debug: {
3
+ default: false,
4
+ type: 'boolean',
5
+ },
6
+ silent: {
7
+ type: 'boolean',
8
+ default: false,
9
+ },
10
+ verbose: {
11
+ type: 'boolean',
12
+ default: false,
13
+ },
14
+ } as const
@@ -0,0 +1,31 @@
1
+ import childProcess from 'child_process'
2
+ import npmRunPath from 'npm-run-path'
3
+ import log from './log'
4
+
5
+ interface Command {
6
+ command: string
7
+ args: string[]
8
+ }
9
+
10
+ export function parseCommand(commandString: string): Command {
11
+ const normalized = commandString.replace(/ +/g, ' ')
12
+ const commandAndArg = normalized.split(' ')
13
+ return {
14
+ command: commandAndArg[0],
15
+ args: commandAndArg.length > 1 ? commandAndArg.slice(1) : [],
16
+ }
17
+ }
18
+
19
+ export async function runCommand(commandString: string): Promise<{code: number}> {
20
+ log.info(`Running command: ${commandString}`)
21
+ const {command, args} = parseCommand(commandString)
22
+
23
+ let options: any = {stdio: 'inherit', env: npmRunPath.env()}
24
+
25
+ // ref: https://stackoverflow.com/questions/37459717/error-spawn-enoent-on-windows/37487465
26
+ options = process.platform === 'win32' ? {...options, shell: true} : options
27
+
28
+ return new Promise((resolve, reject) => {
29
+ childProcess.spawn(command, args, options).on('error', reject).on('close', resolve)
30
+ })
31
+ }
@@ -0,0 +1,7 @@
1
+ export function errorToUndefined(err: any) {
2
+ if (err instanceof TypeError) {
3
+ throw err
4
+ }
5
+
6
+ return undefined
7
+ }
@@ -0,0 +1,249 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import util from 'util'
4
+ import pAny from 'p-any'
5
+ import crypto from 'crypto'
6
+ import {buildExtensions} from '../configs/buildExtensions'
7
+ import {prompt} from './prompt'
8
+ import {InitFlags} from '../actions/init'
9
+ import log from './log'
10
+ import json5 from 'json5'
11
+ import {ManifestPaths} from '../sanity/manifest'
12
+
13
+ export const stat = util.promisify(fs.stat)
14
+ export const mkdir = util.promisify(fs.mkdir)
15
+ export const readdir = util.promisify(fs.readdir)
16
+ export const copyFile = util.promisify(fs.copyFile)
17
+ export const readFile = util.promisify(fs.readFile)
18
+ export const writeFile = util.promisify(fs.writeFile)
19
+
20
+ export function hasSourceEquivalent(compiledFile: string, paths: ManifestPaths) {
21
+ if (!paths.source) {
22
+ return fileExists(
23
+ path.isAbsolute(compiledFile) ? compiledFile : path.resolve(paths.basePath, compiledFile)
24
+ )
25
+ }
26
+
27
+ // /plugin/lib/MyComponent.js => /plugin/src
28
+ const baseDir = path.dirname(compiledFile.replace(paths.compiled as string, paths.source))
29
+
30
+ // /plugin/lib/MyComponent.js => MyComponent
31
+ const baseName = path.basename(compiledFile, path.extname(compiledFile))
32
+
33
+ // MyComponent => /plugin/src/MyComponent
34
+ const pathStub = path.join(baseDir, baseName)
35
+
36
+ /*
37
+ * /plugin/src/MyComponent => [
38
+ * /plugin/src/MyComponent.jsx,
39
+ * /plugin/src/MyComponent.mjs,
40
+ * ...
41
+ * ]
42
+ */
43
+ return buildCandidateExists(pathStub)
44
+ }
45
+
46
+ // Generally used for parts resolving
47
+ export async function hasSourceFile(filePath: string, paths?: ManifestPaths) {
48
+ if (!paths?.source) {
49
+ return fileExists(
50
+ path.isAbsolute(filePath) ? filePath : path.resolve(paths?.basePath ?? '', filePath)
51
+ )
52
+ }
53
+
54
+ // filePath: components/SomeInput
55
+ // paths: {source: '/plugin/src'}
56
+ // MyComponent => /plugin/src/MyComponent
57
+ const pathStub = path.isAbsolute(filePath) ? filePath : path.resolve(paths.source, filePath)
58
+
59
+ if (await fileExists(pathStub)) {
60
+ return true
61
+ }
62
+
63
+ return buildCandidateExists(pathStub)
64
+ }
65
+
66
+ // Generally used for parts resolving
67
+ export function hasCompiledFile(filePath: string, paths?: ManifestPaths) {
68
+ if (!paths?.compiled) {
69
+ return fileExists(
70
+ path.isAbsolute(filePath) ? filePath : path.resolve(paths?.basePath ?? '', filePath)
71
+ )
72
+ }
73
+
74
+ // filePath: components/SomeInput
75
+ // paths: {compiled: '/plugin/lib'}
76
+
77
+ // components/SomeInput => /plugin/lib/components/SomeInput
78
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(paths.compiled, filePath)
79
+
80
+ // /plugin/lib/components/SomeInput => /plugin/lib/components/SomeInput.js
81
+ // /plugin/lib/components/SomeInput.js => /plugin/lib/components/SomeInput.js
82
+ // /plugin/lib/components/SomeInput.css => /plugin/lib/components/SomeInput.css
83
+ const fileExt = path.extname(absPath)
84
+ const withExt = fileExt === '' ? `${absPath}.js` : absPath
85
+
86
+ return fileExists(withExt)
87
+ }
88
+
89
+ export function buildCandidateExists(pathStub: string) {
90
+ const candidates = buildExtensions.map((extCandidate) => `${pathStub}${extCandidate}`)
91
+
92
+ return pAny(candidates.map((candidate) => stat(candidate)))
93
+ .then(() => true)
94
+ .catch(() => false)
95
+ }
96
+
97
+ export function fileExists(filePath: string) {
98
+ return stat(filePath)
99
+ .then(() => true)
100
+ .catch(() => false)
101
+ }
102
+
103
+ export async function readJsonFile<T>(filePath: string) {
104
+ const content = await readFile(filePath, 'utf8')
105
+ return JSON.parse(content) as T
106
+ }
107
+
108
+ export function writeJsonFile(filePath: string, content: Record<string, unknown>) {
109
+ const data = JSON.stringify(content, null, 2)
110
+ return writeFile(filePath, data, {encoding: 'utf8'})
111
+ }
112
+
113
+ export async function writeFileWithOverwritePrompt(
114
+ filePath: string,
115
+ content: string,
116
+ options: {default?: any; force?: boolean} & fs.ObjectEncodingOptions
117
+ ) {
118
+ const {default: defaultVal, force = false, ...writeOptions} = options
119
+ const withinCwd = filePath.startsWith(process.cwd())
120
+ const printablePath = withinCwd ? path.relative(process.cwd(), filePath) : filePath
121
+
122
+ if (await fileEqualsData(filePath, content)) {
123
+ return false
124
+ }
125
+
126
+ if (
127
+ !force &&
128
+ (await fileExists(filePath)) &&
129
+ !(await prompt(`File "${printablePath}" already exists. Overwrite?`, {
130
+ type: 'confirm',
131
+ default: defaultVal,
132
+ }))
133
+ ) {
134
+ return false
135
+ }
136
+
137
+ await writeFile(filePath, content, writeOptions)
138
+ return true
139
+ }
140
+
141
+ export async function copyFileWithOverwritePrompt(from: string, to: string, flags: InitFlags) {
142
+ const withinCwd = to.startsWith(process.cwd())
143
+ const printablePath = withinCwd ? path.relative(process.cwd(), to) : to
144
+
145
+ if (await filesAreEqual(from, to)) {
146
+ return false
147
+ }
148
+
149
+ if (
150
+ !flags.force &&
151
+ (await fileExists(to)) &&
152
+ !(await prompt(`File "${printablePath}" already exists. Overwrite?`, {
153
+ type: 'confirm',
154
+ default: false,
155
+ }))
156
+ ) {
157
+ return false
158
+ }
159
+
160
+ await copyFile(from, to)
161
+ return true
162
+ }
163
+
164
+ export async function fileEqualsData(filePath: string, content: string) {
165
+ const contentHash = crypto.createHash('sha1').update(content).digest('hex')
166
+ const remoteHash = await getFileHash(filePath)
167
+ return contentHash === remoteHash
168
+ }
169
+
170
+ export async function filesAreEqual(file1: string, file2: string) {
171
+ const [hash1, hash2] = await Promise.all([getFileHash(file1, false), getFileHash(file2)])
172
+ return hash1 === hash2
173
+ }
174
+
175
+ export function getFileHash(filePath: string, allowMissing = true) {
176
+ return new Promise((resolve, reject) => {
177
+ const hash = crypto.createHash('sha1')
178
+ const stream = fs.createReadStream(filePath)
179
+ stream.on('error', (err) => {
180
+ if ((err as unknown as {code?: string}).code === 'ENOENT' && allowMissing) {
181
+ resolve(null)
182
+ } else {
183
+ reject(err)
184
+ }
185
+ })
186
+
187
+ stream.on('end', () => resolve(hash.digest('hex')))
188
+ stream.on('data', (chunk) => hash.update(chunk))
189
+ })
190
+ }
191
+
192
+ export async function ensureDir(dirPath: string) {
193
+ try {
194
+ await mkdir(dirPath)
195
+ } catch (err) {
196
+ if ((err as unknown as {code?: string}).code !== 'EEXIST') {
197
+ throw err
198
+ }
199
+ }
200
+ }
201
+
202
+ export async function isEmptyish(dirPath: string) {
203
+ const ignoredFiles = ['.git', '.gitignore', 'license', 'readme.md']
204
+ const allFiles = await readdir(dirPath).catch(() => [])
205
+ const files = allFiles.filter((file) => !ignoredFiles.includes(file.toLowerCase()))
206
+ return files.length === 0
207
+ }
208
+
209
+ export async function readFileContent({
210
+ filename,
211
+ basePath,
212
+ }: {
213
+ filename: string
214
+ basePath: string
215
+ }): Promise<string | undefined> {
216
+ const filepath = path.normalize(path.join(basePath, filename))
217
+ try {
218
+ return await readFile(filepath, 'utf8')
219
+ } catch (err: any) {
220
+ if (err.code === 'ENOENT') {
221
+ log.debug(`No ${filename} file found.`)
222
+ return undefined
223
+ }
224
+ throw new Error(`Failed to read "${filepath}": ${err.message}`)
225
+ }
226
+ }
227
+
228
+ export async function readJson5File<T>({
229
+ filename,
230
+ basePath,
231
+ }: {
232
+ filename: string
233
+ basePath: string
234
+ }): Promise<T | undefined> {
235
+ const content = await readFileContent({filename, basePath})
236
+ if (!content) {
237
+ return undefined
238
+ }
239
+
240
+ return parseJson5<T>(content, filename)
241
+ }
242
+
243
+ export function parseJson5<T>(content: string, errorKey: string): T {
244
+ try {
245
+ return json5.parse<T>(content)
246
+ } catch (err: any) {
247
+ throw new Error(`Error parsing "${errorKey}": ${err.message}`)
248
+ }
249
+ }
@@ -0,0 +1,44 @@
1
+ // Note: This is _specifically_ meant for CLI usage,
2
+ // I realize that "singletons" are bad.
3
+
4
+ import chalk from 'chalk'
5
+
6
+ let beQuiet = false
7
+ let beVerbose = false
8
+
9
+ function setVerbosity({verbose, silent}: {verbose: boolean; silent: boolean}) {
10
+ if (silent) {
11
+ beVerbose = false
12
+ beQuiet = true
13
+ } else if (verbose) {
14
+ beVerbose = true
15
+ beQuiet = false
16
+ }
17
+ }
18
+
19
+ export default {
20
+ setVerbosity: setVerbosity,
21
+
22
+ // Bypasses any checks, prints regardless (only use for things like `cli --version`)
23
+ msg: (msg: any, ...args: any[]) => !beQuiet && console.log(msg, ...args),
24
+
25
+ // Debug only printed on --verbose
26
+ debug: (msg: any, ...args: any[]) =>
27
+ !beQuiet && beVerbose && console.debug(`${chalk.bgBlack.white('[debug]')} ${msg}`, ...args),
28
+
29
+ // Success messages only printed if not --silent
30
+ success: (msg: any, ...args: any[]) =>
31
+ !beQuiet && console.info(`${chalk.bgBlack.greenBright('[success]')} ${msg}`, ...args),
32
+
33
+ // Info only printed if not --silent ("standard" level)
34
+ info: (msg: any, ...args: any[]) =>
35
+ !beQuiet && console.info(`${chalk.bgBlack.cyanBright('[info]')} ${msg}`, ...args),
36
+
37
+ // Warning only printed if not --silent
38
+ warn: (msg: any, ...args: any[]) =>
39
+ !beQuiet && console.warn(`${chalk.bgBlack.yellowBright('[warn]')} ${msg}`, ...args),
40
+
41
+ // Errors are always printed
42
+ error: (msg: any, ...args: any[]) =>
43
+ console.error(`${chalk.bgBlack.redBright('[error]')} ${msg}`, ...args),
44
+ }