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
238 lines
6.9 KiB
TypeScript
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)
|
|
})
|