mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat(shadcn): update file handling for monorepo (#7955)
* feat(shadcn): update monorepo handling * feat(shadcn): update file handling for monorepo * chore: changeset
This commit is contained in:
5
.changeset/hungry-melons-press.md
Normal file
5
.changeset/hungry-melons-press.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": minor
|
||||
---
|
||||
|
||||
update file handling for monorepo
|
||||
@@ -1,12 +1,5 @@
|
||||
import path from "path"
|
||||
import {
|
||||
fetchRegistry,
|
||||
getRegistryItem,
|
||||
getRegistryParentMap,
|
||||
getRegistryTypeAliasMap,
|
||||
registryResolveItemsTree,
|
||||
resolveRegistryItems,
|
||||
} from "@/src/registry/api"
|
||||
import { getRegistryItem, registryResolveItemsTree } from "@/src/registry/api"
|
||||
import {
|
||||
configSchema,
|
||||
registryItemFileSchema,
|
||||
@@ -158,132 +151,143 @@ async function addWorkspaceComponents(
|
||||
const registrySpinner = spinner(`Checking registry.`, {
|
||||
silent: options.silent,
|
||||
})?.start()
|
||||
let registryItems = await resolveRegistryItems(components, config)
|
||||
let result = await fetchRegistry(registryItems)
|
||||
const payload = z.array(registryItemSchema).parse(result)
|
||||
if (!payload) {
|
||||
const tree = await registryResolveItemsTree(components, config)
|
||||
|
||||
if (!tree) {
|
||||
registrySpinner?.fail()
|
||||
return handleError(new Error("Failed to fetch components from registry."))
|
||||
}
|
||||
registrySpinner?.succeed()
|
||||
|
||||
const registryParentMap = getRegistryParentMap(payload)
|
||||
const registryTypeAliasMap = getRegistryTypeAliasMap()
|
||||
try {
|
||||
validateFilesTarget(tree.files ?? [], config.resolvedPaths.cwd)
|
||||
} catch (error) {
|
||||
registrySpinner?.fail()
|
||||
return handleError(error)
|
||||
}
|
||||
|
||||
registrySpinner?.succeed()
|
||||
|
||||
const filesCreated: string[] = []
|
||||
const filesUpdated: string[] = []
|
||||
const filesSkipped: string[] = []
|
||||
|
||||
const files = payload.flatMap((item) => item.files ?? [])
|
||||
try {
|
||||
validateFilesTarget(files, config.resolvedPaths.cwd)
|
||||
} catch (error) {
|
||||
return handleError(error)
|
||||
}
|
||||
|
||||
const rootSpinner = spinner(`Installing components.`)?.start()
|
||||
|
||||
for (const component of payload) {
|
||||
const alias = registryTypeAliasMap.get(component.type)
|
||||
const registryParent = registryParentMap.get(component.name)
|
||||
// Process global updates (tailwind, css vars, dependencies) first for the main target.
|
||||
// These should typically go to the UI package in a workspace.
|
||||
const mainTargetConfig = workspaceConfig.ui
|
||||
const tailwindVersion = await getProjectTailwindVersionFromConfig(
|
||||
mainTargetConfig
|
||||
)
|
||||
const workspaceRoot = findCommonRoot(
|
||||
config.resolvedPaths.cwd,
|
||||
mainTargetConfig.resolvedPaths.ui
|
||||
)
|
||||
|
||||
// We don't support this type of component.
|
||||
if (!alias) {
|
||||
continue
|
||||
}
|
||||
|
||||
// A good start is ui for now.
|
||||
// TODO: Add support for other types.
|
||||
let targetConfig =
|
||||
component.type === "registry:ui" || registryParent?.type === "registry:ui"
|
||||
? workspaceConfig.ui
|
||||
: config
|
||||
|
||||
const tailwindVersion = await getProjectTailwindVersionFromConfig(
|
||||
targetConfig
|
||||
// 1. Update tailwind config.
|
||||
if (tree.tailwind?.config) {
|
||||
await updateTailwindConfig(tree.tailwind?.config, mainTargetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(
|
||||
workspaceRoot,
|
||||
mainTargetConfig.resolvedPaths.tailwindConfig
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const workspaceRoot = findCommonRoot(
|
||||
// 2. Update css vars.
|
||||
if (tree.cssVars) {
|
||||
const overwriteCssVars = await shouldOverwriteCssVars(components, config)
|
||||
await updateCssVars(tree.cssVars, mainTargetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
tailwindConfig: tree.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Update CSS
|
||||
if (tree.css) {
|
||||
await updateCss(tree.css, mainTargetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, mainTargetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Update environment variables
|
||||
if (tree.envVars) {
|
||||
await updateEnvVars(tree.envVars, mainTargetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Update dependencies.
|
||||
await updateDependencies(
|
||||
tree.dependencies,
|
||||
tree.devDependencies,
|
||||
mainTargetConfig,
|
||||
{
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 6. Group files by their type to determine target config and update files.
|
||||
const filesByType = new Map<string, typeof tree.files>()
|
||||
|
||||
for (const file of tree.files ?? []) {
|
||||
const type = file.type || "registry:ui"
|
||||
if (!filesByType.has(type)) {
|
||||
filesByType.set(type, [])
|
||||
}
|
||||
filesByType.get(type)!.push(file)
|
||||
}
|
||||
|
||||
// Process each type of component with its appropriate target config.
|
||||
for (const type of Array.from(filesByType.keys())) {
|
||||
const typeFiles = filesByType.get(type)!
|
||||
|
||||
let targetConfig = type === "registry:ui" ? workspaceConfig.ui : config
|
||||
|
||||
const typeWorkspaceRoot = findCommonRoot(
|
||||
config.resolvedPaths.cwd,
|
||||
targetConfig.resolvedPaths.ui
|
||||
targetConfig.resolvedPaths.ui || targetConfig.resolvedPaths.cwd
|
||||
)
|
||||
const packageRoot =
|
||||
(await findPackageRoot(workspaceRoot, targetConfig.resolvedPaths.cwd)) ??
|
||||
targetConfig.resolvedPaths.cwd
|
||||
(await findPackageRoot(
|
||||
typeWorkspaceRoot,
|
||||
targetConfig.resolvedPaths.cwd
|
||||
)) ?? targetConfig.resolvedPaths.cwd
|
||||
|
||||
// 1. Update tailwind config.
|
||||
if (component.tailwind?.config) {
|
||||
await updateTailwindConfig(component.tailwind?.config, targetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindConfig)
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Update css vars.
|
||||
if (component.cssVars) {
|
||||
const overwriteCssVars = await shouldOverwriteCssVars(components, config)
|
||||
await updateCssVars(component.cssVars, targetConfig, {
|
||||
silent: true,
|
||||
tailwindVersion,
|
||||
tailwindConfig: component.tailwind?.config,
|
||||
overwriteCssVars,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 3. Update CSS
|
||||
if (component.css) {
|
||||
await updateCss(component.css, targetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
filesUpdated.push(
|
||||
path.relative(workspaceRoot, targetConfig.resolvedPaths.tailwindCss)
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Update environment variables
|
||||
if (component.envVars) {
|
||||
await updateEnvVars(component.envVars, targetConfig, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
||||
// 5. Update dependencies.
|
||||
await updateDependencies(
|
||||
component.dependencies,
|
||||
component.devDependencies,
|
||||
targetConfig,
|
||||
{
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
|
||||
// 6. Update files.
|
||||
const files = await updateFiles(component.files, targetConfig, {
|
||||
// Update files for this type.
|
||||
const files = await updateFiles(typeFiles, targetConfig, {
|
||||
overwrite: options.overwrite,
|
||||
silent: true,
|
||||
rootSpinner,
|
||||
isRemote: options.isRemote,
|
||||
isWorkspace: true,
|
||||
})
|
||||
|
||||
filesCreated.push(
|
||||
...files.filesCreated.map((file) =>
|
||||
path.relative(workspaceRoot, path.join(packageRoot, file))
|
||||
path.relative(typeWorkspaceRoot, path.join(packageRoot, file))
|
||||
)
|
||||
)
|
||||
filesUpdated.push(
|
||||
...files.filesUpdated.map((file) =>
|
||||
path.relative(workspaceRoot, path.join(packageRoot, file))
|
||||
path.relative(typeWorkspaceRoot, path.join(packageRoot, file))
|
||||
)
|
||||
)
|
||||
filesSkipped.push(
|
||||
...files.filesSkipped.map((file) =>
|
||||
path.relative(workspaceRoot, path.join(packageRoot, file))
|
||||
path.relative(typeWorkspaceRoot, path.join(packageRoot, file))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -343,6 +347,10 @@ async function addWorkspaceComponents(
|
||||
logger.log(` - ${file}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.docs) {
|
||||
logger.info(tree.docs)
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldOverwriteCssVars(
|
||||
|
||||
260
packages/shadcn/src/utils/compare.test.ts
Normal file
260
packages/shadcn/src/utils/compare.test.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { isContentSame } from "./compare"
|
||||
|
||||
describe("isContentSame", () => {
|
||||
describe("basic comparisons", () => {
|
||||
it("should return true for identical content", () => {
|
||||
const content = `const foo = "bar"`
|
||||
expect(isContentSame(content, content)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for content with different line endings", () => {
|
||||
const content1 = `line1\nline2\nline3`
|
||||
const content2 = `line1\r\nline2\r\nline3`
|
||||
expect(isContentSame(content1, content2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return true for content with different whitespace at ends", () => {
|
||||
const content1 = ` const foo = "bar" `
|
||||
const content2 = `const foo = "bar"`
|
||||
expect(isContentSame(content1, content2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for different content", () => {
|
||||
const content1 = `const foo = "bar"`
|
||||
const content2 = `const foo = "baz"`
|
||||
expect(isContentSame(content1, content2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("import comparisons with ignoreImports enabled", () => {
|
||||
it("should return true for different aliased imports to same module", () => {
|
||||
const content1 = `import { Button } from "@/components/ui/button"`
|
||||
const content2 = `import { Button } from "~/ui/button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should return true for different paths to same final module", () => {
|
||||
const content1 = `import { cn } from "@/lib/utils"`
|
||||
const content2 = `import { cn } from "~/utils"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should preserve relative imports and require exact match", () => {
|
||||
const content1 = `import { Button } from "./button"`
|
||||
const content2 = `import { Button } from "../button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle multiple imports with different aliases", () => {
|
||||
const content1 = `
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Card } from "@/components/ui/card"
|
||||
|
||||
export function Component() {
|
||||
return <div />
|
||||
}
|
||||
`
|
||||
const content2 = `
|
||||
import { Button } from "~/ui/button"
|
||||
import { cn } from "~/utils"
|
||||
import { Card } from "#/components/card"
|
||||
|
||||
export function Component() {
|
||||
return <div />
|
||||
}
|
||||
`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle type imports", () => {
|
||||
const content1 = `import type { Config } from "@/types/config"`
|
||||
const content2 = `import type { Config } from "~/config"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle namespace imports", () => {
|
||||
const content1 = `import * as React from "react"`
|
||||
const content2 = `import * as React from "react"`
|
||||
expect(isContentSame(content1, content2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle mixed default and named imports", () => {
|
||||
const content1 = `import React, { useState } from "react"`
|
||||
const content2 = `import React, { useState } from "react"`
|
||||
expect(isContentSame(content1, content2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false if non-import content differs", () => {
|
||||
const content1 = `
|
||||
import { Button } from "@/components/ui/button"
|
||||
export const foo = "bar"
|
||||
`
|
||||
const content2 = `
|
||||
import { Button } from "~/ui/button"
|
||||
export const foo = "baz"
|
||||
`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle imports with renamed exports", () => {
|
||||
const content1 = `import { Button as Btn } from "@/components/ui/button"`
|
||||
const content2 = `import { Button as Btn } from "~/ui/button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle multiline imports", () => {
|
||||
const content1 = `import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
ButtonVariants
|
||||
} from "@/components/ui/button"`
|
||||
const content2 = `import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
ButtonVariants
|
||||
} from "~/ui/button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("import comparisons with ignoreImports disabled (default)", () => {
|
||||
it("should return false for different aliased imports", () => {
|
||||
const content1 = `import { Button } from "@/components/ui/button"`
|
||||
const content2 = `import { Button } from "~/ui/button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: false })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it("should return false for different aliased imports by default", () => {
|
||||
const content1 = `import { Button } from "@/components/ui/button"`
|
||||
const content2 = `import { Button } from "~/ui/button"`
|
||||
expect(isContentSame(content1, content2)).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true only for exact matches", () => {
|
||||
const content1 = `import { Button } from "@/components/ui/button"`
|
||||
const content2 = `import { Button } from "@/components/ui/button"`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: false })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("should still normalize line endings and whitespace", () => {
|
||||
const content1 = `import { Button } from "@/components/ui/button"\r\n`
|
||||
const content2 = `import { Button } from "@/components/ui/button"\n`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: false })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("complex real-world scenarios", () => {
|
||||
it("should handle React component with different import aliases", () => {
|
||||
const component1 = `
|
||||
import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
|
||||
export function ProfileCard({ className }: { className?: string }) {
|
||||
return (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>Profile</CardHeader>
|
||||
<CardContent>
|
||||
<Button>Click me</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
`
|
||||
const component2 = `
|
||||
import * as React from "react"
|
||||
import { cn } from "~/utils"
|
||||
import { Button } from "~/ui/button"
|
||||
import { Card, CardContent, CardHeader } from "#/components/card"
|
||||
|
||||
export function ProfileCard({ className }: { className?: string }) {
|
||||
return (
|
||||
<Card className={cn("w-full", className)}>
|
||||
<CardHeader>Profile</CardHeader>
|
||||
<CardContent>
|
||||
<Button>Click me</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
`
|
||||
expect(
|
||||
isContentSame(component1, component2, { ignoreImports: true })
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect actual code differences", () => {
|
||||
const component1 = `
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function Component() {
|
||||
return <Button variant="default">Click</Button>
|
||||
}
|
||||
`
|
||||
const component2 = `
|
||||
import { Button } from "~/ui/button"
|
||||
|
||||
export function Component() {
|
||||
return <Button variant="outline">Click</Button>
|
||||
}
|
||||
`
|
||||
expect(
|
||||
isContentSame(component1, component2, { ignoreImports: true })
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle files with no imports", () => {
|
||||
const content1 = `
|
||||
export function add(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
`
|
||||
const content2 = `
|
||||
export function add(a: number, b: number) {
|
||||
return a + b
|
||||
}
|
||||
`
|
||||
expect(isContentSame(content1, content2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle CSS imports", () => {
|
||||
const content1 = `
|
||||
import styles from "@/styles/component.module.css"
|
||||
import "./global.css"
|
||||
`
|
||||
const content2 = `
|
||||
import styles from "~/styles/component.module.css"
|
||||
import "./global.css"
|
||||
`
|
||||
expect(isContentSame(content1, content2, { ignoreImports: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
61
packages/shadcn/src/utils/compare.ts
Normal file
61
packages/shadcn/src/utils/compare.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export function isContentSame(
|
||||
existingContent: string,
|
||||
newContent: string,
|
||||
options: {
|
||||
ignoreImports?: boolean
|
||||
} = {}
|
||||
) {
|
||||
const { ignoreImports = false } = options
|
||||
|
||||
// Normalize line endings and whitespace.
|
||||
const normalizedExisting = existingContent.replace(/\r\n/g, "\n").trim()
|
||||
const normalizedNew = newContent.replace(/\r\n/g, "\n").trim()
|
||||
|
||||
// First, try exact match after normalization.
|
||||
if (normalizedExisting === normalizedNew) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If not ignoring imports or exact match failed, return false
|
||||
if (!ignoreImports) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare with import statements normalized.
|
||||
// This regex matches various import patterns including:
|
||||
// - import defaultExport from "module"
|
||||
// - import * as name from "module"
|
||||
// - import { export1, export2 } from "module"
|
||||
// - import { export1 as alias1 } from "module"
|
||||
// - import defaultExport, { export1 } from "module"
|
||||
// - import type { Type } from "module"
|
||||
// - This Regex written by Claude Code.
|
||||
const importRegex =
|
||||
/^(import\s+(?:type\s+)?(?:\*\s+as\s+\w+|\{[^}]*\}|\w+)?(?:\s*,\s*(?:\{[^}]*\}|\w+))?\s+from\s+["'])([^"']+)(["'])/gm
|
||||
|
||||
// Function to normalize import paths - remove alias differences.
|
||||
const normalizeImports = (content: string) => {
|
||||
return content.replace(
|
||||
importRegex,
|
||||
(_match, prefix, importPath, suffix) => {
|
||||
// Keep relative imports as-is.
|
||||
if (importPath.startsWith(".")) {
|
||||
return `${prefix}${importPath}${suffix}`
|
||||
}
|
||||
|
||||
// For aliased imports, normalize to a common format.
|
||||
// Extract the last meaningful part of the path.
|
||||
const parts = importPath.split("/")
|
||||
const lastPart = parts[parts.length - 1]
|
||||
|
||||
// Normalize to a consistent format.
|
||||
return `${prefix}@normalized/${lastPart}${suffix}`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const existingNormalized = normalizeImports(normalizedExisting)
|
||||
const newNormalized = normalizeImports(normalizedNew)
|
||||
|
||||
return existingNormalized === newNormalized
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { tmpdir } from "os"
|
||||
import path, { basename } from "path"
|
||||
import { getRegistryBaseColor } from "@/src/registry/api"
|
||||
import { RegistryItem, registryItemFileSchema } from "@/src/registry/schema"
|
||||
import { isContentSame } from "@/src/utils/compare"
|
||||
import {
|
||||
findExistingEnvFile,
|
||||
getNewEnvKeys,
|
||||
@@ -36,6 +37,7 @@ export async function updateFiles(
|
||||
silent?: boolean
|
||||
rootSpinner?: ReturnType<typeof spinner>
|
||||
isRemote?: boolean
|
||||
isWorkspace?: boolean
|
||||
}
|
||||
) {
|
||||
if (!files?.length) {
|
||||
@@ -50,6 +52,7 @@ export async function updateFiles(
|
||||
force: false,
|
||||
silent: false,
|
||||
isRemote: false,
|
||||
isWorkspace: false,
|
||||
...options,
|
||||
}
|
||||
const filesCreatedSpinner = spinner(`Updating files.`, {
|
||||
@@ -131,11 +134,14 @@ export async function updateFiles(
|
||||
// Exception: Don't skip .env files as we merge content instead of replacing
|
||||
if (existingFile && !isEnvFile(filePath)) {
|
||||
const existingFileContent = await fs.readFile(filePath, "utf-8")
|
||||
const [normalizedExisting, normalizedNew] = await Promise.all([
|
||||
getNormalizedFileContent(existingFileContent),
|
||||
getNormalizedFileContent(content),
|
||||
])
|
||||
if (normalizedExisting === normalizedNew) {
|
||||
|
||||
if (
|
||||
isContentSame(existingFileContent, content, {
|
||||
// Ignore import differences for workspace components.
|
||||
// TODO: figure out if we always want this.
|
||||
ignoreImports: options.isWorkspace,
|
||||
})
|
||||
) {
|
||||
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||
continue
|
||||
}
|
||||
@@ -410,10 +416,6 @@ export function resolveNestedFilePath(
|
||||
return fileSegments.slice(commonDirIndex + 1).join("/")
|
||||
}
|
||||
|
||||
export async function getNormalizedFileContent(content: string) {
|
||||
return content.replace(/\r\n/g, "\n").trim()
|
||||
}
|
||||
|
||||
export function resolvePageTarget(
|
||||
target: string,
|
||||
framework?: ProjectInfo["framework"]["name"]
|
||||
|
||||
Reference in New Issue
Block a user