@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.
- package/.changeset/config.json +16 -0
- package/.github/actions/setup/action.yml +21 -0
- package/.github/workflows/check.yml +54 -0
- package/.github/workflows/release.yml +0 -0
- package/.github/workflows/snapshot.yml +24 -0
- package/.prettierrc +8 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +46 -0
- package/LICENSE +1 -1
- package/README.md +97 -0
- package/eslint.config.mjs +118 -0
- package/package.json +64 -8
- package/patches/@changesets__get-github-info@0.6.0.patch +48 -0
- package/scripts/copy-package-json.ts +32 -0
- package/src/bin.ts +18 -0
- package/src/cli.ts +66 -0
- package/src/contexts/cli-options.ts +29 -0
- package/src/project-manifest.ts +369 -0
- package/src/services/commands/add.ts +127 -0
- package/src/services/commands/doctor.ts +287 -0
- package/src/services/commands/init.ts +94 -0
- package/src/services/git.ts +39 -0
- package/src/services/package-manager.ts +48 -0
- package/src/services/project-config.ts +295 -0
- package/src/services/required-files-checker.ts +375 -0
- package/src/services/spinner.ts +15 -0
- package/src/services/template.ts +222 -0
- package/src/utils/retry-with.ts +9 -0
- package/src/utils/run-command.ts +10 -0
- package/test/Dummy.test.ts +7 -0
- package/tsconfig.base.json +53 -0
- package/tsconfig.json +14 -0
- package/tsconfig.scripts.json +17 -0
- package/tsconfig.src.json +10 -0
- package/tsconfig.test.json +10 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +12 -0
- package/bin.cjs +0 -59048
|
@@ -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 }
|