From 8dc1a6dd19d2dc9ce435ef0aff85ccf5647f5d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Tue, 3 Dec 2024 09:37:19 +0100 Subject: [PATCH] Convert `@emotion/cache`'s source code to TypeScript (#3277) * Convert `@emotion/cache`'s source code to TypeScript * remove extra comment types * tweak pkg json * fixed pkg.json#imports * try with this * use .ts * try this * try this * one more * try this * update `@types/stylis` --- .changeset/fluffy-garlics-smash.md | 5 + packages/cache/package.json | 20 +- .../src/conditions/{false.js => false.ts} | 0 .../{is-browser.js => is-browser.ts} | 0 .../cache/src/conditions/{true.js => true.ts} | 0 packages/cache/src/index.d.ts | 2 - packages/cache/src/{index.js => index.ts} | 94 ++++------ .../cache/src/{prefixer.js => prefixer.ts} | 15 +- .../{stylis-plugins.js => stylis-plugins.ts} | 174 +++++++++--------- packages/cache/src/{types.js => types.ts} | 9 +- packages/cache/types/index.d.ts | 46 +---- packages/css-prettifier/package.json | 2 +- yarn.lock | 11 +- 13 files changed, 168 insertions(+), 210 deletions(-) create mode 100644 .changeset/fluffy-garlics-smash.md rename packages/cache/src/conditions/{false.js => false.ts} (100%) rename packages/cache/src/conditions/{is-browser.js => is-browser.ts} (100%) rename packages/cache/src/conditions/{true.js => true.ts} (100%) delete mode 100644 packages/cache/src/index.d.ts rename packages/cache/src/{index.js => index.ts} (80%) rename packages/cache/src/{prefixer.js => prefixer.ts} (96%) rename packages/cache/src/{stylis-plugins.js => stylis-plugins.ts} (59%) rename packages/cache/src/{types.js => types.ts} (76%) diff --git a/.changeset/fluffy-garlics-smash.md b/.changeset/fluffy-garlics-smash.md new file mode 100644 index 0000000000..d578a45602 --- /dev/null +++ b/.changeset/fluffy-garlics-smash.md @@ -0,0 +1,5 @@ +--- +'@emotion/cache': minor +--- + +Source code has been migrated to TypeScript. From now on type declarations will be emitted based on that, instead of being hand-written. diff --git a/packages/cache/package.json b/packages/cache/package.json index 7b6152a239..7e70730da1 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -4,6 +4,7 @@ "description": "emotion's cache", "main": "dist/emotion-cache.cjs.js", "module": "dist/emotion-cache.esm.js", + "types": "dist/emotion-cache.cjs.d.ts", "exports": { ".": { "types": { @@ -63,18 +64,17 @@ }, "imports": { "#is-development": { - "development": "./src/conditions/true.js", - "default": "./src/conditions/false.js" + "development": "./src/conditions/true.ts", + "default": "./src/conditions/false.ts" }, "#is-browser": { - "edge-light": "./src/conditions/false.js", - "workerd": "./src/conditions/false.js", - "worker": "./src/conditions/false.js", - "browser": "./src/conditions/true.js", - "default": "./src/conditions/is-browser.js" + "edge-light": "./src/conditions/false.ts", + "workerd": "./src/conditions/false.ts", + "worker": "./src/conditions/false.ts", + "browser": "./src/conditions/true.ts", + "default": "./src/conditions/is-browser.ts" } }, - "types": "types/index.d.ts", "license": "MIT", "repository": "https://github.com/emotion-js/emotion/tree/main/packages/cache", "scripts": { @@ -90,11 +90,11 @@ "devDependencies": { "@definitelytyped/dtslint": "0.0.112", "@emotion/hash": "*", + "@types/stylis": "^4.2.7", "typescript": "^5.4.5" }, "files": [ "src", - "dist", - "types/*.d.ts" + "dist" ] } diff --git a/packages/cache/src/conditions/false.js b/packages/cache/src/conditions/false.ts similarity index 100% rename from packages/cache/src/conditions/false.js rename to packages/cache/src/conditions/false.ts diff --git a/packages/cache/src/conditions/is-browser.js b/packages/cache/src/conditions/is-browser.ts similarity index 100% rename from packages/cache/src/conditions/is-browser.js rename to packages/cache/src/conditions/is-browser.ts diff --git a/packages/cache/src/conditions/true.js b/packages/cache/src/conditions/true.ts similarity index 100% rename from packages/cache/src/conditions/true.js rename to packages/cache/src/conditions/true.ts diff --git a/packages/cache/src/index.d.ts b/packages/cache/src/index.d.ts deleted file mode 100644 index 9e46093759..0000000000 --- a/packages/cache/src/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '../types' -export { default } from '../types' diff --git a/packages/cache/src/index.js b/packages/cache/src/index.ts similarity index 80% rename from packages/cache/src/index.js rename to packages/cache/src/index.ts index abf0289edc..706a3fa41e 100644 --- a/packages/cache/src/index.js +++ b/packages/cache/src/index.ts @@ -1,5 +1,5 @@ import { StyleSheet } from '@emotion/sheet' -/* import { type EmotionCache, type SerializedStyles } from '@emotion/utils' */ +import type { EmotionCache, SerializedStyles } from '@emotion/utils' import { serialize, compile, @@ -8,6 +8,7 @@ import { stringify, COMMENT } from 'stylis' +import type { Element as StylisElement } from 'stylis' import weakMemoize from '@emotion/weak-memoize' import memoize from '@emotion/memoize' import isDevelopment from '#is-development' @@ -19,43 +20,37 @@ import { incorrectImportAlarm } from './stylis-plugins' import { prefixer } from './prefixer' -/* import type { StylisPlugin } from './types' */ +import { StylisPlugin } from './types' -/* -export type Options = { - nonce?: string, - stylisPlugins?: StylisPlugin[], - key: string, - container?: HTMLElement, - speedy?: boolean, - prepend?: boolean, +export interface Options { + nonce?: string + stylisPlugins?: Array + key: string + container?: Node + speedy?: boolean + /** @deprecate use `insertionPoint` instead */ + prepend?: boolean insertionPoint?: HTMLElement } -*/ let getServerStylisCache = isBrowser ? undefined - : weakMemoize(() => - memoize(() => { - let cache = {} - return name => cache[name] - }) - ) + : weakMemoize(() => memoize>(() => ({}))) const defaultStylisPlugins = [prefixer] -let getSourceMap +let getSourceMap: ((styles: string) => string | undefined) | undefined if (isDevelopment) { let sourceMapPattern = /\/\*#\ssourceMappingURL=data:application\/json;\S+\s+\*\//g - getSourceMap = (styles /*: string */) => { + getSourceMap = styles => { let matches = styles.match(sourceMapPattern) if (!matches) return return matches[matches.length - 1] } } -let createCache = (options /*: Options */) /*: EmotionCache */ => { +let createCache = (options: Options): EmotionCache => { let key = options.key if (isDevelopment && !key) { @@ -74,14 +69,14 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { // document.head is a safe place to move them to(though note document.head is not necessarily the last place they will be) // note this very very intentionally targets all style elements regardless of the key to ensure // that creating a cache works inside of render of a React component - Array.prototype.forEach.call(ssrStyles, (node /*: HTMLStyleElement */) => { + Array.prototype.forEach.call(ssrStyles, (node: HTMLStyleElement) => { // we want to only move elements which have a space in the data-emotion attribute value // because that indicates that it is an Emotion 11 server-side rendered style elements // while we will already ignore Emotion 11 client-side inserted styles because of the :not([data-s]) part in the selector // Emotion 10 client-side inserted styles did not have data-s (but importantly did not have a space in their data-emotion attributes) // so checking for the space ensures that loading Emotion 11 after Emotion 10 has inserted some styles // will not result in the Emotion 10 styles being destroyed - const dataEmotionAttribute = node.getAttribute('data-emotion') + const dataEmotionAttribute = node.getAttribute('data-emotion')! if (dataEmotionAttribute.indexOf(' ') === -1) { return } @@ -100,9 +95,9 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { ) } } - let inserted = {} - let container /* : Node */ - const nodesToHydrate = [] + let inserted: EmotionCache['inserted'] = {} + let container: Node + const nodesToHydrate: HTMLStyleElement[] = [] if (isBrowser) { container = options.container || document.head @@ -110,8 +105,8 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { // this means we will ignore elements which don't have a space in them which // means that the style elements we're looking at are only Emotion 11 server-rendered style elements document.querySelectorAll(`style[data-emotion^="${key} "]`), - (node /*: HTMLStyleElement */) => { - const attrib = node.getAttribute(`data-emotion`).split(' ') + (node: HTMLStyleElement) => { + const attrib = node.getAttribute(`data-emotion`)!.split(' ') for (let i = 1; i < attrib.length; i++) { inserted[attrib[i]] = true } @@ -120,12 +115,12 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { ) } - let insert /*: ( + let insert: ( selector: string, serialized: SerializedStyles, sheet: StyleSheet, shouldCache: boolean - ) => string | void */ + ) => string | void const omnipresentPlugins = [compat, removeLabel] if (isDevelopment) { @@ -139,13 +134,13 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { ) } - if (isBrowser) { - let currentSheet + if (!getServerStylisCache) { + let currentSheet: Pick const finalizingPlugins = [ stringify, isDevelopment - ? element => { + ? (element: StylisElement) => { if (!element.root) { if (element.return) { currentSheet.insert(element.return) @@ -164,21 +159,16 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { const serializer = middleware( omnipresentPlugins.concat(stylisPlugins, finalizingPlugins) ) - const stylis = styles => serialize(compile(styles), serializer) + const stylis = (styles: string) => serialize(compile(styles), serializer) - insert = ( - selector /*: string */, - serialized /*: SerializedStyles */, - sheet /*: StyleSheet */, - shouldCache /*: boolean */ - ) /*: void */ => { + insert = (selector, serialized, sheet, shouldCache) => { currentSheet = sheet - if (isDevelopment) { + if (getSourceMap) { let sourceMap = getSourceMap(serialized.styles) if (sourceMap) { currentSheet = { - insert: (rule /*: string */) => { + insert: rule => { sheet.insert(rule + sourceMap) } } @@ -196,13 +186,10 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { const serializer = middleware( omnipresentPlugins.concat(stylisPlugins, finalizingPlugins) ) - const stylis = styles => serialize(compile(styles), serializer) + const stylis = (styles: string) => serialize(compile(styles), serializer) let serverStylisCache = getServerStylisCache(stylisPlugins)(key) - let getRules = ( - selector /*: string */, - serialized /*: SerializedStyles */ - ) /*: string */ => { + let getRules = (selector: string, serialized: SerializedStyles): string => { let name = serialized.name if (serverStylisCache[name] === undefined) { serverStylisCache[name] = stylis( @@ -211,12 +198,7 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { } return serverStylisCache[name] } - insert = ( - selector /*: string */, - serialized /*: SerializedStyles */, - sheet /*: StyleSheet */, - shouldCache /*: boolean */ - ) /*: string | void */ => { + insert = (selector, serialized, sheet, shouldCache) => { let name = serialized.name let rules = getRules(selector, serialized) if (cache.compat === undefined) { @@ -226,7 +208,7 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { if (shouldCache) { cache.inserted[name] = true } - if (isDevelopment) { + if (getSourceMap) { let sourceMap = getSourceMap(serialized.styles) if (sourceMap) { return rules + sourceMap @@ -251,11 +233,11 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { } } - const cache /*: EmotionCache */ = { + const cache: EmotionCache = { key, sheet: new StyleSheet({ key, - container, + container: container!, nonce: options.nonce, speedy: options.speedy, prepend: options.prepend, @@ -273,3 +255,5 @@ let createCache = (options /*: Options */) /*: EmotionCache */ => { } export default createCache +export type { EmotionCache } +export type { StylisElement, StylisPlugin, StylisPluginCallback } from './types' diff --git a/packages/cache/src/prefixer.js b/packages/cache/src/prefixer.ts similarity index 96% rename from packages/cache/src/prefixer.js rename to packages/cache/src/prefixer.ts index 030a608973..8a91467a36 100644 --- a/packages/cache/src/prefixer.js +++ b/packages/cache/src/prefixer.ts @@ -15,12 +15,14 @@ import { RULESET, serialize, strlen, - WEBKIT + WEBKIT, + Element, + Middleware } from 'stylis' // this is a copy of stylis@4.0.13 prefixer, the latter version introduced grid prefixing which we don't want -function prefix(value, length) { +function prefix(value: string, length: number): string { switch (hash(value, length)) { // color-adjust case 5103: @@ -279,7 +281,12 @@ function prefix(value, length) { return value } -export let prefixer = (element, index, children, callback) => { +export let prefixer = ( + element: Element, + index: number, + children: Element[], + callback: Middleware +) => { if (element.length > -1) if (!element.return) switch (element.type) { @@ -297,7 +304,7 @@ export let prefixer = (element, index, children, callback) => { ) case RULESET: if (element.length) - return combine(element.props, function (value) { + return combine(element.props as string[], function (value) { switch (match(value, /(::plac\w+|:read-\w+)/)) { // :read-(only|write) case ':read-only': diff --git a/packages/cache/src/stylis-plugins.js b/packages/cache/src/stylis-plugins.ts similarity index 59% rename from packages/cache/src/stylis-plugins.js rename to packages/cache/src/stylis-plugins.ts index 1c1bbb8215..f007fb4254 100644 --- a/packages/cache/src/stylis-plugins.js +++ b/packages/cache/src/stylis-plugins.ts @@ -1,21 +1,24 @@ +import { EmotionCache } from '@emotion/utils' import { - compile, alloc, dealloc, - next, delimit, - token, - char, + Element, from, + Middleware, + next, peek, position, - slice + slice, + token } from 'stylis' -const last = arr => (arr.length ? arr[arr.length - 1] : null) - // based on https://github.com/thysultan/stylis.js/blob/e6843c373ebcbbfade25ebcc23f540ed8508da0a/src/Tokenizer.js#L239-L244 -const identifierWithPointTracking = (begin, points, index) => { +const identifierWithPointTracking = ( + begin: number, + points: number[], + index: number +) => { let previous = 0 let character = 0 @@ -38,7 +41,7 @@ const identifierWithPointTracking = (begin, points, index) => { return slice(begin, position) } -const toRules = (parsed, points) => { +const toRules = (parsed: string[], points: number[]) => { // pretend we've started with a comma let index = -1 let character = 44 @@ -80,12 +83,13 @@ const toRules = (parsed, points) => { return parsed } -const getRules = (value, points) => dealloc(toRules(alloc(value), points)) +const getRules = (value: string, points: number[]) => + dealloc(toRules(alloc(value) as string[], points)) // WeakSet would be more appropriate, but only WeakMap is supported in IE11 const fixedElements = /* #__PURE__ */ new WeakMap() -export let compat = element => { +export let compat: Middleware = element => { if ( element.type !== 'rule' || !element.parent || @@ -96,7 +100,8 @@ export let compat = element => { return } - let { value, parent } = element + let value = element.value + let parent: Element | null = element.parent let isImplicitRule = element.column === parent.column && element.line === parent.line @@ -122,22 +127,22 @@ export let compat = element => { fixedElements.set(element, true) - const points = [] + const points: number[] = [] const rules = getRules(value, points) const parentRules = parent.props for (let i = 0, k = 0; i < rules.length; i++) { for (let j = 0; j < parentRules.length; j++, k++) { - element.props[k] = points[i] + ;(element.props as string[])[k] = points[i] ? rules[i].replace(/&\f/g, parentRules[j]) : `${parentRules[j]} ${rules[i]}` } } } -export let removeLabel = element => { +export let removeLabel: Middleware = element => { if (element.type === 'decl') { - var value = element.value + const value = element.value if ( // charcode for l value.charCodeAt(0) === 108 && @@ -154,83 +159,86 @@ export let removeLabel = element => { const ignoreFlag = 'emotion-disable-server-rendering-unsafe-selector-warning-please-do-not-use-this-the-warning-exists-for-a-reason' -const isIgnoringComment = element => - element.type === 'comm' && element.children.indexOf(ignoreFlag) > -1 - -export let createUnsafeSelectorsAlarm = cache => (element, index, children) => { - if (element.type !== 'rule' || cache.compat) return - - const unsafePseudoClasses = element.value.match( - /(:first|:nth|:nth-last)-child/g - ) - - if (unsafePseudoClasses) { - const isNested = !!element.parent - // in nested rules comments become children of the "auto-inserted" rule and that's always the `element.parent` - // - // considering this input: - // .a { - // .b /* comm */ {} - // color: hotpink; - // } - // we get output corresponding to this: - // .a { - // & { - // /* comm */ - // color: hotpink; - // } - // .b {} - // } - const commentContainer = isNested - ? element.parent.children - : // global rule at the root level - children - - for (let i = commentContainer.length - 1; i >= 0; i--) { - const node = commentContainer[i] - - if (node.line < element.line) { - break - } +const isIgnoringComment = (element: Element) => + element.type === 'comm' && + (element.children as string).indexOf(ignoreFlag) > -1 + +export let createUnsafeSelectorsAlarm = + (cache: Pick): Middleware => + (element, index, children) => { + if (element.type !== 'rule' || cache.compat) return - // it is quite weird but comments are *usually* put at `column: element.column - 1` - // so we seek *from the end* for the node that is earlier than the rule's `element` and check that - // this will also match inputs like this: + const unsafePseudoClasses = element.value.match( + /(:first|:nth|:nth-last)-child/g + ) + + if (unsafePseudoClasses) { + const isNested = !!element.parent + // in nested rules comments become children of the "auto-inserted" rule and that's always the `element.parent` + // + // considering this input: // .a { - // /* comm */ - // .b {} + // .b /* comm */ {} + // color: hotpink; // } - // - // but that is fine - // - // it would be the easiest to change the placement of the comment to be the first child of the rule: + // we get output corresponding to this: // .a { - // .b { /* comm */ } + // & { + // /* comm */ + // color: hotpink; + // } + // .b {} // } - // with such inputs we wouldn't have to search for the comment at all - // TODO: consider changing this comment placement in the next major version - if (node.column < element.column) { - if (isIgnoringComment(node)) { - return + const commentContainer = isNested + ? element.parent!.children + : // global rule at the root level + children + + for (let i = commentContainer.length - 1; i >= 0; i--) { + const node = commentContainer[i] as Element + + if (node.line < element.line) { + break + } + + // it is quite weird but comments are *usually* put at `column: element.column - 1` + // so we seek *from the end* for the node that is earlier than the rule's `element` and check that + // this will also match inputs like this: + // .a { + // /* comm */ + // .b {} + // } + // + // but that is fine + // + // it would be the easiest to change the placement of the comment to be the first child of the rule: + // .a { + // .b { /* comm */ } + // } + // with such inputs we wouldn't have to search for the comment at all + // TODO: consider changing this comment placement in the next major version + if (node.column < element.column) { + if (isIgnoringComment(node)) { + return + } + break } - break } - } - unsafePseudoClasses.forEach(unsafePseudoClass => { - console.error( - `The pseudo class "${unsafePseudoClass}" is potentially unsafe when doing server-side rendering. Try changing it to "${ - unsafePseudoClass.split('-child')[0] - }-of-type".` - ) - }) + unsafePseudoClasses.forEach(unsafePseudoClass => { + console.error( + `The pseudo class "${unsafePseudoClass}" is potentially unsafe when doing server-side rendering. Try changing it to "${ + unsafePseudoClass.split('-child')[0] + }-of-type".` + ) + }) + } } -} -let isImportRule = element => +let isImportRule = (element: Element) => element.type.charCodeAt(1) === 105 && element.type.charCodeAt(0) === 64 -const isPrependedWithRegularRules = (index, children) => { +const isPrependedWithRegularRules = (index: number, children: Element[]) => { for (let i = index - 1; i >= 0; i--) { if (!isImportRule(children[i])) { return true @@ -242,7 +250,7 @@ const isPrependedWithRegularRules = (index, children) => { // use this to remove incorrect elements from further processing // so they don't get handed to the `sheet` (or anything else) // as that could potentially lead to additional logs which in turn could be overhelming to the user -const nullifyElement = element => { +const nullifyElement = (element: Element) => { element.type = '' element.value = '' element.return = '' @@ -250,7 +258,7 @@ const nullifyElement = element => { element.props = '' } -export let incorrectImportAlarm = (element, index, children) => { +export let incorrectImportAlarm: Middleware = (element, index, children) => { if (!isImportRule(element)) { return } diff --git a/packages/cache/src/types.js b/packages/cache/src/types.ts similarity index 76% rename from packages/cache/src/types.js rename to packages/cache/src/types.ts index f02fb33414..58f4b42306 100644 --- a/packages/cache/src/types.js +++ b/packages/cache/src/types.ts @@ -1,10 +1,10 @@ -/* -export type StylisElement = { +export interface StylisElement { type: string value: string - props: Array + props: Array | string root: StylisElement | null - children: Array + parent: StylisElement | null + children: Array | string line: number column: number length: number @@ -23,4 +23,3 @@ export type StylisPlugin = ( children: Array, callback: StylisPluginCallback ) => string | void -*/ diff --git a/packages/cache/types/index.d.ts b/packages/cache/types/index.d.ts index 98a6ffccd7..bf6a4ae6cb 100644 --- a/packages/cache/types/index.d.ts +++ b/packages/cache/types/index.d.ts @@ -1,45 +1 @@ -// Definitions by: Junyoung Clare Jang -// TypeScript Version: 2.2 - -import { EmotionCache } from '@emotion/utils' - -export { EmotionCache } - -export interface StylisElement { - type: string - value: string - props: Array | string - root: StylisElement | null - parent: StylisElement | null - children: Array | string - line: number - column: number - length: number - return: string -} -export type StylisPluginCallback = ( - element: StylisElement, - index: number, - children: Array, - callback: StylisPluginCallback -) => string | void - -export type StylisPlugin = ( - element: StylisElement, - index: number, - children: Array, - callback: StylisPluginCallback -) => string | void - -export interface Options { - nonce?: string - stylisPlugins?: Array - key: string - container?: Node - speedy?: boolean - /** @deprecate use `insertionPoint` instead */ - prepend?: boolean - insertionPoint?: HTMLElement -} - -export default function createCache(options: Options): EmotionCache +export * from '../src' diff --git a/packages/css-prettifier/package.json b/packages/css-prettifier/package.json index d50599207d..dae1b9474a 100644 --- a/packages/css-prettifier/package.json +++ b/packages/css-prettifier/package.json @@ -28,7 +28,7 @@ "stylis": "4.2.0" }, "devDependencies": { - "@types/stylis": "^4.2.6" + "@types/stylis": "^4.2.7" }, "publishConfig": { "access": "public" diff --git a/yarn.lock b/yarn.lock index fbc6e78ca8..4060a10542 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2714,6 +2714,7 @@ __metadata: "@emotion/sheet": ^1.4.0 "@emotion/utils": ^1.4.2 "@emotion/weak-memoize": ^0.4.0 + "@types/stylis": ^4.2.7 stylis: 4.2.0 typescript: ^5.4.5 languageName: unknown @@ -2724,7 +2725,7 @@ __metadata: resolution: "@emotion/css-prettifier@workspace:packages/css-prettifier" dependencies: "@emotion/memoize": ^0.9.0 - "@types/stylis": ^4.2.6 + "@types/stylis": ^4.2.7 stylis: 4.2.0 languageName: unknown linkType: soft @@ -6468,10 +6469,10 @@ __metadata: languageName: node linkType: hard -"@types/stylis@npm:^4.2.6": - version: 4.2.6 - resolution: "@types/stylis@npm:4.2.6" - checksum: 3a1685f2b465eb943805252b5b2934fc8054fe8706f76e3e28544f69532296f42c042d0b402fadc486ef8e86cc3d8a2c66d2345241e0af8e07974dd36d85a79c +"@types/stylis@npm:^4.2.7": + version: 4.2.7 + resolution: "@types/stylis@npm:4.2.7" + checksum: 5f2c8c07d4d5c3c8624d44d026b726295c5a68164087e3cd86f2694660c592ab36264ae49dec38126cf5580f0901266aa24f1c476a622bb83210eabdf7a464e8 languageName: node linkType: hard