diff --git a/.changeset/cuddly-comics-hammer.md b/.changeset/cuddly-comics-hammer.md new file mode 100644 index 0000000..484e9f8 --- /dev/null +++ b/.changeset/cuddly-comics-hammer.md @@ -0,0 +1,5 @@ +--- +"jsrepo": patch +--- + +feat: Autofix incorrect extension on (.ts|js|mjs|cjs) config files. diff --git a/.changeset/famous-hotels-film.md b/.changeset/famous-hotels-film.md new file mode 100644 index 0000000..fec8cc3 --- /dev/null +++ b/.changeset/famous-hotels-film.md @@ -0,0 +1,5 @@ +--- +"jsrepo": patch +--- + +fix: Ensure registries coming from args are always configured first. diff --git a/.changeset/great-bobcats-hunt.md b/.changeset/great-bobcats-hunt.md new file mode 100644 index 0000000..c9f3be6 --- /dev/null +++ b/.changeset/great-bobcats-hunt.md @@ -0,0 +1,5 @@ +--- +"jsrepo": patch +--- + +feat: Improve error messages when paths are incorrectly named or do not resolve. diff --git a/.changeset/many-geese-thank.md b/.changeset/many-geese-thank.md new file mode 100644 index 0000000..8ebc4ef --- /dev/null +++ b/.changeset/many-geese-thank.md @@ -0,0 +1,5 @@ +--- +"jsrepo": patch +--- + +fix: Do not accept invalid path default blocks path on init. diff --git a/packages/cli/package.json b/packages/cli/package.json index 8bf92a4..78bfc0d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -15,16 +15,7 @@ "bugs": { "url": "https://github.com/ieedan/jsrepo/issues" }, - "keywords": [ - "repo", - "cli", - "svelte", - "vue", - "typescript", - "javascript", - "shadcn", - "registry" - ], + "keywords": ["repo", "cli", "svelte", "vue", "typescript", "javascript", "shadcn", "registry"], "type": "module", "exports": { ".": { @@ -34,10 +25,7 @@ }, "bin": "./dist/index.js", "main": "./dist/index.js", - "files": [ - "./schemas/**/*", - "dist/**/*" - ], + "files": ["./schemas/**/*", "dist/**/*"], "scripts": { "start": "tsup --silent && node ./dist/index.js", "build": "tsup", diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index a7e5605..305f7a2 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -14,6 +14,7 @@ import { import color from 'chalk'; import { Command, Option, program } from 'commander'; import { diffLines } from 'diff'; +import { createPathsMatcher } from 'get-tsconfig'; import { detect, resolveCommand } from 'package-manager-detector'; import path from 'pathe'; import * as v from 'valibot'; @@ -32,7 +33,7 @@ import { } from '../utils/config'; import { installDependencies } from '../utils/dependencies'; import { formatDiff } from '../utils/diff'; -import { formatFile } from '../utils/files'; +import { formatFile, matchJSDescendant, tryGetTsconfig } from '../utils/files'; import { loadFormatterConfig } from '../utils/format'; import { json } from '../utils/language-support'; import * as persisted from '../utils/persisted'; @@ -144,10 +145,29 @@ const _initProject = async (registries: string[], options: Options) => { let paths: Paths; let configFiles: Record = {}; + const tsconfigResult = tryGetTsconfig(options.cwd).unwrapOr(null); + const defaultPathResult = await text({ message: 'Please enter a default path to install the blocks', validate(value) { if (value.trim() === '') return 'Please provide a value'; + + if (!value.startsWith('./')) { + const error = + 'Invalid path alias! If you are intending to use a relative path make sure it starts with `./`'; + + if (tsconfigResult === null) { + return error; + } + + const matcher = createPathsMatcher(tsconfigResult); + + if (matcher) { + const found = matcher(value); + + if (found.length === 0) return error; + } + } }, placeholder: './src/blocks', initialValue: initialConfig.isOk() ? initialConfig.unwrap().paths['*'] : undefined, @@ -200,9 +220,9 @@ const _initProject = async (registries: string[], options: Options) => { const repos = Array.from( new Set([ - ...(initialConfig.isOk() ? initialConfig.unwrap().repos : []), ...registries, ...(options.repos ?? []), + ...(initialConfig.isOk() ? initialConfig.unwrap().repos : []), ]) ); @@ -444,12 +464,33 @@ const promptForProviderConfig = async ({ configFiles[file.name] = result; } - const fullFilePath = path.join(options.cwd, configFiles[file.name]); + let fullFilePath = path.join(options.cwd, configFiles[file.name]); let originalFileContents: string | undefined; if (fs.existsSync(fullFilePath)) { originalFileContents = fs.readFileSync(fullFilePath).toString(); + } else { + const dir = path.dirname(fullFilePath); + + if (fs.existsSync(dir)) { + const matchedPath = matchJSDescendant(fullFilePath); + + if (matchedPath) { + originalFileContents = fs.readFileSync(matchedPath).toString(); + + const newPath = path.relative(options.cwd, matchedPath); + + log.warn( + `Located ${color.bold(configFiles[file.name])} at ${color.bold(newPath)}` + ); + + // update path + configFiles[file.name] = newPath; + + fullFilePath = path.join(options.cwd, newPath); + } + } } loading.start(`Fetching the ${color.cyan(file.name)} from ${color.cyan(repo)}`); @@ -618,7 +659,9 @@ const promptForProviderConfig = async ({ } else { const dir = path.dirname(fullFilePath); - fs.mkdirSync(dir, { recursive: true }); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } } if (acceptedChanges) { diff --git a/packages/cli/src/utils/blocks/ts/strings.ts b/packages/cli/src/utils/blocks/ts/strings.ts index d4cdf3b..3eb1707 100644 --- a/packages/cli/src/utils/blocks/ts/strings.ts +++ b/packages/cli/src/utils/blocks/ts/strings.ts @@ -2,15 +2,15 @@ * * ## Usage * ```ts - * startsWithOneOf('a', 'a', 'b'); // true - * startsWithOneOf('c', 'a', 'b'); // false + * startsWithOneOf('ab', 'a', 'c'); // true + * startsWithOneOf('cc', 'a', 'b'); // false * ``` * * @param str * @param strings * @returns */ -const startsWithOneOf = (str: string, strings: string[]): boolean => { +export const startsWithOneOf = (str: string, strings: string[]): boolean => { for (const s of strings) { if (str.startsWith(s)) return true; } @@ -18,4 +18,22 @@ const startsWithOneOf = (str: string, strings: string[]): boolean => { return false; }; -export { startsWithOneOf }; +/** Returns true if `str` starts with one of the provided `strings`. + * + * ## Usage + * ```ts + * endsWithOneOf('cb', 'a', 'b'); // true + * endsWithOneOf('cc', 'a', 'b'); // false + * ``` + * + * @param str + * @param strings + * @returns + */ +export const endsWithOneOf = (str: string, strings: string[]): boolean => { + for (const s of strings) { + if (str.endsWith(s)) return true; + } + + return false; +}; diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index a8d07d5..64a0bf9 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -1,17 +1,19 @@ import fs from 'node:fs'; -import { createPathsMatcher, getTsconfig } from 'get-tsconfig'; +import color from 'chalk'; +import { createPathsMatcher } from 'get-tsconfig'; import path from 'pathe'; import * as v from 'valibot'; import { type Block, configFileSchema, manifestMeta } from '../types'; import { Err, Ok, type Result } from './blocks/ts/result'; import { ruleConfigSchema } from './build/check'; +import { tryGetTsconfig } from './files'; -const PROJECT_CONFIG_NAME = 'jsrepo.json'; -const REGISTRY_CONFIG_NAME = 'jsrepo-build-config.json'; +export const PROJECT_CONFIG_NAME = 'jsrepo.json'; +export const REGISTRY_CONFIG_NAME = 'jsrepo-build-config.json'; -const formatterSchema = v.union([v.literal('prettier'), v.literal('biome')]); +export const formatterSchema = v.union([v.literal('prettier'), v.literal('biome')]); -const pathsSchema = v.objectWithRest( +export const pathsSchema = v.objectWithRest( { '*': v.string(), }, @@ -20,7 +22,7 @@ const pathsSchema = v.objectWithRest( export type Paths = v.InferInput; -const projectConfigSchema = v.object({ +export const projectConfigSchema = v.object({ $schema: v.string(), repos: v.optional(v.array(v.string()), []), includeTests: v.boolean(), @@ -30,7 +32,7 @@ const projectConfigSchema = v.object({ formatter: v.optional(formatterSchema), }); -const getProjectConfig = (cwd: string): Result => { +export const getProjectConfig = (cwd: string): Result => { if (!fs.existsSync(path.join(cwd, PROJECT_CONFIG_NAME))) { return Err('Could not find your configuration file! Please run `init`.'); } @@ -51,7 +53,7 @@ export type ProjectConfig = v.InferOutput; export type Formatter = v.InferOutput; -const registryConfigSchema = v.object({ +export const registryConfigSchema = v.object({ $schema: v.string(), meta: v.optional(manifestMeta), configFiles: v.optional(v.array(configFileSchema)), @@ -71,7 +73,7 @@ const registryConfigSchema = v.object({ rules: v.optional(ruleConfigSchema), }); -const getRegistryConfig = (cwd: string): Result => { +export const getRegistryConfig = (cwd: string): Result => { if (!fs.existsSync(path.join(cwd, REGISTRY_CONFIG_NAME))) { return Ok(null); } @@ -91,61 +93,47 @@ const getRegistryConfig = (cwd: string): Result = export type RegistryConfig = v.InferOutput; /** Resolves the paths relative to the cwd */ -const resolvePaths = (paths: Paths, cwd: string): Result => { - let config = getTsconfig(cwd, 'tsconfig.json'); - let matcher: ((specifier: string) => string[]) | null = null; +export const resolvePaths = (paths: Paths, cwd: string): Result => { + const config = tryGetTsconfig(cwd).unwrapOr(null); - if (!config) { - // if we don't find the config at first check for a jsconfig - config = getTsconfig(cwd, 'jsconfig.json'); - } - - if (config) { - matcher = createPathsMatcher(config); - } - - let newPaths: Paths; + const matcher = config ? createPathsMatcher(config) : null; - if (!paths['*'].startsWith('.')) { - if (matcher === null) { - return Err("Cannot resolve aliases because we couldn't find a tsconfig!"); - } - - newPaths = { - '*': resolvePath(paths['*'], matcher, cwd), - }; - } else { - newPaths = { - '*': path.relative(cwd, path.join(path.resolve(cwd), paths['*'])), - }; - } + const newPaths: Paths = { '*': '' }; for (const [category, p] of Object.entries(paths)) { - if (category === '*') continue; // we already resolved this one - - if (p.startsWith('.')) { + if (p.startsWith('./')) { newPaths[category] = path.relative(cwd, path.join(path.resolve(cwd), p)); continue; } if (matcher === null) { - return Err("Cannot resolve aliases because we couldn't find a tsconfig!"); + return Err( + `Cannot resolve ${color.bold(`\`"${category}": "${p}"\``)} from paths because we couldn't find a tsconfig! If you intended to use a relative path ensure that your path starts with ${color.bold('`./`')}.` + ); } - newPaths[category] = resolvePath(p, matcher, cwd); + const resolved = tryResolvePath(p, matcher, cwd); + + if (!resolved) { + return Err( + `Cannot resolve ${color.bold(`\`"${category}": "${p}"\``)} from paths because we couldn't find a matching alias in the tsconfig. If you intended to use a relative path ensure that your path starts with ${color.bold('`./`')}.` + ); + } + + newPaths[category] = resolved; } return Ok(newPaths); }; -const resolvePath = ( +const tryResolvePath = ( unresolvedPath: string, matcher: (specifier: string) => string[], cwd: string -): string => { +): string | undefined => { const paths = matcher(unresolvedPath); - return path.relative(cwd, paths[0]); + return paths.length > 0 ? path.relative(cwd, paths[0]) : undefined; }; /** Gets the path where the block should be installed. @@ -155,7 +143,7 @@ const resolvePath = ( * @param cwd * @returns */ -const getPathForBlock = (block: Block, resolvedPaths: Paths, cwd: string): string => { +export const getPathForBlock = (block: Block, resolvedPaths: Paths, cwd: string): string => { let directory: string; if (resolvedPaths[block.category] !== undefined) { @@ -166,15 +154,3 @@ const getPathForBlock = (block: Block, resolvedPaths: Paths, cwd: string): strin return directory; }; - -export { - PROJECT_CONFIG_NAME, - REGISTRY_CONFIG_NAME, - getProjectConfig, - getRegistryConfig, - projectConfigSchema, - registryConfigSchema, - formatterSchema, - resolvePaths, - getPathForBlock, -}; diff --git a/packages/cli/src/utils/files.ts b/packages/cli/src/utils/files.ts index 017762c..8185902 100644 --- a/packages/cli/src/utils/files.ts +++ b/packages/cli/src/utils/files.ts @@ -1,8 +1,12 @@ +import fs from 'node:fs'; import type { PartialConfiguration } from '@biomejs/wasm-nodejs'; import color from 'chalk'; import escapeStringRegexp from 'escape-string-regexp'; +import { type TsConfigResult, getTsconfig } from 'get-tsconfig'; +import path from 'pathe'; import type * as prettier from 'prettier'; import { Err, Ok, type Result } from './blocks/ts/result'; +import { endsWithOneOf } from './blocks/ts/strings'; import type { ProjectConfig } from './config'; import { resolveLocalDependencyTemplate } from './dependencies'; import { languages } from './language-support'; @@ -28,7 +32,7 @@ type TransformRemoteContentOptions = { * @param param0 * @returns */ -const transformRemoteContent = async ({ +export const transformRemoteContent = async ({ file, config, imports, @@ -98,7 +102,7 @@ type FormatOptions = { * @param param0 * @returns */ -const formatFile = async ({ +export const formatFile = async ({ file, config, prettierOptions, @@ -124,4 +128,48 @@ const formatFile = async ({ return newContent; }; -export { transformRemoteContent, formatFile }; +export const matchJSDescendant = (searchFilePath: string): string | undefined => { + const MATCH_EXTENSIONS = ['.js', '.ts', '.cjs', '.mjs']; + + if (!endsWithOneOf(searchFilePath, MATCH_EXTENSIONS)) return undefined; + + const dir = path.dirname(searchFilePath); + + const files = fs.readdirSync(dir); + + const parsedSearch = path.parse(searchFilePath); + + for (const file of files) { + if (!endsWithOneOf(file, MATCH_EXTENSIONS)) continue; + + if (path.parse(file).name === parsedSearch.name) return path.join(dir, file); + } + + return undefined; +}; + +/** Attempts to get the js/tsconfig file for the searched path + * + * @param searchPath + * @returns + */ +export const tryGetTsconfig = (searchPath?: string): Result => { + let config: TsConfigResult | null; + + try { + config = getTsconfig(searchPath, 'tsconfig.json'); + + if (!config) { + // if we don't find the config at first check for a jsconfig + config = getTsconfig(searchPath, 'jsconfig.json'); + + if (!config) { + return Ok(null); + } + } + } catch (err) { + return Err(`Error while trying to get ${color.bold('tsconfig.json')}: ${err}`); + } + + return Ok(config); +}; diff --git a/packages/cli/src/utils/language-support.ts b/packages/cli/src/utils/language-support.ts index 11a81cc..a490e87 100644 --- a/packages/cli/src/utils/language-support.ts +++ b/packages/cli/src/utils/language-support.ts @@ -4,7 +4,7 @@ import { Biome, Distribution } from '@biomejs/js-api'; import type { PartialConfiguration } from '@biomejs/wasm-nodejs'; import color from 'chalk'; import { type Node, walk } from 'estree-walker'; -import { type TsConfigResult, createPathsMatcher, getTsconfig } from 'get-tsconfig'; +import { createPathsMatcher } from 'get-tsconfig'; import * as parse5 from 'parse5'; import path from 'pathe'; import * as prettier from 'prettier'; @@ -16,6 +16,7 @@ import * as ascii from './ascii'; import * as lines from './blocks/ts/lines'; import { Err, Ok, type Result } from './blocks/ts/result'; import type { Formatter } from './config'; +import { tryGetTsconfig } from './files'; import { findNearestPackageJson } from './package'; import { parsePackageName } from './parse-package-name'; @@ -681,22 +682,13 @@ const tryResolveLocalAlias = ( containingDir, }: { filePath: string; containingDir?: string; dirs: string[]; cwd: string } ): Result => { - let config: TsConfigResult | null; + const configResult = tryGetTsconfig(filePath); - try { - config = getTsconfig(filePath, 'tsconfig.json'); + if (configResult.isErr()) return Err(configResult.unwrapErr()); - if (!config) { - // if we don't find the config at first check for a jsconfig - config = getTsconfig(filePath, 'jsconfig.json'); + const config = configResult.unwrap(); - if (!config) { - return Ok(undefined); - } - } - } catch (err) { - return Err(`Error while trying to get ${color.bold('tsconfig.json')}: ${err}`); - } + if (config === null) return Ok(undefined); const matcher = createPathsMatcher(config); diff --git a/sites/docs/jsrepo.json b/sites/docs/jsrepo.json index 3a34593..76f005e 100644 --- a/sites/docs/jsrepo.json +++ b/sites/docs/jsrepo.json @@ -1,5 +1,5 @@ { - "$schema": "https://unpkg.com/jsrepo@1.18.0/schemas/project-config.json", + "$schema": "https://unpkg.com/jsrepo@1.33.0/schemas/project-config.json", "repos": ["github/ieedan/std", "github/ieedan/shadcn-svelte-extras"], "includeTests": false, "watermark": true,