ban using @internal
items in examples (#3746)
There's been some confusion in the community as our example use a few `@internal` methods. These things are intended for use inside the tldraw library, but aren't a part of the public API. That means that when those examples are copied out of the tldraw repo, those `@internal` references produce errors. This diff bans the use of items tagged as `@internal` inside our examples app by adding an eslint plugin (adapted from the one we already have that protects against deprecated types) preventing them. ### Change Type - [x] `docs` — Changes to the documentation, examples, or templates. - [x] `bugfix` — Bug fix - [x] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc.
This commit is contained in:
parent
7226afc1ff
commit
5a15c49d63
11 changed files with 213 additions and 21 deletions
|
@ -100,6 +100,7 @@ module.exports = {
|
||||||
files: ['apps/examples/**/*'],
|
files: ['apps/examples/**/*'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-syntax': 'off',
|
'no-restricted-syntax': 'off',
|
||||||
|
'local/no-at-internal': 'error',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {
|
import {
|
||||||
DefaultSizeStyle,
|
DefaultSizeStyle,
|
||||||
SharedStyleMap,
|
|
||||||
Tldraw,
|
Tldraw,
|
||||||
TldrawUiIcon,
|
TldrawUiIcon,
|
||||||
TLEditorComponents,
|
TLEditorComponents,
|
||||||
|
@ -27,8 +26,7 @@ const ContextToolbarComponent = track(() => {
|
||||||
if (!selectionRotatedPageBounds) return null
|
if (!selectionRotatedPageBounds) return null
|
||||||
|
|
||||||
// [2]
|
// [2]
|
||||||
const styles = new SharedStyleMap(editor.getSharedStyles())
|
const size = editor.getSharedStyles().get(DefaultSizeStyle)
|
||||||
const size = styles.get(DefaultSizeStyle)
|
|
||||||
if (!size) return null
|
if (!size) return null
|
||||||
const currentSize = size.type === 'shared' ? size.value : undefined
|
const currentSize = size.type === 'shared' ? size.value : undefined
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PDFDocument } from 'pdf-lib'
|
import { PDFDocument } from 'pdf-lib'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Editor, assert, exportToBlob, useEditor } from 'tldraw'
|
import { Editor, exportToBlob, useEditor } from 'tldraw'
|
||||||
import { Pdf } from './PdfPicker'
|
import { Pdf } from './PdfPicker'
|
||||||
|
|
||||||
export function ExportPdfButton({ pdf }: { pdf: Pdf }) {
|
export function ExportPdfButton({ pdf }: { pdf: Pdf }) {
|
||||||
|
@ -40,7 +40,9 @@ async function exportPdf(
|
||||||
tickProgress()
|
tickProgress()
|
||||||
|
|
||||||
const pdfPages = pdf.getPages()
|
const pdfPages = pdf.getPages()
|
||||||
assert(pdfPages.length === pages.length, 'PDF page count mismatch')
|
if (pdfPages.length !== pages.length) {
|
||||||
|
throw new Error('PDF page count mismatch')
|
||||||
|
}
|
||||||
|
|
||||||
const pageShapeIds = new Set(pages.map((page) => page.shapeId))
|
const pageShapeIds = new Set(pages.map((page) => page.shapeId))
|
||||||
const allIds = Array.from(editor.getCurrentPageShapeIds()).filter((id) => !pageShapeIds.has(id))
|
const allIds = Array.from(editor.getCurrentPageShapeIds()).filter((id) => !pageShapeIds.has(id))
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
DEFAULT_CAMERA_OPTIONS,
|
|
||||||
SVGContainer,
|
SVGContainer,
|
||||||
TLComponents,
|
TLComponents,
|
||||||
TLImageShape,
|
TLImageShape,
|
||||||
TLShapePartial,
|
TLShapePartial,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
compact,
|
|
||||||
getIndicesBetween,
|
getIndicesBetween,
|
||||||
react,
|
react,
|
||||||
sortByIndex,
|
sortByIndex,
|
||||||
|
@ -118,7 +116,6 @@ export function PdfEditor({ pdf }: { pdf: Pdf }) {
|
||||||
|
|
||||||
function updateCameraBounds(isMobile: boolean) {
|
function updateCameraBounds(isMobile: boolean) {
|
||||||
editor.setCameraOptions({
|
editor.setCameraOptions({
|
||||||
...DEFAULT_CAMERA_OPTIONS,
|
|
||||||
constraints: {
|
constraints: {
|
||||||
bounds: targetBounds,
|
bounds: targetBounds,
|
||||||
padding: { x: isMobile ? 16 : 164, y: 64 },
|
padding: { x: isMobile ? 16 : 164, y: 64 },
|
||||||
|
@ -153,14 +150,14 @@ const PageOverlayScreen = track(function PageOverlayScreen({ pdf }: { pdf: Pdf }
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
const viewportPageBounds = editor.getViewportPageBounds()
|
||||||
const viewportScreenBounds = editor.getViewportScreenBounds()
|
const viewportScreenBounds = editor.getViewportScreenBounds()
|
||||||
|
|
||||||
const relevantPageBounds = compact(
|
const relevantPageBounds = pdf.pages
|
||||||
pdf.pages.map((page) => {
|
.map((page) => {
|
||||||
if (!viewportPageBounds.collides(page.bounds)) return null
|
if (!viewportPageBounds.collides(page.bounds)) return null
|
||||||
const topLeft = editor.pageToViewport(page.bounds)
|
const topLeft = editor.pageToViewport(page.bounds)
|
||||||
const bottomRight = editor.pageToViewport({ x: page.bounds.maxX, y: page.bounds.maxY })
|
const bottomRight = editor.pageToViewport({ x: page.bounds.maxX, y: page.bounds.maxY })
|
||||||
return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
|
return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
|
||||||
})
|
})
|
||||||
)
|
.filter((bounds): bounds is Box => bounds !== null)
|
||||||
|
|
||||||
function pathForPageBounds(bounds: Box) {
|
function pathForPageBounds(bounds: Box) {
|
||||||
return `M ${bounds.x} ${bounds.y} L ${bounds.maxX} ${bounds.y} L ${bounds.maxX} ${bounds.maxY} L ${bounds.x} ${bounds.maxY} Z`
|
return `M ${bounds.x} ${bounds.y} L ${bounds.maxX} ${bounds.y} L ${bounds.maxX} ${bounds.maxY} L ${bounds.x} ${bounds.maxY} Z`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { AssetRecordType, Box, TLAssetId, TLShapeId, assertExists, createShapeId } from 'tldraw'
|
import { AssetRecordType, Box, TLAssetId, TLShapeId, createShapeId } from 'tldraw'
|
||||||
import tldrawPdf from './assets/tldraw.pdf'
|
import tldrawPdf from './assets/tldraw.pdf'
|
||||||
|
|
||||||
export type PdfPage = {
|
export type PdfPage = {
|
||||||
|
@ -30,7 +30,8 @@ export function PdfPicker({ onOpenPdf }: { onOpenPdf: (pdf: Pdf) => void }) {
|
||||||
const pages: PdfPage[] = []
|
const pages: PdfPage[] = []
|
||||||
|
|
||||||
const canvas = window.document.createElement('canvas')
|
const canvas = window.document.createElement('canvas')
|
||||||
const context = assertExists(canvas.getContext('2d'))
|
const context = canvas.getContext('2d')
|
||||||
|
if (!context) throw new Error('Failed to create canvas context')
|
||||||
|
|
||||||
const visualScale = 1.5
|
const visualScale = 1.5
|
||||||
const scale = window.devicePixelRatio
|
const scale = window.devicePixelRatio
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
DEFAULT_CAMERA_OPTIONS,
|
|
||||||
Editor,
|
Editor,
|
||||||
TLFrameShape,
|
TLFrameShape,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
|
@ -39,7 +38,6 @@ function InsideSlidesContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.setCameraOptions({
|
editor.setCameraOptions({
|
||||||
...DEFAULT_CAMERA_OPTIONS,
|
|
||||||
constraints: {
|
constraints: {
|
||||||
bounds: nextBounds,
|
bounds: nextBounds,
|
||||||
behavior: 'contain',
|
behavior: 'contain',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable local/no-at-internal -- this is only used for our internal develop endpoint */
|
||||||
import { TLUiEventSource, TLUiOverrides, debugFlags, measureCbDuration, useValue } from 'tldraw'
|
import { TLUiEventSource, TLUiOverrides, debugFlags, measureCbDuration, useValue } from 'tldraw'
|
||||||
|
|
||||||
export function usePerformance(): TLUiOverrides {
|
export function usePerformance(): TLUiOverrides {
|
||||||
|
|
|
@ -17,7 +17,9 @@ import EndToEnd from './misc/end-to-end'
|
||||||
// we use secret internal `setDefaultAssetUrls` functions to set these at the
|
// we use secret internal `setDefaultAssetUrls` functions to set these at the
|
||||||
// top-level so assets don't need to be passed down in every single example.
|
// top-level so assets don't need to be passed down in every single example.
|
||||||
const assetUrls = getAssetUrlsByMetaUrl()
|
const assetUrls = getAssetUrlsByMetaUrl()
|
||||||
|
// eslint-disable-next-line local/no-at-internal
|
||||||
setDefaultEditorAssetUrls(assetUrls)
|
setDefaultEditorAssetUrls(assetUrls)
|
||||||
|
// eslint-disable-next-line local/no-at-internal
|
||||||
setDefaultUiAssetUrls(assetUrls)
|
setDefaultUiAssetUrls(assetUrls)
|
||||||
const gettingStartedExamples = examples.find((e) => e.id === 'Getting started')
|
const gettingStartedExamples = examples.find((e) => e.id === 'Getting started')
|
||||||
if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples')
|
if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples')
|
||||||
|
|
|
@ -1297,7 +1297,7 @@ export function serializeTldrawJson(store: TLStore): Promise<string>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function serializeTldrawJsonBlob(store: TLStore): Promise<Blob>;
|
export function serializeTldrawJsonBlob(store: TLStore): Promise<Blob>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @internal (undocumented)
|
||||||
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls): void;
|
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls): void;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
|
|
|
@ -22,7 +22,7 @@ export let defaultEditorAssetUrls: TLEditorAssetUrls = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @internal */
|
||||||
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls) {
|
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls) {
|
||||||
defaultEditorAssetUrls = assetUrls
|
defaultEditorAssetUrls = assetUrls
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
// eslint plugins can't use esm
|
// eslint plugins can't use esm
|
||||||
const { ESLintUtils } =
|
|
||||||
require('@typescript-eslint/utils') as typeof import('@typescript-eslint/utils')
|
// @ts-ignore - no import/require
|
||||||
const { SymbolFlags } = require('typescript') as typeof import('typescript')
|
import ts = require('typescript')
|
||||||
|
// @ts-ignore - no import/require
|
||||||
|
import utils = require('@typescript-eslint/utils')
|
||||||
|
|
||||||
|
const { isReassignmentTarget } = require('tsutils') as typeof import('tsutils')
|
||||||
|
|
||||||
|
const { ESLintUtils } = utils
|
||||||
|
import TSESTree = utils.TSESTree
|
||||||
|
|
||||||
exports.rules = {
|
exports.rules = {
|
||||||
'no-export-star': ESLintUtils.RuleCreator.withoutDocs({
|
'no-export-star': ESLintUtils.RuleCreator.withoutDocs({
|
||||||
|
@ -26,7 +33,7 @@ exports.rules = {
|
||||||
// 3. Find all the imported names from the file
|
// 3. Find all the imported names from the file
|
||||||
const importedNames = checker.getExportsOfModule(importedFileSymbol).map((imported) => ({
|
const importedNames = checker.getExportsOfModule(importedFileSymbol).map((imported) => ({
|
||||||
name: imported.getEscapedName(),
|
name: imported.getEscapedName(),
|
||||||
isType: !(imported.flags & SymbolFlags.Value),
|
isType: !(imported.flags & ts.SymbolFlags.Value),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// report the error and offer a fix (listing imported names)
|
// report the error and offer a fix (listing imported names)
|
||||||
|
@ -103,4 +110,189 @@ exports.rules = {
|
||||||
},
|
},
|
||||||
defaultOptions: [],
|
defaultOptions: [],
|
||||||
}),
|
}),
|
||||||
|
'no-at-internal': ESLintUtils.RuleCreator.withoutDocs({
|
||||||
|
create(context) {
|
||||||
|
// adapted from https://github.com/gund/eslint-plugin-deprecation
|
||||||
|
|
||||||
|
function identifierRule(id: TSESTree.Identifier | TSESTree.JSXIdentifier) {
|
||||||
|
const services = ESLintUtils.getParserServices(context)
|
||||||
|
// Don't consider deprecations in certain cases:
|
||||||
|
|
||||||
|
// - On JSX closing elements (only flag the opening element)
|
||||||
|
const isClosingElement =
|
||||||
|
id.type === 'JSXIdentifier' && id.parent?.type === 'JSXClosingElement'
|
||||||
|
|
||||||
|
if (isClosingElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Inside an import
|
||||||
|
const isInsideImport = context.getAncestors().some((anc) => anc.type.includes('Import'))
|
||||||
|
|
||||||
|
if (isInsideImport) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalMarker = getInternalMarker(id, services)
|
||||||
|
|
||||||
|
if (internalMarker) {
|
||||||
|
context.report({
|
||||||
|
node: id,
|
||||||
|
messageId: 'internal',
|
||||||
|
data: {
|
||||||
|
name: id.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInternalMarker(
|
||||||
|
id: TSESTree.Identifier | TSESTree.JSXIdentifier,
|
||||||
|
services: utils.ParserServices
|
||||||
|
) {
|
||||||
|
const tc = services.program.getTypeChecker()
|
||||||
|
const callExpression = getCallExpression(id)
|
||||||
|
|
||||||
|
if (callExpression) {
|
||||||
|
const tsCallExpression = services.esTreeNodeToTSNodeMap.get(
|
||||||
|
callExpression
|
||||||
|
) as ts.CallLikeExpression
|
||||||
|
const signature = tc.getResolvedSignature(tsCallExpression)
|
||||||
|
if (signature) {
|
||||||
|
const deprecation = getJsDocInternal(signature.getJsDocTags())
|
||||||
|
if (deprecation) {
|
||||||
|
return deprecation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = getSymbol(id, services, tc)
|
||||||
|
|
||||||
|
if (!symbol) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (callExpression && isFunction(symbol)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return getJsDocInternal(symbol.getJsDocTags())
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFunction(symbol: ts.Symbol) {
|
||||||
|
const { declarations } = symbol
|
||||||
|
if (declarations === undefined || declarations.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch (declarations[0].kind) {
|
||||||
|
case ts.SyntaxKind.MethodDeclaration:
|
||||||
|
case ts.SyntaxKind.FunctionDeclaration:
|
||||||
|
case ts.SyntaxKind.FunctionExpression:
|
||||||
|
case ts.SyntaxKind.MethodSignature:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallExpression(
|
||||||
|
id: TSESTree.Node
|
||||||
|
): TSESTree.CallExpression | TSESTree.TaggedTemplateExpression | undefined {
|
||||||
|
const ancestors = context.getAncestors()
|
||||||
|
let callee = id
|
||||||
|
let parent = ancestors.length > 0 ? ancestors[ancestors.length - 1] : undefined
|
||||||
|
|
||||||
|
if (parent && parent.type === 'MemberExpression' && parent.property === id) {
|
||||||
|
callee = parent
|
||||||
|
parent = ancestors.length > 1 ? ancestors[ancestors.length - 2] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCallExpression(parent, callee)) {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCallExpression(
|
||||||
|
node: TSESTree.Node | undefined,
|
||||||
|
callee: TSESTree.Node
|
||||||
|
): node is TSESTree.CallExpression | TSESTree.TaggedTemplateExpression {
|
||||||
|
if (node) {
|
||||||
|
if (node.type === 'NewExpression' || node.type === 'CallExpression') {
|
||||||
|
return node.callee === callee
|
||||||
|
} else if (node.type === 'TaggedTemplateExpression') {
|
||||||
|
return node.tag === callee
|
||||||
|
} else if (node.type === 'JSXOpeningElement') {
|
||||||
|
return node.name === callee
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsDocInternal(tags: ts.JSDocTagInfo[]) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (tag.name === 'internal') {
|
||||||
|
return { reason: ts.displayPartsToString(tag.text) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSymbol(
|
||||||
|
id: TSESTree.Identifier | TSESTree.JSXIdentifier,
|
||||||
|
services: utils.ParserServices,
|
||||||
|
tc: ts.TypeChecker
|
||||||
|
) {
|
||||||
|
let symbol: ts.Symbol | undefined
|
||||||
|
const tsId = services.esTreeNodeToTSNodeMap.get(id as TSESTree.Node) as ts.Identifier
|
||||||
|
const parent = tsId.parent
|
||||||
|
|
||||||
|
if (parent.kind === ts.SyntaxKind.BindingElement) {
|
||||||
|
symbol = tc.getTypeAtLocation(parent.parent).getProperty(tsId.text)
|
||||||
|
} else if (
|
||||||
|
(isPropertyAssignment(parent) && parent.name === tsId) ||
|
||||||
|
(isShorthandPropertyAssignment(parent) &&
|
||||||
|
parent.name === tsId &&
|
||||||
|
isReassignmentTarget(tsId))
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
symbol = tc.getPropertySymbolOfDestructuringAssignment(tsId)
|
||||||
|
} catch (e) {
|
||||||
|
// we are in object literal, not destructuring
|
||||||
|
// no obvious easy way to check that in advance
|
||||||
|
symbol = tc.getSymbolAtLocation(tsId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
symbol = tc.getSymbolAtLocation(tsId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
||||||
|
symbol = tc.getAliasedSymbol(symbol)
|
||||||
|
}
|
||||||
|
return symbol
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPropertyAssignment(node: ts.Node): node is ts.PropertyAssignment {
|
||||||
|
return node.kind === ts.SyntaxKind.PropertyAssignment
|
||||||
|
}
|
||||||
|
|
||||||
|
function isShorthandPropertyAssignment(
|
||||||
|
node: ts.Node
|
||||||
|
): node is ts.ShorthandPropertyAssignment {
|
||||||
|
return node.kind === ts.SyntaxKind.ShorthandPropertyAssignment
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Identifier: identifierRule,
|
||||||
|
JSXIdentifier: identifierRule,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
messages: {
|
||||||
|
internal: '"{{name}}" is internal and can\'t be used publicly.',
|
||||||
|
},
|
||||||
|
type: 'problem',
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
defaultOptions: [],
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue