Set up manual compatibility tests (#171)

This commit is contained in:
Aria 2022-05-20 18:52:57 +02:00 committed by GitHub
parent e28a084dbb
commit d36b336fdd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1862 additions and 13286 deletions

View file

@ -0,0 +1,34 @@
# Compatibility tests
This setup helps us test whether Theatre.js is compatible with popular tools in the JS ecosystem, such as Vite, webpack, react, vue, etc.
> This setup is 3/10 complete.
The currentr workflow is:
1. Run a local npm registry, and build and publish the @theatre/* packages to it. All packages will have the 0.0.1-COMPAT.1 version
```sh
yarn workspace @theatre/compatibility-tests run registry:start
```
This script will keep running until it is terminated. While running, npm will be configured to point to the local registry that contains the `@theatre/*@0.0.1-COMPAT.1` packages.
2. `cd` into any test project (either use those in `./test-*` or make a new npm package elsewhere via `npm init`.)
3. Install `@theatre/*` from the local registry:
```sh
npm install @theatre/core@0.0.1-COMPAT.1 @theatre/studio@0.0.1-COMPAT.1 @theatre/r3f@0.0.1-COMPAT.1
```
4. Test your local build of Thatre.js against your setup by manually building, running a dev server, and using Theatre.js on a browser pointing to the dev server.
Inspired by the
[#help channel on our Discord](https://discord.com/channels/870988717190426644/870988717190426647)
we collect examples for including `Theatre.js` in project that use different
tools (`parcel`, `Next.js`, `vanilla Rollup`, etc...) to build them in the CI
(these are the _compatibility tests_).
> **Gotchas**
> Some bundlers like webpack are not configured to work well with yarn workspaces by default. For example, the webpack config of create-react-app, tries to look up the node_modules chain to find missing dependencies, which is not a behavior that we want in build-tests setups. So if a setup doesn't work, try running it outside the monorepo to see if being in the monorepo is what's causing it to fail.

View file

@ -0,0 +1,16 @@
{
"name": "@theatre/compatibility-tests",
"private": true,
"scripts": {
"registry:start": "zx ./scripts/start-registry.mjs",
"test:ci": "zx ./scripts/ci.mjs"
},
"dependencies": {
"node-cleanup": "^2.1.2",
"prettier": "^2.6.2",
"verdaccio": "^5.10.2",
"verdaccio-auth-memory": "^10.2.0",
"verdaccio-memory": "^10.2.0",
"zx": "^6.1.0"
}
}

View file

@ -0,0 +1,39 @@
/**
* Build the test setups
*/
import path from 'path'
import {colorize, getCompatibilityTestSetups} from './utils.mjs'
const root = path.resolve(__dirname, '../..')
const absPathOfCompatibilityTestSetups = getCompatibilityTestSetups(root)
const setupsWithErros = []
// Try building the setups
;(async function () {
for (const setupDir of absPathOfCompatibilityTestSetups) {
try {
cd(setupDir)
const pathToSetup = path.join(absPathOfCompatibilityTestSetups, setupDir)
fs.removeSync(path.join(pathToSetup, 'node_modules'))
fs.removeSync(path.join(pathToSetup, 'package-lock.json'))
fs.removeSync(path.join(pathToSetup, 'yarn.lock'))
await $`npm install`
await $`npm run build`
} catch (err) {
console.error(err)
setupsWithErros.push(setupDir)
}
}
// Stop if there were any errors during the build process,
// and print all of them to the console.
if (setupsWithErros.length !== 0) {
throw new Error(
`The following setups had problems when their dependencies were being installed:\n${colorize.red(
setupsWithErros.join('\n'),
)}`,
)
}
})()

View file

@ -0,0 +1,5 @@
import {startRegistry} from './utils.mjs'
;(async function runCI() {
await startRegistry()
process.exit(0)
})()

View file

@ -0,0 +1,3 @@
import {startRegistry} from './utils.mjs'
startRegistry()

View file

@ -0,0 +1,261 @@
/**
* Utility functions for the compatibility tests
*/
import fs from 'fs'
import path from 'path'
import {YAML} from 'zx'
import onCleanup from 'node-cleanup'
import * as verdaccioPackage from 'verdaccio'
// 'verdaccio' is not an es module so we have to do this:
const startVerdaccioServer = verdaccioPackage.default.default
export const VERDACCIO_PORT = 4823
export const VERDACCIO_HOST = `localhost`
export const VERDACCIO_URL = `http://${VERDACCIO_HOST}:${VERDACCIO_PORT}/`
export const MONOREPO_ROOT = path.join(__dirname, '../..')
export const PATH_TO_YARNRC = path.join(MONOREPO_ROOT, '.yarnrc.yml')
/**
* This script will:
* 1. Start verdaccio (a local npm registry),
* 2. Configure npm (and _not_ yarn) to use verdaccio as its registry
* 3. It will _not_ affect the global yarn installation yet. That is a TODO
* 4. It does _not_ affect the monorepo yarnrc file.
*
* If the script is interrupted, it'll attempt to restore the npm/yarn
* registry config to its original state, but that's not guaranteed.
*/
export async function startRegistry() {
const npmOriginalRegistry = (
await $`npm config get registry --location=global`
).stdout.trim()
onCleanup((exitCode, signal) => {
onCleanup.uninstall()
$`npm config set registry ${npmOriginalRegistry} --location=global`.then(
() => {
process.kill(process.pid, signal)
},
)
return false
})
await $`echo "Setting npm registry url to verdaccio's"`
await $`npm config set registry ${VERDACCIO_URL} --location=global`
await $`echo Running verdaccio on ${VERDACCIO_URL}`
const verdaccioServer = await startVerdaccio(VERDACCIO_PORT)
await releaseToVerdaccio()
}
/**
* Starts the verdaccio server and returns a promise that resolves when the serve is up and ready
*
* Credid: https://github.com/storybookjs/storybook/blob/92b23c080d03433765cbc7a60553d036a612a501/scripts/run-registry.ts
*/
const startVerdaccio = (port) => {
let resolved = false
return Promise.race([
new Promise((resolve) => {
const config = {
...YAML.parse(
fs.readFileSync(path.join(__dirname, '../verdaccio.yml'), 'utf8'),
),
}
const onReady = (webServer) => {
webServer.listen(port, () => {
resolved = true
resolve(webServer)
})
}
startVerdaccioServer(
config,
6000,
undefined,
'1.0.0',
'verdaccio',
onReady,
)
}),
new Promise((_, rej) => {
setTimeout(() => {
if (!resolved) {
resolved = true
rej(new Error(`TIMEOUT - verdaccio didn't start within 10s`))
}
}, 10000)
}),
])
}
const packagesToPublish = [
'@theatre/core',
'@theatre/studio',
'@theatre/dataverse',
'@theatre/react',
'@theatre/browser-bundles',
'@theatre/r3f',
]
/**
* Assigns a new version to each of @theatre/* packages. If there a package depends on another package in this monorepo,
* this function makes sure the dependency version is fixed at "hash"
*
* @param {{name: string, location: string}[]} workspacesListObjects - An Array of objects containing information about the workspaces
* @param {string} hash - Hash of the latest commit (or any other string)
* @returns {Promise<() => void>} - An async function that restores the package.json files to their original version
*/
async function assignVersions(workspacesListObjects, hash) {
/**
* An array of functions each of which restores a certain package.json to its original state
* @type {Array<() => void>}
*/
const restores = []
for (const workspaceData of workspacesListObjects) {
const pathToPackage = path.resolve(
MONOREPO_ROOT,
workspaceData.location,
'./package.json',
)
const originalFileContent = fs.readFileSync(pathToPackage, {
encoding: 'utf-8',
})
const originalJson = JSON.parse(originalFileContent)
restores.push(() => {
fs.writeFileSync(pathToPackage, originalFileContent, {encoding: 'utf-8'})
})
let {dependencies, peerDependencies, devDependencies} = originalJson
const version = hash
// Normally we don't have to override the package versions in dependencies because yarn would already convert
// all the "workspace:*" versions to a fixed version before publishing. However, packages like @theatre/studio
// have a peerDependency on @theatre/core set to "*" (meaning they would work with any version of @theatre/core).
// This is not the desired behavior in pre-release versions, so here, we'll fix those "*" versions to the set version.
for (const deps of [dependencies, peerDependencies, devDependencies]) {
if (!deps) continue
for (const wpObject of workspacesListObjects) {
if (deps[wpObject.name]) {
deps[wpObject.name] = hash
}
}
}
const newJson = {
...originalJson,
version,
dependencies,
peerDependencies,
devDependencies,
}
fs.writeFileSync(pathToPackage, JSON.stringify(newJson, undefined, 2), {
encoding: 'utf-8',
})
}
return () =>
restores.forEach((fn) => {
fn()
})
}
/**
* Builds all the @theatre/* packages with version number 0.0.1-COMPAT.1 and publishes
* them all to the verdaccio registry
*/
async function releaseToVerdaccio() {
const version = '0.0.1-COMPAT.1'
cd(MONOREPO_ROOT)
// @ts-ignore ignore
process.env.THEATRE_IS_PUBLISHING = true
const workspacesListString = await $`yarn workspaces list --json`
const workspacesListObjects = workspacesListString.stdout
.split(os.EOL)
// strip out empty lines
.filter(Boolean)
.map((x) => JSON.parse(x))
const restorePackages = await assignVersions(workspacesListObjects, version)
process.on('SIGINT', async function cleanup(a) {
restorePackages()
process.exit(0)
})
// set verdaccio as the publish registry, and add it to the whitelist
const restoreYarnRc = patchYarnRcToUseVerdaccio()
await $`yarn clean`
await $`yarn build`
await Promise.all(
packagesToPublish.map(async (workspaceName) => {
const npmTag = 'compat'
await $`yarn workspace ${workspaceName} npm publish --access public --tag ${npmTag}`
}),
)
restorePackages()
restoreYarnRc()
}
/**
* Temporarily patches the yarnrc file to sue verdaccio as its publish registry.
*
* Restores yarnrc to the old version when restoreYarnRc() is called.
*/
function patchYarnRcToUseVerdaccio() {
const originalYarnrcContent = fs.readFileSync(PATH_TO_YARNRC, {
encoding: 'utf-8',
})
const newYarnRcContent = YAML.stringify({
...YAML.parse(originalYarnrcContent),
unsafeHttpWhitelist: [VERDACCIO_HOST],
npmPublishRegistry: VERDACCIO_URL,
npmAuthIdent: 'test:test',
})
fs.writeFileSync(PATH_TO_YARNRC, newYarnRcContent, {encoding: 'utf-8'})
return function restoreYarnRc() {
fs.writeFileSync(PATH_TO_YARNRC, originalYarnrcContent, {encoding: 'utf-8'})
}
}
/**
* Get all the setups from `./compatibility-tests/`
*
* @returns {Array<string>} An array containing the absolute paths to the compatibility test setups
*/
export function getCompatibilityTestSetups() {
const buildTestsDir = path.join(MONOREPO_ROOT, 'compatibility-tests')
let buildTestsDirEntries
try {
buildTestsDirEntries = fs.readdirSync(buildTestsDir)
} catch {
throw new Error(
`Could not list directory: "${buildTestsDir}" Is it an existing directory?`,
)
}
const setupsAbsPaths = []
// NOTE: We assume that every directory matching `compatibility-tests/test-*` is
// a test package
for (const entry of buildTestsDirEntries) {
if (!entry.startsWith('test-')) continue
const entryAbsPath = path.join(buildTestsDir, entry)
if (fs.lstatSync(entryAbsPath).isDirectory()) {
setupsAbsPaths.push(entryAbsPath)
}
}
return setupsAbsPaths
}

View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -0,0 +1,16 @@
# Theatre.js project with `create-react-app` using the `r3f extension`
## Setup
Please refer to [this guide](../README.md#testing-the-configurations-locally).
## Usage
- start the development server:
```sh
yarn start
```
- build the project:
```sh
yarn build
```

View file

@ -0,0 +1,41 @@
{
"name": "@theatre-build-tests/react-app",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"@react-three/drei": ">7.2.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@theatre/core": "^0.0.1-COMPAT.1",
"@theatre/studio": "^0.0.1-COMPAT.1",
"@theatre/r3f": "^0.0.1-COMPAT.1",
"react-scripts": "^5.0.1",
"three": ">0.132.0",
"web-vitals": "^1.0.1"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View file

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -0,0 +1,150 @@
import {getProject} from '@theatre/core'
import * as THREE from 'three'
import {useState, useEffect, useRef} from 'react'
import {useFrame, Canvas} from '@react-three/fiber'
import {Shadow, softShadows} from '@react-three/drei'
import React from 'react'
import studio from '@theatre/studio'
import {editable as e, SheetProvider, extension} from '@theatre/r3f'
if (process.env.NODE_ENV === 'development') {
studio.extend(extension)
studio.initialize()
}
// Soft shadows are expensive, comment and refresh when it's too slow
softShadows()
// credit: https://codesandbox.io/s/camera-pan-nsb7f
function Button() {
const vec = new THREE.Vector3()
const light = useRef(undefined)
const [active, setActive] = useState(false)
const [zoom, set] = useState(true)
useEffect(
() => void (document.body.style.cursor = active ? 'pointer' : 'auto'),
[active],
)
useFrame((state) => {
const step = 0.1
const camera = state.camera
camera.fov = THREE.MathUtils.lerp(camera.fov, zoom ? 10 : 42, step)
camera.position.lerp(
vec.set(zoom ? 25 : 10, zoom ? 1 : 5, zoom ? 0 : 10),
step,
)
state.camera.lookAt(0, 0, 0)
state.camera.updateProjectionMatrix()
light.current.position.lerp(
vec.set(zoom ? 4 : 0, zoom ? 3 : 8, zoom ? 3 : 5),
step,
)
})
return (
<e.mesh
receiveShadow
castShadow
onClick={() => set(!zoom)}
onPointerOver={() => setActive(true)}
onPointerOut={() => setActive(false)}
uniqueName="The Button"
>
<sphereBufferGeometry args={[0.75, 64, 64]} />
<meshPhysicalMaterial
color={active ? 'purple' : '#e7b056'}
clearcoat={1}
clearcoatRoughness={0}
/>
<Shadow
position-y={-0.79}
rotation-x={-Math.PI / 2}
opacity={0.6}
scale={[0.8, 0.8, 1]}
/>
<directionalLight
ref={light}
castShadow
intensity={1.5}
shadow-camera-far={70}
/>
</e.mesh>
)
}
function Plane({color, uniqueName, ...props}) {
return (
<e.mesh receiveShadow castShadow {...props} uniqueName={uniqueName}>
<boxBufferGeometry />
<meshStandardMaterial color={color} />
</e.mesh>
)
}
const sheet = getProject('Playground - R3F').sheet('R3F-Canvas')
function App() {
return (
<div>
<Canvas
// @ts-ignore
shadowMap
>
<SheetProvider sheet={sheet}>
{/* @ts-ignore */}
<e.perspectiveCamera makeDefault uniqueName="Camera" />
<ambientLight intensity={0.4} />
<e.pointLight
position={[-10, -10, 5]}
intensity={2}
color="#ff20f0"
uniqueName="Light 1"
/>
<e.pointLight
position={[0, 0.5, -1]}
distance={1}
intensity={2}
color="#e4be00"
uniqueName="Light 2"
/>
<group position={[0, -0.9, -3]}>
<Plane
color="hotpink"
rotation-x={-Math.PI / 2}
position-z={2}
scale={[4, 20, 0.2]}
uniqueName="plane1"
/>
<Plane
color="#e4be00"
rotation-x={-Math.PI / 2}
position-y={1}
scale={[4.2, 0.2, 4]}
uniqueName="plane2"
/>
<Plane
color="#736fbd"
rotation-x={-Math.PI / 2}
position={[-1.7, 1, 3.5]}
scale={[0.5, 4, 4]}
uniqueName="plane3"
/>
<Plane
color="white"
rotation-x={-Math.PI / 2}
position={[0, 4.5, 3]}
scale={[2, 0.03, 4]}
uniqueName="plane4"
/>
</group>
<Button />
</SheetProvider>
</Canvas>
</div>
)
}
export default App

View file

@ -0,0 +1,7 @@
import {render, screen} from '@testing-library/react'
test('renders learn react link', () => {
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})

View file

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -0,0 +1,19 @@
import ReactDOM from 'react-dom'
import './index.css'
import reportWebVitals from './reportWebVitals'
import '@theatre/studio'
import {getProject} from '@theatre/core'
import React from 'react'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App project={getProject('CRA project')} />
</React.StrictMode>,
document.getElementById('root'),
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View file

@ -0,0 +1,13 @@
const reportWebVitals = (onPerfEntry) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => {
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
export default reportWebVitals

View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'

View file

@ -0,0 +1,130 @@
import {useLayoutEffect, useRef} from 'react'
const noop = () => {}
function createCursorLock(cursor) {
const el = document.createElement('div')
el.style.cssText = `
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 9999999;`
el.style.cursor = cursor
document.body.appendChild(el)
const relinquish = () => {
document.body.removeChild(el)
}
return relinquish
}
export default function useDrag(target, opts) {
const optsRef = useRef(opts)
optsRef.current = opts
const modeRef = useRef('notDragging')
const stateRef = useRef({dragHappened: false, startPos: {x: 0, y: 0}})
useLayoutEffect(() => {
if (!target) return
const getDistances = (event) => {
const {startPos} = stateRef.current
return [event.screenX - startPos.x, event.screenY - startPos.y]
}
let relinquishCursorLock = noop
const dragHandler = (event) => {
if (!stateRef.current.dragHappened && optsRef.current.lockCursorTo) {
relinquishCursorLock = createCursorLock(optsRef.current.lockCursorTo)
}
if (!stateRef.current.dragHappened) stateRef.current.dragHappened = true
modeRef.current = 'dragging'
const deltas = getDistances(event)
optsRef.current.onDrag(deltas[0], deltas[1], event)
}
const dragEndHandler = () => {
removeDragListeners()
modeRef.current = 'notDragging'
optsRef.current.onDragEnd &&
optsRef.current.onDragEnd(stateRef.current.dragHappened)
relinquishCursorLock()
relinquishCursorLock = noop
}
const addDragListeners = () => {
document.addEventListener('mousemove', dragHandler)
document.addEventListener('mouseup', dragEndHandler)
}
const removeDragListeners = () => {
document.removeEventListener('mousemove', dragHandler)
document.removeEventListener('mouseup', dragEndHandler)
}
const preventUnwantedClick = (event) => {
if (optsRef.current.disabled) return
if (stateRef.current.dragHappened) {
if (
!optsRef.current.dontBlockMouseDown &&
modeRef.current !== 'notDragging'
) {
event.stopPropagation()
event.preventDefault()
}
stateRef.current.dragHappened = false
}
}
const dragStartHandler = (event) => {
const opts = optsRef.current
if (opts.disabled === true) return
if (event.button !== 0) return
const resultOfStart = opts.onDragStart && opts.onDragStart(event)
if (resultOfStart === false) return
if (!opts.dontBlockMouseDown) {
event.stopPropagation()
event.preventDefault()
}
modeRef.current = 'dragStartCalled'
const {screenX, screenY} = event
stateRef.current.startPos = {x: screenX, y: screenY}
stateRef.current.dragHappened = false
addDragListeners()
}
const onMouseDown = (e) => {
dragStartHandler(e)
}
target.addEventListener('mousedown', onMouseDown)
target.addEventListener('click', preventUnwantedClick)
return () => {
removeDragListeners()
target.removeEventListener('mousedown', onMouseDown)
target.removeEventListener('click', preventUnwantedClick)
relinquishCursorLock()
if (modeRef.current !== 'notDragging') {
optsRef.current.onDragEnd &&
optsRef.current.onDragEnd(modeRef.current === 'dragging')
}
modeRef.current = 'notDragging'
}
}, [target])
}

View file

@ -0,0 +1,24 @@
store:
memory:
limit: 1000
auth:
auth-memory:
users:
test:
name: test
password: test
uplinks:
npmjs:
url: https://registry.npmjs.org/
packages:
'@theatre/*':
access: $all
publish: $all
'@*/*':
access: $all
publish: $all
proxy: npmjs
'**':
access: $all
proxy: npmjs
log: {type: stdout, format: pretty, level: http}