Files
upstream-tldraw/internal/scripts/check-circular-deps.ts
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

238 lines
6.9 KiB
TypeScript

import { execFile } from 'child_process'
import { access, readdir } from 'fs/promises'
import path from 'path'
import { REPO_ROOT } from './lib/file'
import { nicelog } from './lib/nicelog'
const packageEntry = 'src/index.ts'
async function exists(filePath: string) {
try {
await access(filePath)
return true
} catch {
return false
}
}
async function getPackageEntries() {
const packagesDir = path.join(REPO_ROOT, 'packages')
const packageNames = (await readdir(packagesDir)).sort()
const entries: Record<string, string> = {}
for (const packageName of packageNames) {
const entryPath = path.join(packagesDir, packageName, packageEntry)
if (await exists(entryPath)) {
entries[packageName] = entryPath
}
}
return entries
}
async function getIncludePatternSets(packageName: string) {
const fullPackagePatternSet = [[`**/packages/${packageName}/**/*.{js,jsx,ts,tsx}`]]
if (packageName !== 'tldraw') return fullPackagePatternSet
// NOTE:
// `vite-plugin-circular-dependency` crashes with "Invalid array length" on tldraw when
// `ui/components` and `ui/context` are scanned together in a single build graph.
// To keep checks reliable and fast, we build tldraw in a few dynamic batches:
// - all non-ui scopes together
// - ui/components alone
// - ui/context alone
// - remaining ui scopes together
// These batches are discovered from the filesystem to avoid brittle hardcoded scope lists.
const tldrawLibDir = path.join(REPO_ROOT, 'packages', 'tldraw', 'src', 'lib')
const tldrawEntries = (await readdir(tldrawLibDir, { withFileTypes: true })).sort((a, b) =>
a.name.localeCompare(b.name)
)
const nonUiPatterns: string[] = []
let uiComponentsPattern: string | null = null
let uiContextPattern: string | null = null
const uiOtherPatterns: string[] = []
for (const entry of tldrawEntries.filter((entry) => !entry.name.startsWith('.'))) {
if (!entry.isDirectory()) {
if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
nonUiPatterns.push(`**/packages/tldraw/src/lib/${entry.name}`)
}
continue
}
if (entry.name !== 'ui') {
nonUiPatterns.push(`**/packages/tldraw/src/lib/${entry.name}/**/*.{js,jsx,ts,tsx}`)
continue
}
const tldrawUiDir = path.join(tldrawLibDir, 'ui')
const tldrawUiEntries = (await readdir(tldrawUiDir, { withFileTypes: true })).sort((a, b) =>
a.name.localeCompare(b.name)
)
for (const uiEntry of tldrawUiEntries.filter((uiEntry) => !uiEntry.name.startsWith('.'))) {
const uiPattern = uiEntry.isDirectory()
? `**/packages/tldraw/src/lib/ui/${uiEntry.name}/**/*.{js,jsx,ts,tsx}`
: /\.(ts|tsx|js|jsx)$/.test(uiEntry.name)
? `**/packages/tldraw/src/lib/ui/${uiEntry.name}`
: null
if (!uiPattern) continue
if (uiEntry.name === 'components') {
uiComponentsPattern = uiPattern
} else if (uiEntry.name === 'context') {
uiContextPattern = uiPattern
} else {
uiOtherPatterns.push(uiPattern)
}
}
}
const includePatternSets = [
['**/packages/tldraw/src/index.ts', ...nonUiPatterns],
...(uiComponentsPattern ? [['**/packages/tldraw/src/index.ts', uiComponentsPattern]] : []),
...(uiContextPattern ? [['**/packages/tldraw/src/index.ts', uiContextPattern]] : []),
...(uiOtherPatterns.length > 0
? [['**/packages/tldraw/src/index.ts', ...uiOtherPatterns]]
: []),
]
return includePatternSets
}
async function checkPackageForCircularDependencies(packageName: string, packageEntryPath: string) {
const includePatternSets = await getIncludePatternSets(packageName)
const script = `
import { build } from 'vite'
import circularDependencyChecker from 'vite-plugin-circular-dependency'
import { mkdirSync, readFileSync, rmSync } from 'fs'
import path from 'path'
const packageName = process.argv[1]
const packageEntryPath = process.argv[2]
const includePatternSets = JSON.parse(process.argv[3])
const maxConcurrency = packageName === 'tldraw' ? 2 : 1
async function checkIncludeSet(includePatterns) {
const outputFilePath = path.join(
'.lazy',
'circle-deps',
packageName + '-' + Date.now() + '-' + Math.random().toString(36).slice(2) + '.json'
)
mkdirSync(path.dirname(outputFilePath), { recursive: true })
let cycleReport = {}
try {
await build({
root: ${JSON.stringify(REPO_ROOT)},
logLevel: 'error',
plugins: [
circularDependencyChecker({
circleImportThrowErr: false,
outputFilePath,
include: includePatterns,
}),
],
build: {
write: false,
emptyOutDir: false,
minify: false,
target: 'es2022',
rollupOptions: {
input: {
[packageName]: packageEntryPath,
},
onwarn(warning, warn) {
if (warning.code === 'MODULE_LEVEL_DIRECTIVE') return
warn(warning)
},
},
},
})
cycleReport = JSON.parse(readFileSync(outputFilePath, 'utf8'))
} finally {
rmSync(outputFilePath, { force: true })
}
const cycleSignatures = Object.values(cycleReport)
.flat()
.map((chain) => chain.join(' -> '))
if (cycleSignatures.length > 0) {
console.error(
'Found circular dependencies:\\n' +
cycleSignatures.map((signature) => ' ' + signature).join('\\n')
)
process.exit(1)
}
}
const queue = [...includePatternSets]
const workers = Array.from({ length: Math.min(maxConcurrency, queue.length) }, async () => {
while (queue.length > 0) {
const includePatterns = queue.shift()
if (!includePatterns) return
await checkIncludeSet(includePatterns)
}
})
await Promise.all(workers)
`
return new Promise<void>((resolve, reject) => {
const child = execFile(
'node',
[
'--input-type=module',
'-e',
script,
packageName,
packageEntryPath,
JSON.stringify(includePatternSets),
],
{ cwd: REPO_ROOT },
(error) => {
if (error) reject(error)
else resolve()
}
)
child.stdout?.on('data', (chunk) => process.stdout.write(chunk))
child.stderr?.on('data', (chunk) => process.stderr.write(chunk))
})
}
async function main() {
const entries = await getPackageEntries()
const packageNames = Object.keys(entries)
nicelog(`Checking for circular dependencies in packages (${packageNames.length}):`)
nicelog(packageNames.join(', '))
const packageCheckResults = await Promise.allSettled(
packageNames.map(async (packageName) => {
const packageEntryPath = entries[packageName]
nicelog(`Checking ${packageName}...`)
await checkPackageForCircularDependencies(packageName, packageEntryPath)
})
)
const failedPackages = packageCheckResults.flatMap((result, index) =>
result.status === 'rejected' ? [packageNames[index]] : []
)
if (failedPackages.length > 0) {
console.error(
`Circular dependency checks failed for ${failedPackages.length} package(s): ${failedPackages.join(', ')}`
)
process.exit(1)
}
nicelog(`No circular dependencies detected across ${packageNames.length} packages.`)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})