@react-native-reusables/cli 0.6.2 → 0.7.0

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.
@@ -0,0 +1,295 @@
1
+ import { CliOptions } from "@cli/contexts/cli-options.js"
2
+ import { retryWith } from "@cli/utils/retry-with.js"
3
+ import { Prompt } from "@effect/cli"
4
+ import { FileSystem, Path } from "@effect/platform"
5
+ import { Effect, Schema } from "effect"
6
+ import { Git } from "./git.js"
7
+ import { type ConfigLoaderSuccessResult, createMatchPath, loadConfig as loadTypescriptConfig } from "tsconfig-paths"
8
+ import { PROJECT_MANIFEST } from "@cli/project-manifest.js"
9
+
10
+ const componentJsonSchema = Schema.Struct({
11
+ $schema: Schema.optional(Schema.String),
12
+ style: Schema.String,
13
+ rsc: Schema.Boolean,
14
+ tsx: Schema.Boolean,
15
+ tailwind: Schema.Struct({
16
+ config: Schema.optional(Schema.String),
17
+ css: Schema.String,
18
+ baseColor: Schema.String,
19
+ cssVariables: Schema.Boolean,
20
+ prefix: Schema.optional(Schema.String)
21
+ }),
22
+ aliases: Schema.Struct({
23
+ components: Schema.String,
24
+ utils: Schema.String,
25
+ ui: Schema.optional(Schema.String),
26
+ lib: Schema.optional(Schema.String),
27
+ hooks: Schema.optional(Schema.String)
28
+ }),
29
+ iconLibrary: Schema.optional(Schema.String)
30
+ })
31
+
32
+ const supportedExtensions = [".ts", ".tsx", ".jsx", ".js", ".css"]
33
+
34
+ class ProjectConfig extends Effect.Service<ProjectConfig>()("ProjectConfig", {
35
+ dependencies: [Git.Default],
36
+ effect: Effect.gen(function* () {
37
+ const fs = yield* FileSystem.FileSystem
38
+ const path = yield* Path.Path
39
+ const options = yield* CliOptions
40
+ const git = yield* Git
41
+
42
+ let componentJsonConfig: typeof componentJsonSchema.Type | null = null
43
+ let tsConfig: ConfigLoaderSuccessResult | null = null
44
+
45
+ const getComponentJson = () =>
46
+ Effect.gen(function* () {
47
+ if (componentJsonConfig) {
48
+ return componentJsonConfig
49
+ }
50
+
51
+ const componentJsonExists = yield* fs.exists(path.join(options.cwd, "components.json"))
52
+ if (!componentJsonExists) {
53
+ return yield* handleInvalidComponentJson(false)
54
+ }
55
+ const config = yield* fs.readFileString(path.join(options.cwd, "components.json")).pipe(
56
+ Effect.flatMap(Schema.decodeUnknown(Schema.parseJson())),
57
+ Effect.flatMap(Schema.decodeUnknown(componentJsonSchema)),
58
+ Effect.catchTags({
59
+ ParseError: () => handleInvalidComponentJson(true)
60
+ })
61
+ )
62
+
63
+ componentJsonConfig = config
64
+
65
+ yield* Effect.logDebug(`componentJsonConfig: ${JSON.stringify(componentJsonConfig, null, 2)}`)
66
+ return config
67
+ })
68
+
69
+ const getUniwindDtsPath = () =>
70
+ Effect.gen(function* () {
71
+ const metroConfigPaths = ["metro.config.js", "metro.config.ts"].map((p) => path.join(options.cwd, p)) as [
72
+ string,
73
+ ...Array<string>
74
+ ]
75
+
76
+ const metroContent = yield* retryWith((filePath: string) => fs.readFileString(filePath), metroConfigPaths).pipe(
77
+ Effect.catchAll(() => Effect.succeed(null))
78
+ )
79
+
80
+ if (!metroContent?.includes("withUniwindConfig")) {
81
+ return null
82
+ }
83
+
84
+ const dtsFileMatch = metroContent.match(/dtsFile\s*:\s*["']([^"']+)["']/)
85
+ if (dtsFileMatch?.[1]) {
86
+ const dtsPath = dtsFileMatch[1].replace(/^\.\//, "")
87
+ return path.join(options.cwd, dtsPath)
88
+ }
89
+
90
+ return path.join(options.cwd, PROJECT_MANIFEST.uniwindTypesFile)
91
+ })
92
+
93
+ const getStylingLibrary = () =>
94
+ Effect.gen(function* () {
95
+ if (options.stylingLibrary) {
96
+ return options.stylingLibrary
97
+ }
98
+
99
+ const metroConfigPaths = ["metro.config.js", "metro.config.ts"].map((p) => path.join(options.cwd, p)) as [
100
+ string,
101
+ ...Array<string>
102
+ ]
103
+
104
+ const metroContent = yield* retryWith((filePath: string) => fs.readFileString(filePath), metroConfigPaths).pipe(
105
+ Effect.catchAll(() => Effect.succeed(null))
106
+ )
107
+
108
+ if (metroContent?.includes("withUniwindConfig")) {
109
+ return "uniwind" as const
110
+ }
111
+
112
+ // v4 uses withNativeWind
113
+ // v5 uses withNativewind
114
+ if (metroContent?.toLowerCase().includes("withnativewind")) {
115
+ return "nativewind" as const
116
+ }
117
+
118
+ return "unknown" as const
119
+ })
120
+
121
+ const handleInvalidComponentJson = (exists: boolean) =>
122
+ Effect.gen(function* () {
123
+ yield* Effect.logWarning(
124
+ `${exists ? "Invalid components.json" : "Missing components.json"}${" (required to continue)"}`
125
+ )
126
+ const agreeToWrite = yield* Prompt.confirm({
127
+ message: `Would you like to ${exists ? "update the" : "write a"} components.json file?`,
128
+ label: { confirm: "y", deny: "n" },
129
+ initial: true,
130
+ placeholder: { defaultConfirm: "y/n" }
131
+ })
132
+ if (!agreeToWrite) {
133
+ return yield* Effect.fail(new Error("Unable to continue without a valid components.json file."))
134
+ }
135
+
136
+ const baseColor = options.yes
137
+ ? "neutral"
138
+ : yield* Prompt.select({
139
+ message: "Which color would you like to use as the base color?",
140
+ choices: [
141
+ { title: "neutral", value: "neutral" },
142
+ { title: "stone", value: "stone" },
143
+ { title: "zinc", value: "zinc" },
144
+ { title: "gray", value: "gray" },
145
+ { title: "slate", value: "slate" }
146
+ ] as const
147
+ })
148
+
149
+ const hasRootGlobalCss = yield* fs.exists(path.join(options.cwd, "global.css"))
150
+
151
+ const hasSrcGlobalCss = hasRootGlobalCss ? false : yield* fs.exists(path.join(options.cwd, "src/global.css"))
152
+
153
+ const detectedCss = hasRootGlobalCss ? "global.css" : hasSrcGlobalCss ? "src/global.css" : ""
154
+
155
+ const css =
156
+ options.yes && detectedCss
157
+ ? detectedCss
158
+ : yield* Prompt.text({
159
+ message: "What is the name of the CSS file and path to it? (e.g. global.css or src/global.css)",
160
+ default: detectedCss
161
+ })
162
+
163
+ const stylingLibrary = yield* getStylingLibrary()
164
+
165
+ const hasTailwindConfig = yield* fs.exists(path.join(options.cwd, "tailwind.config.js"))
166
+ const tailwindConfig = stylingLibrary === "uniwind" ? "" :
167
+ options.yes && hasTailwindConfig
168
+ ? "tailwind.config.js"
169
+ : yield* Prompt.text({
170
+ message:
171
+ "What is the name of the Tailwind config file and path to it? (e.g. tailwind.config.js or src/tailwind.config.js)",
172
+ default: "tailwind.config.js"
173
+ })
174
+
175
+ const tsConfig = yield* getTsConfig()
176
+
177
+ const aliasSymbol = `${(Object.keys(tsConfig.paths ?? {})[0] ?? "@/*").split("/*")[0]}`
178
+
179
+ const detectedAliases = {
180
+ components: `${aliasSymbol}/components`,
181
+ utils: `${aliasSymbol}/lib/utils`,
182
+ ui: `${aliasSymbol}/components/ui`,
183
+ lib: `${aliasSymbol}/lib`,
184
+ hooks: `${aliasSymbol}/hooks`
185
+ }
186
+
187
+ let aliases = detectedAliases
188
+
189
+ if (!options.yes) {
190
+ const useDetectedAliases = yield* Prompt.confirm({
191
+ message: `Use detected alias (${aliasSymbol}/*) in your setup?`,
192
+ initial: true
193
+ })
194
+
195
+ if (!useDetectedAliases) {
196
+ const [componentsAlias, utilsAlias, uiAlias, libAlias, hooksAlias] = yield* Prompt.all([
197
+ Prompt.text({
198
+ message: "What is the name of the components alias?",
199
+ default: detectedAliases.components
200
+ }),
201
+ Prompt.text({
202
+ message: "What is the name of the utils alias?",
203
+ default: detectedAliases.utils
204
+ }),
205
+ Prompt.text({
206
+ message: "What is the name of the ui alias?",
207
+ default: detectedAliases.ui
208
+ }),
209
+ Prompt.text({
210
+ message: "What is the name of the lib alias?",
211
+ default: detectedAliases.lib
212
+ }),
213
+ Prompt.text({
214
+ message: "What is the name of the hooks alias?",
215
+ default: detectedAliases.hooks
216
+ })
217
+ ])
218
+
219
+ aliases = {
220
+ components: componentsAlias,
221
+ utils: utilsAlias,
222
+ ui: uiAlias,
223
+ lib: libAlias,
224
+ hooks: hooksAlias
225
+ }
226
+ }
227
+ }
228
+
229
+ const newComponentJson = yield* Schema.encode(componentJsonSchema)({
230
+ $schema: "https://ui.shadcn.com/schema.json",
231
+ style: "new-york",
232
+ aliases,
233
+ rsc: false,
234
+ tsx: true,
235
+ tailwind: {
236
+ css,
237
+ baseColor,
238
+ cssVariables: true,
239
+ config: tailwindConfig
240
+ }
241
+ })
242
+
243
+ yield* git.promptIfDirty()
244
+ yield* fs.writeFileString(path.join(options.cwd, "components.json"), JSON.stringify(newComponentJson, null, 2))
245
+ yield* Effect.logDebug(`newComponentJson: ${JSON.stringify(newComponentJson, null, 2)}`)
246
+ return newComponentJson
247
+ })
248
+
249
+ const getTsConfig = () =>
250
+ Effect.try({
251
+ try: () => {
252
+ if (tsConfig) {
253
+ return tsConfig
254
+ }
255
+ const configResult = loadTypescriptConfig(options.cwd)
256
+ if (configResult.resultType === "failed") {
257
+ throw new Error("Error loading tsconfig.json", { cause: configResult.message })
258
+ }
259
+ tsConfig = configResult
260
+ return configResult
261
+ },
262
+ catch: (error) => new Error("Error loading {ts,js}config.json", { cause: String(error) })
263
+ })
264
+
265
+ const resolvePathFromAlias = (aliasPath: string) =>
266
+ Effect.gen(function* () {
267
+ const config = yield* getTsConfig()
268
+ return yield* Effect.try({
269
+ try: () => {
270
+ const matchPath = createMatchPath(config.absoluteBaseUrl, config.paths)(
271
+ aliasPath,
272
+ undefined,
273
+ () => true,
274
+ supportedExtensions
275
+ )
276
+ if (!matchPath) {
277
+ throw new Error("Path not found", { cause: aliasPath })
278
+ }
279
+ return matchPath
280
+ },
281
+ catch: (error) => new Error("Path not found", { cause: String(error) })
282
+ })
283
+ })
284
+
285
+ return {
286
+ getComponentJson,
287
+ getTsConfig,
288
+ resolvePathFromAlias,
289
+ getStylingLibrary,
290
+ getUniwindDtsPath
291
+ }
292
+ })
293
+ }) {}
294
+
295
+ export { ProjectConfig }
@@ -0,0 +1,375 @@
1
+ import { CliOptions } from "@cli/contexts/cli-options.js"
2
+ import type { CustomFileCheck, FileCheck, FileWithContent, MissingInclude } from "@cli/project-manifest.js"
3
+ import { PROJECT_MANIFEST } from "@cli/project-manifest.js"
4
+ import { ProjectConfig } from "@cli/services/project-config.js"
5
+ import { retryWith } from "@cli/utils/retry-with.js"
6
+ import { FileSystem, Path } from "@effect/platform"
7
+ import { Data, Effect } from "effect"
8
+ import logSymbols from "log-symbols"
9
+
10
+ class RequiredFileError extends Data.TaggedError("RequiredFileError")<{
11
+ file: string
12
+ message?: string
13
+ }> { }
14
+
15
+ class RequiredFilesChecker extends Effect.Service<RequiredFilesChecker>()("RequiredFilesChecker", {
16
+ effect: Effect.gen(function* () {
17
+ const fs = yield* FileSystem.FileSystem
18
+ const path = yield* Path.Path
19
+ const options = yield* CliOptions
20
+ const projectConfig = yield* ProjectConfig
21
+
22
+ const checkFiles = (fileChecks: Array<FileCheck>, stylingLibrary: "nativewind" | "uniwind") =>
23
+ Effect.gen(function* () {
24
+ const missingFiles: Array<FileCheck> = []
25
+ const missingIncludes: Array<MissingInclude> = []
26
+
27
+ const filesWithContent = yield* Effect.forEach(
28
+ fileChecks.filter((file) => file.stylingLibraries.includes(stylingLibrary)),
29
+ (file) =>
30
+ retryWith(
31
+ (filePath: string) =>
32
+ Effect.gen(function* () {
33
+ const fileContents = yield* fs.readFileString(filePath)
34
+ yield* Effect.logDebug(`${logSymbols.success} ${file.name} found`)
35
+ return { ...file, content: fileContents } as FileWithContent
36
+ }),
37
+ file.fileNames.map((p) => path.join(options.cwd, p)) as [string, ...Array<string>]
38
+ ).pipe(
39
+ Effect.catchAll(() => {
40
+ missingFiles.push(file)
41
+ return Effect.logDebug(`${logSymbols.error} ${file.name} not found`).pipe(() => Effect.succeed(null))
42
+ })
43
+ ),
44
+ { concurrency: "unbounded" }
45
+ ).pipe(Effect.map((files) => files.filter((file): file is FileWithContent => file !== null)))
46
+
47
+ yield* Effect.forEach(filesWithContent, (file) =>
48
+ Effect.gen(function* () {
49
+ const { content, includes, name } = file
50
+ for (const include of includes) {
51
+ if (include.content.every((str) => content.includes(str))) {
52
+ yield* Effect.logDebug(`${logSymbols.success} ${name} has ${include.content.join(", ")}`)
53
+ continue
54
+ }
55
+ yield* Effect.logDebug(`${logSymbols.error} ${name} missing ${include.content.join(", ")}`)
56
+ missingIncludes.push({ ...include, fileName: name })
57
+ }
58
+ })
59
+ )
60
+
61
+ return { missingFiles, missingIncludes }
62
+ })
63
+
64
+ const checkDeprecatedFiles = (
65
+ deprecatedFromLib: Array<Omit<FileCheck, "docs" | "stylingLibraries">>,
66
+ deprecatedFromUi: Array<Omit<FileCheck, "docs" | "stylingLibraries">>
67
+ ) =>
68
+ Effect.gen(function* () {
69
+ const componentJson = yield* projectConfig.getComponentJson()
70
+ const aliasForLib = componentJson.aliases.lib ?? `${componentJson.aliases.utils}/lib`
71
+
72
+ const existingDeprecatedFromLibs = yield* Effect.forEach(
73
+ deprecatedFromLib,
74
+ (file) =>
75
+ projectConfig.resolvePathFromAlias(`${aliasForLib}/${file.fileNames[0]}`).pipe(
76
+ Effect.flatMap((fullPath) =>
77
+ Effect.gen(function* () {
78
+ const exists = yield* fs.exists(fullPath)
79
+ if (!exists) {
80
+ yield* Effect.logDebug(
81
+ `${logSymbols.success} Deprecated ${aliasForLib}/${file.fileNames[0]} not found`
82
+ )
83
+ return { ...file, hasIncludes: false }
84
+ }
85
+
86
+ yield* Effect.logDebug(`${logSymbols.error} Deprecated ${aliasForLib}/${file.fileNames[0]} found`)
87
+
88
+ const fileContent = yield* fs.readFileString(fullPath)
89
+
90
+ return {
91
+ ...file,
92
+ hasIncludes: file.includes.some((include) =>
93
+ include.content.some((content) => fileContent.includes(content))
94
+ )
95
+ }
96
+ })
97
+ )
98
+ ),
99
+ { concurrency: "unbounded" }
100
+ ).pipe(
101
+ Effect.map((results) =>
102
+ results.filter((result) => result.hasIncludes).map(({ hasIncludes: _hasIncludes, ...result }) => result)
103
+ )
104
+ )
105
+
106
+ const aliasForUi = componentJson.aliases.ui ?? `${componentJson.aliases.components}/ui`
107
+
108
+ const existingDeprecatedFromUi = yield* Effect.forEach(
109
+ deprecatedFromUi,
110
+ (file) =>
111
+ projectConfig.resolvePathFromAlias(`${aliasForUi}/${file.fileNames[0]}`).pipe(
112
+ Effect.flatMap((fullPath) =>
113
+ Effect.gen(function* () {
114
+ const exists = yield* fs.exists(fullPath)
115
+ if (!exists) {
116
+ yield* Effect.logDebug(
117
+ `${logSymbols.success} Deprecated ${aliasForUi}/${file.fileNames[0]} not found`
118
+ )
119
+ return { ...file, hasIncludes: false }
120
+ }
121
+
122
+ yield* Effect.logDebug(`${logSymbols.error} Deprecated ${aliasForUi}/${file.fileNames[0]} found`)
123
+
124
+ const fileContent = yield* fs.readFileString(fullPath)
125
+
126
+ return {
127
+ ...file,
128
+ hasIncludes: file.includes.some((include) =>
129
+ include.content.some((content) => fileContent.includes(content))
130
+ )
131
+ }
132
+ })
133
+ )
134
+ ),
135
+ { concurrency: "unbounded" }
136
+ ).pipe(
137
+ Effect.map((results) =>
138
+ results.filter((result) => result.hasIncludes).map(({ hasIncludes: _hasIncludes, ...result }) => result)
139
+ )
140
+ )
141
+
142
+ return [...existingDeprecatedFromLibs, ...existingDeprecatedFromUi]
143
+ })
144
+
145
+ const checkCustomFiles = (
146
+ customFileChecks: Record<string, CustomFileCheck>,
147
+ stylingLibrary: "nativewind" | "uniwind"
148
+ ) =>
149
+ Effect.gen(function* () {
150
+ const componentJson = yield* projectConfig.getComponentJson()
151
+ const aliasForLib = componentJson.aliases.lib ?? `${componentJson.aliases.utils}/lib`
152
+ const missingFiles: Array<CustomFileCheck> = []
153
+ const missingIncludes: Array<MissingInclude> = []
154
+
155
+ // Check CSS files
156
+ const cssPaths = [componentJson.tailwind.css, "global.css", "src/global.css"].filter((p) => p != null)
157
+ const cssContent = yield* retryWith(
158
+ (filePath: string) =>
159
+ Effect.gen(function* () {
160
+ const content = yield* fs.readFileString(filePath)
161
+ yield* Effect.logDebug(`${logSymbols.success} ${customFileChecks.css.name} found`)
162
+ return content
163
+ }),
164
+ cssPaths.map((p) => path.join(options.cwd, p)) as [string, ...Array<string>]
165
+ ).pipe(
166
+ Effect.catchAll(() =>
167
+ Effect.fail(
168
+ new RequiredFileError({
169
+ file: "CSS",
170
+ message:
171
+ "CSS file not found. Please follow the instructions at https://www.nativewind.dev/docs/getting-started/installation#installation-with-expo"
172
+ })
173
+ )
174
+ )
175
+ )
176
+
177
+ const cssShouldInclude =
178
+ stylingLibrary === "uniwind" ? customFileChecks.uniwindCss.includes : customFileChecks.css.includes
179
+
180
+ for (const include of cssShouldInclude) {
181
+ if (include.content.every((str) => cssContent.includes(str))) {
182
+ yield* Effect.logDebug(
183
+ `${logSymbols.success} ${customFileChecks.css.name} has ${include.content.join(", ")}`
184
+ )
185
+ continue
186
+ }
187
+ yield* Effect.logDebug(
188
+ `${logSymbols.error} ${customFileChecks.css.name} missing ${include.content.join(", ")}`
189
+ )
190
+ missingIncludes.push({ ...include, fileName: customFileChecks.css.name })
191
+ }
192
+
193
+ // Check Nativewind env or Uniwind types file
194
+ if (componentJson.tsx !== false) {
195
+ const missingTypeFiles: Array<CustomFileCheck> = []
196
+
197
+ if (stylingLibrary === "nativewind") {
198
+ const nativewindEnvContent = yield* fs
199
+ .readFileString(path.join(options.cwd, PROJECT_MANIFEST.nativewindEnvFile))
200
+ .pipe(
201
+ Effect.catchAll(() => {
202
+ missingTypeFiles.push(customFileChecks.nativewindEnv)
203
+ return Effect.succeed(null)
204
+ })
205
+ )
206
+
207
+ if (nativewindEnvContent) {
208
+ for (const include of customFileChecks.nativewindEnv.includes) {
209
+ if (include.content.every((str) => nativewindEnvContent.includes(str))) {
210
+ yield* Effect.logDebug(
211
+ `${logSymbols.success} ${customFileChecks.nativewindEnv.name} has ${include.content.join(", ")}`
212
+ )
213
+ continue
214
+ }
215
+ yield* Effect.logDebug(
216
+ `${logSymbols.error} ${customFileChecks.nativewindEnv.name} missing ${include.content.join(", ")}`
217
+ )
218
+ missingIncludes.push({ ...include, fileName: customFileChecks.nativewindEnv.name })
219
+ }
220
+ }
221
+ }
222
+
223
+ if (stylingLibrary === "uniwind") {
224
+ // Get uniwind dts path from metro config (supports custom dtsFile)
225
+ const uniwindDtsPath = yield* projectConfig.getUniwindDtsPath()
226
+ const uniwindTypesContent: string | null = uniwindDtsPath
227
+ ? yield* fs.readFileString(uniwindDtsPath).pipe(
228
+ Effect.catchAll(() => {
229
+ missingTypeFiles.push(customFileChecks.uniwindTypes)
230
+ return Effect.succeed(null)
231
+ })
232
+ )
233
+ : null
234
+
235
+ if (uniwindTypesContent) {
236
+ for (const include of customFileChecks.uniwindTypes.includes) {
237
+ if (include.content.every((str) => uniwindTypesContent.includes(str))) {
238
+ yield* Effect.logDebug(
239
+ `${logSymbols.success} ${customFileChecks.uniwindTypes.name} has ${include.content.join(", ")}`
240
+ )
241
+ continue
242
+ }
243
+ yield* Effect.logDebug(
244
+ `${logSymbols.error} ${customFileChecks.uniwindTypes.name} missing ${include.content.join(", ")}`
245
+ )
246
+ missingIncludes.push({ ...include, fileName: customFileChecks.uniwindTypes.name })
247
+ }
248
+ }
249
+ }
250
+
251
+ if (missingTypeFiles.length === 2) {
252
+ missingFiles.push(missingTypeFiles[0], missingTypeFiles[1])
253
+ }
254
+ }
255
+
256
+ if (stylingLibrary === "nativewind") {
257
+ // Check Tailwind config
258
+ const tailwindConfigPaths = [
259
+ componentJson.tailwind.config,
260
+ "tailwind.config.js",
261
+ "tailwind.config.ts"
262
+ ].filter((p) => p != null)
263
+ const tailwindConfigContent = yield* retryWith(
264
+ (filePath: string) =>
265
+ Effect.gen(function* () {
266
+ const content = yield* fs.readFileString(filePath)
267
+ yield* Effect.logDebug(`${logSymbols.success} ${customFileChecks.tailwindConfig.name} found`)
268
+ return content
269
+ }),
270
+ tailwindConfigPaths.map((p) => path.join(options.cwd, p)) as [string, ...Array<string>]
271
+ ).pipe(
272
+ Effect.catchAll(() => {
273
+ console.warn(
274
+ `${logSymbols.warning} Tailwind config not found, Please follow the instructions at https://www.nativewind.dev/docs/getting-started/installation#installation-with-expo`);
275
+ return Effect.succeed("")
276
+ })
277
+ )
278
+
279
+ for (const include of customFileChecks.tailwindConfig.includes) {
280
+ if (include.content.every((str) => tailwindConfigContent.includes(str))) {
281
+ yield* Effect.logDebug(
282
+ `${logSymbols.success} ${customFileChecks.tailwindConfig.name} has ${include.content.join(", ")}`
283
+ )
284
+ continue
285
+ }
286
+ yield* Effect.logDebug(
287
+ `${logSymbols.error} ${customFileChecks.tailwindConfig.name} missing ${include.content.join(", ")}`
288
+ )
289
+ missingIncludes.push({ ...include, fileName: customFileChecks.tailwindConfig.name })
290
+ }
291
+ }
292
+
293
+ // Check theme file
294
+ const themeAliasPath = yield* projectConfig.resolvePathFromAlias(`${aliasForLib}/theme.ts`)
295
+ const themeContent = yield* fs.readFileString(themeAliasPath).pipe(
296
+ Effect.catchAll(() => {
297
+ missingFiles.push(customFileChecks.theme)
298
+ return Effect.succeed(null)
299
+ })
300
+ )
301
+
302
+ if (themeContent) {
303
+ for (const include of customFileChecks.theme.includes) {
304
+ if (include.content.every((str) => themeContent.includes(str))) {
305
+ yield* Effect.logDebug(
306
+ `${logSymbols.success} ${customFileChecks.theme.name} has ${include.content.join(", ")}`
307
+ )
308
+ continue
309
+ }
310
+ yield* Effect.logDebug(
311
+ `${logSymbols.error} ${customFileChecks.theme.name} missing ${include.content.join(", ")}`
312
+ )
313
+ missingIncludes.push({ ...include, fileName: customFileChecks.theme.name })
314
+ }
315
+ } else {
316
+ yield* Effect.logDebug(`${logSymbols.error} ${customFileChecks.theme.name} not found`)
317
+ }
318
+
319
+ // Check utils file
320
+ const utilsPath = yield* projectConfig.resolvePathFromAlias(`${aliasForLib}/utils.ts`)
321
+ const utilsContent = yield* fs.readFileString(utilsPath).pipe(
322
+ Effect.catchAll(() => {
323
+ missingFiles.push(customFileChecks.utils)
324
+ return Effect.succeed(null)
325
+ })
326
+ )
327
+
328
+ if (utilsContent) {
329
+ for (const include of customFileChecks.utils.includes) {
330
+ if (include.content.every((str) => utilsContent.includes(str))) {
331
+ yield* Effect.logDebug(
332
+ `${logSymbols.success} ${customFileChecks.utils.name} has ${include.content.join(", ")}`
333
+ )
334
+ continue
335
+ }
336
+ yield* Effect.logDebug(
337
+ `${logSymbols.error} ${customFileChecks.utils.name} missing ${include.content.join(", ")}`
338
+ )
339
+ missingIncludes.push({ ...include, fileName: customFileChecks.utils.name })
340
+ }
341
+ } else {
342
+ yield* Effect.logDebug(`${logSymbols.error} ${customFileChecks.utils.name} not found`)
343
+ }
344
+
345
+ return { missingFiles, missingIncludes }
346
+ })
347
+
348
+ return {
349
+ run: ({
350
+ customFileChecks,
351
+ deprecatedFromLib,
352
+ deprecatedFromUi,
353
+ fileChecks,
354
+ stylingLibrary
355
+ }: {
356
+ fileChecks: Array<FileCheck>
357
+ customFileChecks: Record<string, CustomFileCheck>
358
+ deprecatedFromLib: Array<Omit<FileCheck, "docs" | "stylingLibraries">>
359
+ deprecatedFromUi: Array<Omit<FileCheck, "docs" | "stylingLibraries">>
360
+ stylingLibrary: "nativewind" | "uniwind"
361
+ }) =>
362
+ Effect.gen(function* () {
363
+ const [fileResults, customFileResults, deprecatedFileResults] = yield* Effect.all([
364
+ checkFiles(fileChecks, stylingLibrary),
365
+ checkCustomFiles(customFileChecks, stylingLibrary),
366
+ checkDeprecatedFiles(deprecatedFromLib, deprecatedFromUi)
367
+ ])
368
+
369
+ return { fileResults, customFileResults, deprecatedFileResults }
370
+ })
371
+ }
372
+ })
373
+ }) { }
374
+
375
+ export { RequiredFilesChecker }
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect"
2
+ import ora from "ora"
3
+
4
+ class Spinner extends Effect.Service<Spinner>()("Spinner", {
5
+ effect: Effect.gen(function* () {
6
+ const spinner = yield* Effect.try({
7
+ try: () => ora(),
8
+ catch: () => new Error("Failed to create spinner")
9
+ })
10
+
11
+ return spinner
12
+ })
13
+ }) {}
14
+
15
+ export { Spinner }