Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/add-framer-rewrites.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Add Framer Rewrites

on:
workflow_dispatch:

permissions:
contents: write
pull-requests: write

env:
CI: 1

defaults:
run:
shell: bash

jobs:
add-rewrites:
name: 'Add Framer Rewrites'
timeout-minutes: 10
runs-on: ubuntu-latest

steps:
- name: Check out code
uses: actions/checkout@v3

- name: Run our setup
uses: ./.github/actions/setup

- name: Check for changes before running script
id: before
run: |
echo "BEFORE_HASH=$(md5sum apps/docs/next.config.js | cut -d' ' -f1)" >> $GITHUB_OUTPUT

- name: Run add-framer-rewrites script
run: yarn add-framer-rewrites

- name: Check for changes after running script
id: after
run: |
echo "AFTER_HASH=$(md5sum apps/docs/next.config.js | cut -d' ' -f1)" >> $GITHUB_OUTPUT

- name: Create PR if changes detected
if: steps.before.outputs.BEFORE_HASH != steps.after.outputs.AFTER_HASH
env:
GH_TOKEN: ${{ github.token }}
run: |
BRANCH_NAME="framer-rewrites-$(date +%Y%m%d-%H%M%S)"

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git checkout -b "$BRANCH_NAME"
git add apps/docs/next.config.js
git commit -m "Add missing Framer rewrites to next.config.js"
git push origin "$BRANCH_NAME"

gh pr create \
--title "Add missing Framer rewrites" \
--body "Adds missing rewrites for new Framer marketing pages to \`apps/docs/next.config.js\`.

### Change type

- [x] \`improvement\`" \
--label "docs-hotfix-please"

- name: No changes detected
if: steps.before.outputs.BEFORE_HASH == steps.after.outputs.AFTER_HASH
run: |
echo "✔ No changes detected. All Framer paths already have rewrites configured."
191 changes: 191 additions & 0 deletions apps/docs/scripts/add-framer-rewrites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import * as fs from 'fs'
import * as path from 'path'

const { log: nicelog } = console

const REWRITE_DOMAIN = 'tldrawdotdev.framer.website'
const SITEMAP_URL = `https://${REWRITE_DOMAIN}/sitemap.xml`

async function fetchFramerSitemap(): Promise<string[]> {
const response = await fetch(SITEMAP_URL)
if (!response.ok) {
throw new Error(`Failed to fetch sitemap: ${response.statusText}`)
}

const xml = await response.text()
const locRegex = /<loc>([^<]+)<\/loc>/g
const paths: string[] = []

let match
while ((match = locRegex.exec(xml)) !== null) {
const url = match[1]
const urlObj = new URL(url)
paths.push(urlObj.pathname)
}

return paths
}

function getCurrentRewrites(configPath: string): Set<string> {
const configContent = fs.readFileSync(configPath, 'utf-8')
const sourceRegex = /source:\s*['"]([^'"]+)['"]/g
const rewrites = new Set<string>()

let match
while ((match = sourceRegex.exec(configContent)) !== null) {
rewrites.add(match[1])
}

return rewrites
}

function pathMatchesRewrite(p: string, rewritePattern: string): boolean {
const normalizedPath = p.replace(/\/$/, '')
const normalizedPattern = rewritePattern.replace(/\/$/, '')

if (normalizedPath === normalizedPattern) {
return true
}

if (normalizedPattern.includes(':path*')) {
const prefix = normalizedPattern.split('/:path*')[0]
return normalizedPath.startsWith(prefix)
}
if (normalizedPattern.includes(':path+')) {
const prefix = normalizedPattern.split('/:path+')[0]
return normalizedPath.startsWith(prefix) && normalizedPath !== prefix
}

if (normalizedPattern.includes('/:')) {
const parts = normalizedPattern.split('/')
const pathParts = normalizedPath.split('/')

if (parts.length !== pathParts.length) {
return false
}

return parts.every((part, i) => {
if (part.startsWith(':')) {
return true
}
return part === pathParts[i]
})
}

return false
}

