@tldraw/utils
Version:
tldraw infinite canvas SDK (private utilities).
194 lines (168 loc) • 6.31 kB
text/typescript
interface TldrawLibraryVersion {
name: string
version: string
modules: string
}
interface TldrawLibraryVersionInfo {
versions: TldrawLibraryVersion[]
didWarn: boolean
scheduledNotice: number | NodeJS.Timeout | null
}
const TLDRAW_LIBRARY_VERSION_KEY = '__TLDRAW_LIBRARY_VERSIONS__' as const
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
declare module globalThis {
export const __TLDRAW_LIBRARY_VERSIONS__: TldrawLibraryVersionInfo
}
function getLibraryVersions(): TldrawLibraryVersionInfo {
if (globalThis[TLDRAW_LIBRARY_VERSION_KEY]) {
return globalThis[TLDRAW_LIBRARY_VERSION_KEY]
}
const info: TldrawLibraryVersionInfo = {
versions: [],
didWarn: false,
scheduledNotice: null,
}
Object.defineProperty(globalThis, TLDRAW_LIBRARY_VERSION_KEY, {
value: info,
writable: false,
configurable: false,
enumerable: false,
})
return info
}
export function clearRegisteredVersionsForTests() {
const info = getLibraryVersions()
info.versions = []
info.didWarn = false
if (info.scheduledNotice) {
clearTimeout(info.scheduledNotice)
info.scheduledNotice = null
}
}
/** @internal */
export function registerTldrawLibraryVersion(name?: string, version?: string, modules?: string) {
if (!name || !version || !modules) {
if ((globalThis as any).TLDRAW_LIBRARY_IS_BUILD) {
throw new Error('Missing name/version/module system in built version of tldraw library')
}
return
}
const info = getLibraryVersions()
info.versions.push({ name, version, modules })
if (!info.scheduledNotice) {
try {
// eslint-disable-next-line no-restricted-globals
info.scheduledNotice = setTimeout(() => {
info.scheduledNotice = null
checkLibraryVersions(info)
}, 100)
} catch {
// some environments (e.g. cloudflare workers) don't support setTimeout immediately, only in a handler.
// in this case, we'll just check immediately.
checkLibraryVersions(info)
}
}
}
function checkLibraryVersions(info: TldrawLibraryVersionInfo) {
if (!info.versions.length) return
if (info.didWarn) return
const sorted = info.versions.sort((a, b) => compareVersions(a.version, b.version))
const latestVersion = sorted[sorted.length - 1].version
const matchingVersions = new Set<string>()
const nonMatchingVersions = new Map<string, Set<string>>()
for (const lib of sorted) {
if (nonMatchingVersions.has(lib.name)) {
matchingVersions.delete(lib.name)
entry(nonMatchingVersions, lib.name, new Set()).add(lib.version)
continue
}
if (lib.version === latestVersion) {
matchingVersions.add(lib.name)
} else {
matchingVersions.delete(lib.name)
entry(nonMatchingVersions, lib.name, new Set()).add(lib.version)
}
}
if (nonMatchingVersions.size > 0) {
const message = [
`${format('[tldraw]', ['bold', 'bgRed', 'textWhite'])} ${format('You have multiple versions of tldraw libraries installed. This can lead to bugs and unexpected behavior.', ['textRed', 'bold'])}`,
'',
`The latest version you have installed is ${format(`v${latestVersion}`, ['bold', 'textBlue'])}. The following libraries are on the latest version:`,
...Array.from(matchingVersions, (name) => ` • ✅ ${format(name, ['bold'])}`),
'',
`The following libraries are not on the latest version, or have multiple versions installed:`,
...Array.from(nonMatchingVersions, ([name, versions]) => {
const sortedVersions = Array.from(versions)
.sort(compareVersions)
.map((v) => format(`v${v}`, v === latestVersion ? ['textGreen'] : ['textRed']))
return ` • ❌ ${format(name, ['bold'])} (${sortedVersions.join(', ')})`
}),
]
// eslint-disable-next-line no-console
console.log(message.join('\n'))
info.didWarn = true
return
}
// at this point, we know that everything has the same version. there may still be duplicates though!
const potentialDuplicates = new Map<string, { version: string; modules: string[] }>()
for (const lib of sorted) {
entry(potentialDuplicates, lib.name, { version: lib.version, modules: [] }).modules.push(
lib.modules
)
}
const duplicates = new Map<string, { version: string; modules: string[] }>()
for (const [name, lib] of potentialDuplicates) {
if (lib.modules.length > 1) duplicates.set(name, lib)
}
if (duplicates.size > 0) {
const message = [
`${format('[tldraw]', ['bold', 'bgRed', 'textWhite'])} ${format('You have multiple instances of some tldraw libraries active. This can lead to bugs and unexpected behavior. ', ['textRed', 'bold'])}`,
'',
'This usually means that your bundler is misconfigured, and is importing the same library multiple times - usually once as an ES Module, and once as a CommonJS module.',
'',
'The following libraries have been imported multiple times:',
...Array.from(duplicates, ([name, lib]) => {
const modules = lib.modules
.map((m, i) => (m === 'esm' ? ` ${i + 1}. ES Modules` : ` ${i + 1}. CommonJS`))
.join('\n')
return ` • ❌ ${format(name, ['bold'])} v${lib.version}: \n${modules}`
}),
'',
'You should configure your bundler to only import one version of each library.',
]
// eslint-disable-next-line no-console
console.log(message.join('\n'))
info.didWarn = true
return
}
}
function compareVersions(a: string, b: string) {
const aMatch = a.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/)
const bMatch = b.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/)
if (!aMatch || !bMatch) return a.localeCompare(b)
if (aMatch[1] !== bMatch[1]) return Number(aMatch[1]) - Number(bMatch[1])
if (aMatch[2] !== bMatch[2]) return Number(aMatch[2]) - Number(bMatch[2])
if (aMatch[3] !== bMatch[3]) return Number(aMatch[3]) - Number(bMatch[3])
if (aMatch[4] && bMatch[4]) return aMatch[4].localeCompare(bMatch[4])
if (aMatch[4]) return 1
if (bMatch[4]) return -1
return 0
}
const formats = {
bold: '1',
textBlue: '94',
textRed: '31',
textGreen: '32',
bgRed: '41',
textWhite: '97',
} as const
function format(value: string, formatters: (keyof typeof formats)[] = []) {
return `\x1B[${formatters.map((f) => formats[f]).join(';')}m${value}\x1B[m`
}
function entry<K, V>(map: Map<K, V>, key: K, defaultValue: V): V {
if (map.has(key)) {
return map.get(key)!
}
map.set(key, defaultValue)
return defaultValue
}