snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
237
internal/scripts/check-circular-deps.ts
Normal file
237
internal/scripts/check-circular-deps.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user