function findMissingRewrites(framerPaths: string[], currentRewrites: Set<string>): string[] {
return framerPaths.filter((p) => {
for (const rewrite of currentRewrites) {
if (pathMatchesRewrite(p, rewrite)) {
return false
}
}
return true
})
}

function addRewritesToConfig(configPath: string, missingRewrites: string[]): void {
let configContent = fs.readFileSync(configPath, 'utf-8')

// Find the beforeFiles array
const beforeFilesStart = configContent.indexOf('beforeFiles: [')
const beforeFilesEnd = configContent.indexOf('],', beforeFilesStart)

if (beforeFilesStart === -1 || beforeFilesEnd === -1) {
throw new Error('Could not find beforeFiles array in next.config.js')
}

// Extract current beforeFiles content
const beforeFilesContent = configContent.substring(
beforeFilesStart + 'beforeFiles: ['.length,
beforeFilesEnd
)

// Parse existing rewrites
const existingRewrites: Array<{ source: string; destination: string }> = []
const rewriteBlockRegex =
/\{\s*source:\s*['"]([^'"]+)['"]\s*,\s*destination:\s*[`'"]([^`'"]+)[`'"]\s*,?\s*\}/g

let match
while ((match = rewriteBlockRegex.exec(beforeFilesContent)) !== null) {
existingRewrites.push({ source: match[1], destination: match[2] })
}

// For each missing rewrite, find where to insert it alphabetically
const sortedMissing = [...missingRewrites].sort()

for (const path of sortedMissing) {
// Find the insertion point
let insertIndex = 0
for (let i = 0; i < existingRewrites.length; i++) {
if (path.localeCompare(existingRewrites[i].source) < 0) {
insertIndex = i
break
}
insertIndex = i + 1
}

// Insert at the found position
existingRewrites.splice(insertIndex, 0, {
source: path,
destination: `https://\${REWRITE_DOMAIN}${path}`,
})
}

// Rebuild the array with consistent formatting
const rewriteEntries = existingRewrites
.map((r) => {
// Use template literal only if there's interpolation
const destQuote = r.destination.includes('${') ? '`' : "'"
return `\t\t\t\t{\n\t\t\t\t\tsource: '${r.source}',\n\t\t\t\t\tdestination: ${destQuote}${r.destination}${destQuote},\n\t\t\t\t}`
})
.join(',\n')

const newBeforeFiles = `beforeFiles: [\n${rewriteEntries},\n\t\t\t],`

configContent =
configContent.substring(0, beforeFilesStart) +
newBeforeFiles +
configContent.substring(beforeFilesEnd + 2)

fs.writeFileSync(configPath, configContent, 'utf-8')
}

async function addMissingRewrites() {
nicelog('• Fetching Framer sitemap...')

const framerPaths = await fetchFramerSitemap()
const configPath = path.join(__dirname, '../next.config.js')
const currentRewrites = getCurrentRewrites(configPath)
const missingRewrites = findMissingRewrites(framerPaths, currentRewrites)

nicelog(`• Found ${framerPaths.length} paths in Framer sitemap`)
nicelog(`• Found ${currentRewrites.size} existing rewrites in config`)

if (missingRewrites.length === 0) {
nicelog('✔ No missing rewrites found')
return { added: 0, paths: [] }
}

nicelog(`• Adding ${missingRewrites.length} missing rewrites...`)
addRewritesToConfig(configPath, missingRewrites)

nicelog('✔ Successfully added missing rewrites')

return { added: missingRewrites.length, paths: missingRewrites }
}

if (import.meta.url === `file://${process.argv[1]}`) {
addMissingRewrites()
.then(() => {
process.exit(0)
})
.catch((error) => {
console.error('Error:', error)
process.exit(1)
})
}

export { addMissingRewrites }
68 changes: 22 additions & 46 deletions apps/dotcom/client/src/tla/app/TldrawApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,27 +202,14 @@ export class TldrawApp {
.related('groupMembers')
}

async preload(initialUserData: TlaUser) {
let didCreate = false
async preload() {
await this.userQuery().preload().complete
await this.changesFlushed
if (!this.user$.get()) {
didCreate = true

// Always use new groups initialization for new users (protocol v3+)
await this.z.mutate.init({ user: initialUserData, time: Date.now() })

updateLocalSessionState((state) => ({ ...state, shouldShowWelcomeDialog: true }))
}
await new Promise((resolve) => {
let unlisten = () => {}
unlisten = react('wait for user', () => this.user$.get() && resolve(unlisten()))
})
if (!this.user$.get()) {
throw Error('could not create user')
}
await this.fileStateQuery().preload().complete
return didCreate
}

messages = defineMessages({
Expand Down Expand Up @@ -764,9 +751,6 @@ export class TldrawApp {

static async create(opts: {
userId: string
fullName: string
email: string
avatar: string
getToken(): Promise<string | undefined>
onClientTooOld(): void
trackEvent: TLAppUiContextType
Expand All @@ -779,37 +763,29 @@ export class TldrawApp {
const app = new TldrawApp(opts.userId, opts.getToken, opts.onClientTooOld, opts.trackEvent)
// @ts-expect-error
window.app = app
const didCreate = await app.preload({
id: opts.userId,
name: opts.fullName,
email: opts.email,
color: color ?? defaultUserPreferences.color,
avatar: opts.avatar,
exportFormat: 'png',
exportTheme: 'light',
exportBackground: false,
exportPadding: true,
createdAt: Date.now(),
updatedAt: Date.now(),
flags: '',
allowAnalyticsCookie: null,
...restOfPreferences,
inputMode: restOfPreferences.inputMode ?? null,
locale: restOfPreferences.locale ?? null,
animationSpeed: restOfPreferences.animationSpeed ?? null,
areKeyboardShortcutsEnabled: restOfPreferences.areKeyboardShortcutsEnabled ?? null,
edgeScrollSpeed: restOfPreferences.edgeScrollSpeed ?? null,
colorScheme: restOfPreferences.colorScheme ?? null,
isSnapMode: restOfPreferences.isSnapMode ?? null,
isWrapMode: restOfPreferences.isWrapMode ?? null,
isDynamicSizeMode: restOfPreferences.isDynamicSizeMode ?? null,
isPasteAtCursorMode: restOfPreferences.isPasteAtCursorMode ?? null,
enhancedA11yMode: restOfPreferences.enhancedA11yMode ?? null,
})
if (didCreate) {
await app.preload()
const user = app.getUser()
if (user.color === '___INIT___') {
app.updateUser({
color: color ?? defaultUserPreferences.color,
...restOfPreferences,
inputMode: restOfPreferences.inputMode ?? null,
locale: restOfPreferences.locale ?? null,
animationSpeed: restOfPreferences.animationSpeed ?? null,
areKeyboardShortcutsEnabled: restOfPreferences.areKeyboardShortcutsEnabled ?? null,
edgeScrollSpeed: restOfPreferences.edgeScrollSpeed ?? null,
colorScheme: restOfPreferences.colorScheme ?? null,
isSnapMode: restOfPreferences.isSnapMode ?? null,
isWrapMode: restOfPreferences.isWrapMode ?? null,
isDynamicSizeMode: restOfPreferences.isDynamicSizeMode ?? null,
isPasteAtCursorMode: restOfPreferences.isPasteAtCursorMode ?? null,
enhancedA11yMode: restOfPreferences.enhancedA11yMode ?? null,
})

opts.trackEvent('create-user', { source: 'app' })
updateLocalSessionState((state) => ({ ...state, shouldShowWelcomeDialog: true }))
}
return { app, userId: opts.userId }
return { app, userId: user.id }
}

getIntl() {
Expand Down
3 changes: 0 additions & 3 deletions apps/dotcom/client/src/tla/hooks/useAppState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ export function AppStateProvider({ children }: { children: ReactNode }) {
if (!token) throw new Error('no token')
TldrawApp.create({
userId: auth.userId,
fullName: user.fullName || '',
email: user.emailAddresses[0]?.emailAddress || '',
avatar: user.imageUrl || '',
getToken: async () => {
const token = await auth.getToken()
return token || undefined
Expand Down
Loading
Loading