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/**/*'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
'local/no-at-internal': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
DefaultSizeStyle,
|
||||
SharedStyleMap,
|
||||
Tldraw,
|
||||
TldrawUiIcon,
|
||||
TLEditorComponents,
|
||||
|
@ -27,8 +26,7 @@ const ContextToolbarComponent = track(() => {
|
|||
if (!selectionRotatedPageBounds) return null
|
||||
|
||||
// [2]
|
||||
const styles = new SharedStyleMap(editor.getSharedStyles())
|
||||
const size = styles.get(DefaultSizeStyle)
|
||||
const size = editor.getSharedStyles().get(DefaultSizeStyle)
|
||||
if (!size) return null
|
||||
const currentSize = size.type === 'shared' ? size.value : undefined
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PDFDocument } from 'pdf-lib'
|
||||
import { useState } from 'react'
|
||||
import { Editor, assert, exportToBlob, useEditor } from 'tldraw'
|
||||
import { Editor, exportToBlob, useEditor } from 'tldraw'
|
||||
import { Pdf } from './PdfPicker'
|
||||
|
||||
export function ExportPdfButton({ pdf }: { pdf: Pdf }) {
|
||||
|
@ -40,7 +40,9 @@ async function exportPdf(
|
|||
tickProgress()
|
||||
|
||||
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 allIds = Array.from(editor.getCurrentPageShapeIds()).filter((id) => !pageShapeIds.has(id))
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { useMemo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
DEFAULT_CAMERA_OPTIONS,
|
||||
SVGContainer,
|
||||
TLComponents,
|
||||
TLImageShape,
|
||||
TLShapePartial,
|
||||
Tldraw,
|
||||
compact,
|
||||
getIndicesBetween,
|
||||
react,
|
||||
sortByIndex,
|
||||
|
@ -118,7 +116,6 @@ export function PdfEditor({ pdf }: { pdf: Pdf }) {
|
|||
|
||||
function updateCameraBounds(isMobile: boolean) {
|
||||
editor.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
bounds: targetBounds,
|
||||
padding: { x: isMobile ? 16 : 164, y: 64 },
|
||||
|
@ -153,14 +150,14 @@ const PageOverlayScreen = track(function PageOverlayScreen({ pdf }: { pdf: Pdf }
|
|||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
const viewportScreenBounds = editor.getViewportScreenBounds()
|
||||
|
||||
const relevantPageBounds = compact(
|
||||
pdf.pages.map((page) => {
|
||||
const relevantPageBounds = pdf.pages
|
||||
.map((page) => {
|
||||
if (!viewportPageBounds.collides(page.bounds)) return null
|
||||
const topLeft = editor.pageToViewport(page.bounds)
|
||||
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)
|
||||
})
|
||||
)
|
||||
.filter((bounds): bounds is Box => bounds !== null)
|
||||
|
||||
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`
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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'
|
||||
|
||||
export type PdfPage = {
|
||||
|
@ -30,7 +30,8 @@ export function PdfPicker({ onOpenPdf }: { onOpenPdf: (pdf: Pdf) => void }) {
|
|||
const pages: PdfPage[] = []
|
||||
|
||||
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 scale = window.devicePixelRatio
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
DEFAULT_CAMERA_OPTIONS,
|
||||
Editor,
|
||||
TLFrameShape,
|
||||
Tldraw,
|
||||
|
@ -39,7 +38,6 @@ function InsideSlidesContext() {
|
|||
}
|
||||
|
||||
editor.setCameraOptions({
|
||||
...DEFAULT_CAMERA_OPTIONS,
|
||||
constraints: {
|
||||
bounds: nextBounds,
|
||||
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'
|
||||
|
||||
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
|
||||
// top-level so assets don't need to be passed down in every single example.
|
||||
const assetUrls = getAssetUrlsByMetaUrl()
|
||||
// eslint-disable-next-line local/no-at-internal
|
||||
setDefaultEditorAssetUrls(assetUrls)
|
||||
// eslint-disable-next-line local/no-at-internal
|
||||
setDefaultUiAssetUrls(assetUrls)
|
||||
const gettingStartedExamples = examples.find((e) => e.id === 'Getting started')
|
||||
if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples')
|
||||
|
|
|
@ -1297,7 +1297,7 @@ export function serializeTldrawJson(store: TLStore): Promise<string>;
|
|||
// @public (undocumented)
|
||||
export function serializeTldrawJsonBlob(store: TLStore): Promise<Blob>;
|
||||
|
||||
// @public (undocumented)
|
||||
// @internal (undocumented)
|
||||
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls): void;
|
||||
|
||||
// @internal (undocumented)
|
||||
|
|
|
@ -22,7 +22,7 @@ export let defaultEditorAssetUrls: TLEditorAssetUrls = {
|
|||
},
|
||||
}
|
||||
|
||||
/** @public */
|
||||
/** @internal */
|
||||
export function setDefaultEditorAssetUrls(assetUrls: TLEditorAssetUrls) {
|
||||
defaultEditorAssetUrls = assetUrls
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
|
||||
// eslint plugins can't use esm
|
||||
const { ESLintUtils } =
|
||||
require('@typescript-eslint/utils') as typeof import('@typescript-eslint/utils')
|
||||
const { SymbolFlags } = require('typescript') as typeof import('typescript')
|
||||
|
||||
// @ts-ignore - no import/require
|
||||
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 = {
|
||||
'no-export-star': ESLintUtils.RuleCreator.withoutDocs({
|
||||
|
@ -26,7 +33,7 @@ exports.rules = {
|
|||
// 3. Find all the imported names from the file
|
||||
const importedNames = checker.getExportsOfModule(importedFileSymbol).map((imported) => ({
|
||||
name: imported.getEscapedName(),
|
||||
isType: !(imported.flags & SymbolFlags.Value),
|
||||
isType: !(imported.flags & ts.SymbolFlags.Value),
|
||||
}))
|
||||
|
||||
// report the error and offer a fix (listing imported names)
|
||||
|
@ -103,4 +110,189 @@ exports.rules = {
|
|||
},
|
||||
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