mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-02-01 05:16:10 +01:00
Upgrade to React v16 lifecycle (#1975)
* Trying to use memoization and upgrade to react v16 * Fix error * Fix error * Fix handlers error * Add annotation * Remove EventHandlers * No state * Remove un-necessary polyfill * Remove un-necessary polyfill * Remove un-necessary handlers settings * Early Return * Fix Early Return * Fix onChange * Do not run onChange stack twice on same change * Update annotation * Better sense of resolve++ * Cache value in onChange and didMount * Remove un-necessary rechack * Renaming * Remove change in leaf.js * Handlers as this.handlers * do not re-initialize change in onChange * Re-run onChange stack only when change happens * Update value when stack changes * Rename to memoize-one * queue changes * Unify interface * Fix bug * Add document * Remove id * Do not use map * Fix bug * Fix eslint * Fix update when props.value changes * Add annotation * Fix stack * Inline queueChange * Restore onChange * restore onChange * Refactor change and onChange * Use onChange as the single interface for update * Do not flushChange if inside event * Give a warning about synchronous editor.change call * Change isInChange in editor.change * refactor resolution and tmp logic, cleanup code
This commit is contained in:
parent
d05e90e546
commit
877dea16bf
@ -46,6 +46,7 @@
|
||||
"lerna": "^2.7.1",
|
||||
"lodash": "^4.17.4",
|
||||
"matcha": "^0.7.0",
|
||||
"memoize-one": "^4.0.0",
|
||||
"mocha": "^2.5.3",
|
||||
"mocha-lcov-reporter": "^1.3.0",
|
||||
"npm-run-all": "^4.1.2",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"is-window": "^1.0.2",
|
||||
"keycode": "^2.1.2",
|
||||
"lodash": "^4.1.1",
|
||||
"memoize-one": "^4.0.0",
|
||||
"prop-types": "^15.5.8",
|
||||
"react-immutable-proptypes": "^2.1.0",
|
||||
"react-portal": "^3.1.0",
|
||||
|
@ -60,23 +60,26 @@ class Content extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Temporary values.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.tmp = {}
|
||||
this.tmp.isUpdatingSelection = false
|
||||
|
||||
EVENT_HANDLERS.forEach(handler => {
|
||||
this[handler] = event => {
|
||||
this.onEvent(handler, event)
|
||||
}
|
||||
})
|
||||
tmp = {
|
||||
isUpdatingSelection: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a set of bound event handlers.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = event => this.onEvent(handler, event)
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
/**
|
||||
* When the editor first mounts in the DOM we need to:
|
||||
*
|
||||
@ -84,7 +87,7 @@ class Content extends React.Component {
|
||||
* - Update the selection, in case it starts focused.
|
||||
*/
|
||||
|
||||
componentDidMount = () => {
|
||||
componentDidMount() {
|
||||
const window = getWindow(this.element)
|
||||
|
||||
window.document.addEventListener(
|
||||
@ -124,7 +127,7 @@ class Content extends React.Component {
|
||||
* On update, update the selection.
|
||||
*/
|
||||
|
||||
componentDidUpdate = () => {
|
||||
componentDidUpdate() {
|
||||
this.updateSelection()
|
||||
}
|
||||
|
||||
@ -363,7 +366,7 @@ class Content extends React.Component {
|
||||
*/
|
||||
|
||||
render() {
|
||||
const { props } = this
|
||||
const { props, handlers } = this
|
||||
const {
|
||||
className,
|
||||
readOnly,
|
||||
@ -386,11 +389,6 @@ class Content extends React.Component {
|
||||
return this.renderNode(child, isSelected, childrenDecorations[i])
|
||||
})
|
||||
|
||||
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = this[handler]
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const style = {
|
||||
// Prevent the default outline styles.
|
||||
outline: 'none',
|
||||
@ -417,21 +415,6 @@ class Content extends React.Component {
|
||||
contentEditable={readOnly ? null : true}
|
||||
suppressContentEditableWarning
|
||||
className={className}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
onCompositionEnd={this.onCompositionEnd}
|
||||
onCompositionStart={this.onCompositionStart}
|
||||
onCopy={this.onCopy}
|
||||
onCut={this.onCut}
|
||||
onDragEnd={this.onDragEnd}
|
||||
onDragOver={this.onDragOver}
|
||||
onDragStart={this.onDragStart}
|
||||
onDrop={this.onDrop}
|
||||
onInput={this.onInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onPaste={this.onPaste}
|
||||
onSelect={this.onSelect}
|
||||
autoCorrect={props.autoCorrect ? 'on' : 'off'}
|
||||
spellCheck={spellCheck}
|
||||
style={style}
|
||||
|
@ -5,6 +5,7 @@ import SlateTypes from 'slate-prop-types'
|
||||
import Types from 'prop-types'
|
||||
import logger from 'slate-dev-logger'
|
||||
import { Schema, Stack } from 'slate'
|
||||
import memoizeOne from 'memoize-one'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import PLUGINS_PROPS from '../constants/plugin-props'
|
||||
@ -66,94 +67,61 @@ class Editor extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* Initial state.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
this.tmp = {}
|
||||
this.tmp.updates = 0
|
||||
this.tmp.resolves = 0
|
||||
state = {}
|
||||
|
||||
// Resolve the plugins and create a stack and schema from them.
|
||||
const plugins = this.resolvePlugins(props.plugins, props.schema)
|
||||
const stack = Stack.create({ plugins })
|
||||
const schema = Schema.create({ plugins })
|
||||
this.state.schema = schema
|
||||
this.state.stack = stack
|
||||
/**
|
||||
* Temporary values.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
// Run `onChange` on the passed-in value because we need to ensure that it
|
||||
// is normalized, and queue the resulting change.
|
||||
const change = props.value.change()
|
||||
stack.run('onChange', change, this)
|
||||
this.queueChange(change)
|
||||
this.state.value = change.value
|
||||
|
||||
// Create a bound event handler for each event.
|
||||
EVENT_HANDLERS.forEach(handler => {
|
||||
this[handler] = (...args) => {
|
||||
this.onEvent(handler, ...args)
|
||||
}
|
||||
})
|
||||
tmp = {
|
||||
change: null,
|
||||
isChanging: false,
|
||||
operationsSize: null,
|
||||
plugins: null,
|
||||
resolves: 0,
|
||||
updates: 0,
|
||||
value: null,
|
||||
}
|
||||
|
||||
/**
|
||||
* When the `props` are updated, create a new `Stack` if necessary and run
|
||||
* `onChange` to ensure the value is normalized.
|
||||
* Create a set of bound event handlers.
|
||||
*
|
||||
* @param {Object} props
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
componentWillReceiveProps = props => {
|
||||
let { schema, stack } = this
|
||||
handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = event => this.onEvent(handler, event)
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
// Increment the updates counter as a baseline.
|
||||
/**
|
||||
* When the component first mounts, flush any temporary changes, and then,
|
||||
* focus the editor if `autoFocus` is set.
|
||||
*/
|
||||
|
||||
componentDidMount() {
|
||||
this.tmp.updates++
|
||||
|
||||
// If the plugins or the schema have changed, we need to re-resolve the
|
||||
// plugins, since it will result in a new stack and new validations.
|
||||
if (
|
||||
props.plugins != this.props.plugins ||
|
||||
props.schema != this.props.schema
|
||||
) {
|
||||
const plugins = this.resolvePlugins(props.plugins, props.schema)
|
||||
stack = Stack.create({ plugins })
|
||||
schema = Schema.create({ plugins })
|
||||
this.setState({ schema, stack })
|
||||
const { autoFocus } = this.props
|
||||
const { change } = this.tmp
|
||||
|
||||
// Increment the resolves counter.
|
||||
this.tmp.resolves++
|
||||
|
||||
// If we've resolved a few times already, and it's exactly in line with
|
||||
// the updates, then warn the user that they may be doing something wrong.
|
||||
if (this.tmp.resolves > 5 && this.tmp.resolves == this.tmp.updates) {
|
||||
logger.warn(
|
||||
'A Slate <Editor> is re-resolving `props.plugins` or `props.schema` on each update, which leads to poor performance. This is often due to passing in a new `schema` or `plugins` prop with each render by declaring them inline in your render function. Do not do this!'
|
||||
)
|
||||
if (autoFocus) {
|
||||
if (change) {
|
||||
change.focus()
|
||||
} else {
|
||||
this.focus()
|
||||
}
|
||||
}
|
||||
|
||||
// Run `onChange` on the passed-in value because we need to ensure that it
|
||||
// is normalized, and queue the resulting change.
|
||||
const change = props.value.change()
|
||||
stack.run('onChange', change, this)
|
||||
this.queueChange(change)
|
||||
this.setState({ value: change.value })
|
||||
}
|
||||
|
||||
/**
|
||||
* When the component first mounts, flush any temporary changes,
|
||||
* and then, focus the editor if `autoFocus` is set.
|
||||
*/
|
||||
|
||||
componentDidMount = () => {
|
||||
this.flushChange()
|
||||
|
||||
if (this.props.autoFocus) {
|
||||
this.focus()
|
||||
if (change) {
|
||||
this.onChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,113 +129,24 @@ class Editor extends React.Component {
|
||||
* When the component updates, flush any temporary change.
|
||||
*/
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.flushChange()
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
this.tmp.updates++
|
||||
|
||||
/**
|
||||
* Queue a `change` object, to be able to flush it later. This is required for
|
||||
* when a change needs to be applied to the value, but because of the React
|
||||
* lifecycle we can't apply that change immediately. So we cache it here and
|
||||
* later can call `this.flushChange()` to flush it.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
const { change, resolves, updates } = this.tmp
|
||||
|
||||
queueChange = change => {
|
||||
if (change.operations.size) {
|
||||
debug('queueChange', { change })
|
||||
this.tmp.change = change
|
||||
// If we've resolved a few times already, and it's exactly in line with
|
||||
// the updates, then warn the user that they may be doing something wrong.
|
||||
if (resolves > 5 && resolves === updates) {
|
||||
logger.warn(
|
||||
'A Slate <Editor> component is re-resolving `props.plugins` or `props.schema` on each update, which leads to poor performance. This is often due to passing in a new `schema` or `plugins` prop with each render by declaring them inline in your render function. Do not do this!'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush a temporarily stored `change` object, for when a change needed to be
|
||||
* made but couldn't because of React's lifecycle.
|
||||
*/
|
||||
|
||||
flushChange = () => {
|
||||
const { change } = this.tmp
|
||||
|
||||
if (change) {
|
||||
debug('flushChange', { change })
|
||||
delete this.tmp.change
|
||||
this.props.onChange(change)
|
||||
this.onChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a change on the editor, passing `...args` to `change.call`.
|
||||
*
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
change = (...args) => {
|
||||
const change = this.value.change().call(...args)
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically blur the editor.
|
||||
*/
|
||||
|
||||
blur = () => {
|
||||
this.change(c => c.blur())
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically focus the editor.
|
||||
*/
|
||||
|
||||
focus = () => {
|
||||
this.change(c => c.focus())
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters for exposing public properties of the editor's state.
|
||||
*/
|
||||
|
||||
get schema() {
|
||||
return this.state.schema
|
||||
}
|
||||
|
||||
get stack() {
|
||||
return this.state.stack
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
/**
|
||||
* On event.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onEvent = (handler, event) => {
|
||||
this.change(change => {
|
||||
this.stack.run(handler, event, change, this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On change.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
onChange = change => {
|
||||
debug('onChange', { change })
|
||||
|
||||
this.stack.run('onChange', change, this)
|
||||
const { value } = change
|
||||
const { onChange } = this.props
|
||||
if (value == this.value) return
|
||||
onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor.
|
||||
*
|
||||
@ -291,7 +170,162 @@ class Editor extends React.Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an array of plugins from `plugins` and `schema` props.
|
||||
* Get the editor's current plugins.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
get plugins() {
|
||||
const plugins = this.resolvePlugins(this.props.plugins, this.props.schema)
|
||||
return plugins
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor's current schema.
|
||||
*
|
||||
* @return {Schema}
|
||||
*/
|
||||
|
||||
get schema() {
|
||||
const schema = this.resolveSchema(this.plugins)
|
||||
return schema
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor's current stack.
|
||||
*
|
||||
* @return {Stack}
|
||||
*/
|
||||
|
||||
get stack() {
|
||||
const stack = this.resolveStack(this.plugins)
|
||||
return stack
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editor's current value.
|
||||
*
|
||||
* @return {Value}
|
||||
*/
|
||||
|
||||
get value() {
|
||||
// If the current `plugins` and `value` are the same as the last seen ones
|
||||
// that were saved in `tmp`, don't re-resolve because that will trigger
|
||||
// extra `onChange` runs.
|
||||
if (
|
||||
this.plugins === this.tmp.plugins &&
|
||||
this.props.value === this.tmp.value
|
||||
) {
|
||||
return this.tmp.value
|
||||
}
|
||||
|
||||
const value = this.resolveValue(this.plugins, this.props.value)
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a change on the editor, passing `...args` to `change.call`.
|
||||
*
|
||||
* @param {Mixed} ...args
|
||||
*/
|
||||
|
||||
change = (...args) => {
|
||||
if (this.tmp.isChanging) {
|
||||
logger.warn(
|
||||
"The `editor.change` method was called from within an existing `editor.change` callback. This is not allowed, and often due to calling `editor.change` directly from a plugin's event handler which is unnecessary."
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const change = this.value.change()
|
||||
|
||||
try {
|
||||
this.tmp.isChanging = true
|
||||
change.call(...args)
|
||||
} catch (error) {
|
||||
throw error
|
||||
} finally {
|
||||
this.tmp.isChanging = false
|
||||
}
|
||||
|
||||
this.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically blur the editor.
|
||||
*/
|
||||
|
||||
blur = () => {
|
||||
this.change(c => c.blur())
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically focus the editor.
|
||||
*/
|
||||
|
||||
focus = () => {
|
||||
this.change(c => c.focus())
|
||||
}
|
||||
|
||||
/**
|
||||
* On change.
|
||||
*
|
||||
* @param {Change} change
|
||||
*/
|
||||
|
||||
onChange = change => {
|
||||
// If the change doesn't define any operations to apply, abort.
|
||||
if (change.operations.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
debug('onChange', { change })
|
||||
change = this.resolveChange(this.plugins, change, change.operations.size)
|
||||
|
||||
// Store a reference to the last `value` and `plugins` that were seen by the
|
||||
// editor, so we can know whether to normalize a new unknown value if one
|
||||
// is passed in via `this.props`.
|
||||
this.tmp.value = change.value
|
||||
this.tmp.plugins = this.plugins
|
||||
|
||||
// Remove the temporary `change`, since it's being flushed.
|
||||
delete this.tmp.change
|
||||
delete this.tmp.operationsSize
|
||||
|
||||
this.props.onChange(change)
|
||||
}
|
||||
|
||||
/**
|
||||
* On event.
|
||||
*
|
||||
* @param {String} handler
|
||||
* @param {Event} event
|
||||
*/
|
||||
|
||||
onEvent = (handler, event) => {
|
||||
this.change(change => {
|
||||
this.stack.run(handler, event, change, this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a change from the current `plugins`, a potential `change` and its
|
||||
* current operations `size`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @param {Change} change
|
||||
* @param {Number} size
|
||||
*/
|
||||
|
||||
resolveChange = memoizeOne((plugins, change, size) => {
|
||||
const stack = this.resolveStack(plugins)
|
||||
stack.run('onChange', change, this)
|
||||
return change
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolve a set of plugins from potential `plugins` and a `schema`.
|
||||
*
|
||||
* In addition to the plugins provided in props, this will initialize three
|
||||
* other plugins:
|
||||
@ -304,19 +338,20 @@ class Editor extends React.Component {
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
resolvePlugins = (plugins, schema) => {
|
||||
resolvePlugins = memoizeOne((plugins = [], schema = {}) => {
|
||||
debug('resolvePlugins', { plugins, schema })
|
||||
this.tmp.resolves++
|
||||
|
||||
const beforePlugin = BeforePlugin()
|
||||
const afterPlugin = AfterPlugin()
|
||||
const editorPlugin = {
|
||||
schema: schema || {},
|
||||
}
|
||||
const editorPlugin = { schema }
|
||||
|
||||
for (const prop of PLUGINS_PROPS) {
|
||||
// Skip `onChange` because the editor's `onChange` is special.
|
||||
if (prop == 'onChange') continue
|
||||
|
||||
// Skip `schema` because it can't be proxied easily, so it must be
|
||||
// passed in as an argument to this function instead.
|
||||
// Skip `schema` because it can't be proxied easily, so it must be passed
|
||||
// in as an argument to this function instead.
|
||||
if (prop == 'schema') continue
|
||||
|
||||
// Define a function that will just proxies into `props`.
|
||||
@ -325,12 +360,59 @@ class Editor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
return [beforePlugin, editorPlugin, ...(plugins || []), afterPlugin]
|
||||
}
|
||||
return [beforePlugin, editorPlugin, ...plugins, afterPlugin]
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolve a schema from the current `plugins`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @return {Schema}
|
||||
*/
|
||||
|
||||
resolveSchema = memoizeOne(plugins => {
|
||||
debug('resolveSchema', { plugins })
|
||||
const schema = Schema.create({ plugins })
|
||||
return schema
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolve a stack from the current `plugins`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @return {Stack}
|
||||
*/
|
||||
|
||||
resolveStack = memoizeOne(plugins => {
|
||||
debug('resolveStack', { plugins })
|
||||
const stack = Stack.create({ plugins })
|
||||
return stack
|
||||
})
|
||||
|
||||
/**
|
||||
* Resolve a value from the current `plugins` and a potential `value`.
|
||||
*
|
||||
* @param {Array} plugins
|
||||
* @param {Value} value
|
||||
* @return {Change}
|
||||
*/
|
||||
|
||||
resolveValue = memoizeOne((plugins, value) => {
|
||||
debug('resolveValue', { plugins, value })
|
||||
let change = value.change()
|
||||
change = this.resolveChange(plugins, change, change.operations.size)
|
||||
|
||||
// Store the change and it's operations count so that it can be flushed once
|
||||
// the component next updates.
|
||||
this.tmp.change = change
|
||||
this.tmp.operationsSize = change.operations.size
|
||||
|
||||
return change.value
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mix in the property types for the event handlers.
|
||||
* Mix in the prop types for the event handlers.
|
||||
*/
|
||||
|
||||
for (const prop of EVENT_HANDLERS) {
|
||||
|
@ -62,7 +62,7 @@ class Node extends React.Component {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
|
||||
shouldComponentUpdate = nextProps => {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { props } = this
|
||||
const { stack } = props.editor
|
||||
const shouldUpdate = stack.find(
|
||||
|
@ -7,7 +7,6 @@ import getWindow from 'get-window'
|
||||
import { Block, Inline, Text } from 'slate'
|
||||
import Hotkeys from 'slate-hotkeys'
|
||||
|
||||
import EVENT_HANDLERS from '../constants/event-handlers'
|
||||
import Content from '../components/content'
|
||||
import cloneFragment from '../utils/clone-fragment'
|
||||
import findDOMNode from '../utils/find-dom-node'
|
||||
@ -683,10 +682,7 @@ function AfterPlugin() {
|
||||
*/
|
||||
|
||||
function renderEditor(props, editor) {
|
||||
const handlers = EVENT_HANDLERS.reduce((obj, handler) => {
|
||||
obj[handler] = editor[handler]
|
||||
return obj
|
||||
}, {})
|
||||
const { handlers } = editor
|
||||
|
||||
return (
|
||||
<Content
|
||||
|
@ -5532,6 +5532,10 @@ mem@^1.1.0:
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
memoize-one@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-4.0.0.tgz#fc5e2f1427a216676a62ec652cf7398cfad123db"
|
||||
|
||||
memory-fs@^0.4.0, memory-fs@~0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
|
||||
|
Loading…
x
Reference in New Issue
Block a user