diff options
author | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:46:06 -0300 |
---|---|---|
committer | Sebastian <sebasjm@gmail.com> | 2021-08-23 16:48:30 -0300 |
commit | 38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch) | |
tree | 453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/babel/src/utils | |
parent | f26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff) | |
download | node-vendor-master.tar.gz node-vendor-master.tar.bz2 node-vendor-master.zip |
Diffstat (limited to '@linaria/packages/babel/src/utils')
-rw-r--r-- | @linaria/packages/babel/src/utils/getLinariaComment.ts | 31 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/getVisitorKeys.ts | 10 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/hasImport.ts | 83 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/isBoxedPrimitive.ts | 10 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/isNode.ts | 5 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/isSerializable.ts | 11 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/isStyledOrCss.ts | 67 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/loadOptions.ts | 38 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/peek.ts | 3 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/slugify.ts | 83 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/stripLines.ts | 23 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/throwIfInvalid.ts | 49 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/toCSS.ts | 57 | ||||
-rw-r--r-- | @linaria/packages/babel/src/utils/toValidCSSIdentifier.ts | 3 |
14 files changed, 473 insertions, 0 deletions
diff --git a/@linaria/packages/babel/src/utils/getLinariaComment.ts b/@linaria/packages/babel/src/utils/getLinariaComment.ts new file mode 100644 index 0000000..06b3edb --- /dev/null +++ b/@linaria/packages/babel/src/utils/getLinariaComment.ts @@ -0,0 +1,31 @@ +import type { Node } from '@babel/types'; + +const pattern = /^linaria (css|styled) (.+)$/; + +export default function getLinariaComment( + path: { node: Node }, + remove: boolean = true +): ['css' | 'styled' | null, ...(string | null)[]] { + const comments = path.node.leadingComments; + if (!comments) { + return [null, null, null, null]; + } + + const idx = comments.findIndex((comment) => pattern.test(comment.value)); + if (idx === -1) { + return [null, null, null, null]; + } + + const matched = comments[idx].value.match(pattern); + if (!matched) { + return [null, null, null, null]; + } + + if (remove) { + path.node.leadingComments = comments.filter((_, i) => i !== idx); + } + + const type = matched[1] === 'css' ? 'css' : 'styled'; + + return [type, ...matched[2].split(' ').map((i) => (i ? i : null))]; +} diff --git a/@linaria/packages/babel/src/utils/getVisitorKeys.ts b/@linaria/packages/babel/src/utils/getVisitorKeys.ts new file mode 100644 index 0000000..72f11f8 --- /dev/null +++ b/@linaria/packages/babel/src/utils/getVisitorKeys.ts @@ -0,0 +1,10 @@ +import { types as t } from '@babel/core'; +import type { Node, VisitorKeys } from '@babel/types'; + +type Keys<T extends Node> = (VisitorKeys[T['type']] & keyof T)[]; + +export default function getVisitorKeys<TNode extends Node>( + node: TNode +): Keys<TNode> { + return t.VISITOR_KEYS[node.type] as Keys<TNode>; +} diff --git a/@linaria/packages/babel/src/utils/hasImport.ts b/@linaria/packages/babel/src/utils/hasImport.ts new file mode 100644 index 0000000..39c1f07 --- /dev/null +++ b/@linaria/packages/babel/src/utils/hasImport.ts @@ -0,0 +1,83 @@ +import { dirname } from 'path'; +import Module from '../module'; + +const linariaLibs = new Set([ + '@linaria/core', + '@linaria/react', + 'linaria', + 'linaria/react', +]); + +const safeResolve = (name: string) => { + try { + return require.resolve(name); + } catch (err) { + return null; + } +}; + +// Verify if the binding is imported from the specified source +export default function hasImport( + t: any, + scope: any, + filename: string, + identifier: string, + sources: string[] +): boolean { + const binding = scope.getAllBindings()[identifier]; + + if (!binding) { + return false; + } + + const p = binding.path; + + const resolveFromFile = (id: string) => { + try { + return Module._resolveFilename(id, { + id: filename, + filename, + paths: Module._nodeModulePaths(dirname(filename)), + }); + } catch (e) { + return null; + } + }; + + const isImportingModule = (value: string) => + sources.some( + (source) => + // If the value is an exact match, assume it imports the module + value === source || + // Otherwise try to resolve both and check if they are the same file + resolveFromFile(value) === + (linariaLibs.has(source) + ? safeResolve(source) + : resolveFromFile(source)) + ); + + if (t.isImportSpecifier(p) && t.isImportDeclaration(p.parentPath)) { + return isImportingModule(p.parentPath.node.source.value); + } + + if (t.isVariableDeclarator(p)) { + if ( + t.isCallExpression(p.node.init) && + t.isIdentifier(p.node.init.callee) && + p.node.init.callee.name === 'require' && + p.node.init.arguments.length === 1 + ) { + const node = p.node.init.arguments[0]; + + if (t.isStringLiteral(node)) { + return isImportingModule(node.value); + } + + if (t.isTemplateLiteral(node) && node.quasis.length === 1) { + return isImportingModule(node.quasis[0].value.cooked); + } + } + } + + return false; +} diff --git a/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts b/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts new file mode 100644 index 0000000..2d20eec --- /dev/null +++ b/@linaria/packages/babel/src/utils/isBoxedPrimitive.ts @@ -0,0 +1,10 @@ +// There is a problem with using boxed numbers and strings in TS, +// so we cannot just use `instanceof` here + +const constructors = ['Number', 'String']; +export default function isBoxedPrimitive(o: any): o is Number | String { + return ( + constructors.includes(o.constructor.name) && + typeof o?.valueOf() !== 'object' + ); +} diff --git a/@linaria/packages/babel/src/utils/isNode.ts b/@linaria/packages/babel/src/utils/isNode.ts new file mode 100644 index 0000000..2340ef9 --- /dev/null +++ b/@linaria/packages/babel/src/utils/isNode.ts @@ -0,0 +1,5 @@ +import type { Node } from '@babel/types'; + +const isNode = (obj: any): obj is Node => obj?.type !== undefined; + +export default isNode; diff --git a/@linaria/packages/babel/src/utils/isSerializable.ts b/@linaria/packages/babel/src/utils/isSerializable.ts new file mode 100644 index 0000000..1f65d58 --- /dev/null +++ b/@linaria/packages/babel/src/utils/isSerializable.ts @@ -0,0 +1,11 @@ +import type { Serializable } from '../types'; +import isBoxedPrimitive from './isBoxedPrimitive'; + +export default function isSerializable(o: any): o is Serializable { + return ( + (Array.isArray(o) && o.every(isSerializable)) || + (typeof o === 'object' && + o !== null && + (o.constructor.name === 'Object' || isBoxedPrimitive(o))) + ); +} diff --git a/@linaria/packages/babel/src/utils/isStyledOrCss.ts b/@linaria/packages/babel/src/utils/isStyledOrCss.ts new file mode 100644 index 0000000..04e4789 --- /dev/null +++ b/@linaria/packages/babel/src/utils/isStyledOrCss.ts @@ -0,0 +1,67 @@ +import type { + CallExpression, + Expression, + TaggedTemplateExpression, +} from '@babel/types'; +import type { NodePath } from '@babel/traverse'; +import type { State, TemplateExpression } from '../types'; +import { Core } from '../babel'; +import hasImport from './hasImport'; + +type Result = NonNullable<TemplateExpression['styled']> | 'css' | null; + +const cache = new WeakMap<NodePath<TaggedTemplateExpression>, Result>(); + +export default function isStyledOrCss( + { types: t }: Core, + path: NodePath<TaggedTemplateExpression>, + state: State +): Result { + if (!cache.has(path)) { + const { tag } = path.node; + + const localName = state.file.metadata.localName || 'styled'; + + if ( + t.isCallExpression(tag) && + t.isIdentifier(tag.callee) && + tag.arguments.length === 1 && + tag.callee.name === localName && + hasImport(t, path.scope, state.file.opts.filename, localName, [ + '@linaria/react', + 'linaria/react', + ]) + ) { + const tagPath = path.get('tag') as NodePath<CallExpression>; + cache.set(path, { + component: tagPath.get('arguments')[0] as NodePath<Expression>, + }); + } else if ( + t.isMemberExpression(tag) && + t.isIdentifier(tag.object) && + t.isIdentifier(tag.property) && + tag.object.name === localName && + hasImport(t, path.scope, state.file.opts.filename, localName, [ + '@linaria/react', + 'linaria/react', + ]) + ) { + cache.set(path, { + component: { node: t.stringLiteral(tag.property.name) }, + }); + } else if ( + hasImport(t, path.scope, state.file.opts.filename, 'css', [ + '@linaria/core', + 'linaria', + ]) && + t.isIdentifier(tag) && + tag.name === 'css' + ) { + cache.set(path, 'css'); + } else { + cache.set(path, null); + } + } + + return cache.get(path) ?? null; +} diff --git a/@linaria/packages/babel/src/utils/loadOptions.ts b/@linaria/packages/babel/src/utils/loadOptions.ts new file mode 100644 index 0000000..8c0f0b8 --- /dev/null +++ b/@linaria/packages/babel/src/utils/loadOptions.ts @@ -0,0 +1,38 @@ +import cosmiconfig from 'cosmiconfig'; +import type { StrictOptions } from '../types'; + +export type PluginOptions = StrictOptions & { + configFile?: string; +}; + +const explorer = cosmiconfig('linaria'); + +export default function loadOptions( + overrides: Partial<PluginOptions> = {} +): Partial<StrictOptions> { + const { configFile, ignore, ...rest } = overrides; + + const result = + configFile !== undefined + ? explorer.loadSync(configFile) + : explorer.searchSync(); + + return { + displayName: false, + evaluate: true, + rules: [ + { + // FIXME: if `rule` is not specified in a config, `@linaria/shaker` should be added as a dependency + // eslint-disable-next-line import/no-extraneous-dependencies + action: require('@linaria/shaker').default, + }, + { + // The old `ignore` option is used as a default value for `ignore` rule. + test: ignore ?? /[\\/]node_modules[\\/]/, + action: 'ignore', + }, + ], + ...(result ? result.config : null), + ...rest, + }; +} diff --git a/@linaria/packages/babel/src/utils/peek.ts b/@linaria/packages/babel/src/utils/peek.ts new file mode 100644 index 0000000..cabadc4 --- /dev/null +++ b/@linaria/packages/babel/src/utils/peek.ts @@ -0,0 +1,3 @@ +const peek = <T>(stack: T[], offset = 1): T => stack[stack.length - offset]; + +export default peek; diff --git a/@linaria/packages/babel/src/utils/slugify.ts b/@linaria/packages/babel/src/utils/slugify.ts new file mode 100644 index 0000000..39772ad --- /dev/null +++ b/@linaria/packages/babel/src/utils/slugify.ts @@ -0,0 +1,83 @@ +/** + * This file contains a utility to generate hashes to be used as generated class names + */ + +/* eslint-disable no-bitwise, default-case, no-param-reassign, prefer-destructuring */ + +/** + * murmurhash2 via https://gist.github.com/raycmorgan/588423 + */ + +function doHash(str: string, seed: number = 0) { + const m = 0x5bd1e995; + const r = 24; + let h = seed ^ str.length; + let length = str.length; + let currentIndex = 0; + + while (length >= 4) { + let k = UInt32(str, currentIndex); + + k = Umul32(k, m); + k ^= k >>> r; + k = Umul32(k, m); + + h = Umul32(h, m); + h ^= k; + + currentIndex += 4; + length -= 4; + } + + switch (length) { + case 3: + h ^= UInt16(str, currentIndex); + h ^= str.charCodeAt(currentIndex + 2) << 16; + h = Umul32(h, m); + break; + + case 2: + h ^= UInt16(str, currentIndex); + h = Umul32(h, m); + break; + + case 1: + h ^= str.charCodeAt(currentIndex); + h = Umul32(h, m); + break; + } + + h ^= h >>> 13; + h = Umul32(h, m); + h ^= h >>> 15; + + return h >>> 0; +} + +function UInt32(str: string, pos: number) { + return ( + str.charCodeAt(pos++) + + (str.charCodeAt(pos++) << 8) + + (str.charCodeAt(pos++) << 16) + + (str.charCodeAt(pos) << 24) + ); +} + +function UInt16(str: string, pos: number) { + return str.charCodeAt(pos++) + (str.charCodeAt(pos++) << 8); +} + +function Umul32(n: number, m: number) { + n |= 0; + m |= 0; + const nlo = n & 0xffff; + const nhi = n >>> 16; + const res = (nlo * m + (((nhi * m) & 0xffff) << 16)) | 0; + return res; +} + +function slugify(code: string) { + return doHash(code).toString(36); +} + +export default slugify; diff --git a/@linaria/packages/babel/src/utils/stripLines.ts b/@linaria/packages/babel/src/utils/stripLines.ts new file mode 100644 index 0000000..d0495ae --- /dev/null +++ b/@linaria/packages/babel/src/utils/stripLines.ts @@ -0,0 +1,23 @@ +import type { Location } from '../types'; + +// Stripping away the new lines ensures that we preserve line numbers +// This is useful in case of tools such as the stylelint pre-processor +// This should be safe because strings cannot contain newline: https://www.w3.org/TR/CSS2/syndata.html#strings +export default function stripLines( + loc: { start: Location; end: Location }, + text: string | number +) { + let result = String(text) + .replace(/[\r\n]+/g, ' ') + .trim(); + + // If the start and end line numbers aren't same, add new lines to span the text across multiple lines + if (loc.start.line !== loc.end.line) { + result += '\n'.repeat(loc.end.line - loc.start.line); + + // Add extra spaces to offset the column + result += ' '.repeat(loc.end.column); + } + + return result; +} diff --git a/@linaria/packages/babel/src/utils/throwIfInvalid.ts b/@linaria/packages/babel/src/utils/throwIfInvalid.ts new file mode 100644 index 0000000..a883f9f --- /dev/null +++ b/@linaria/packages/babel/src/utils/throwIfInvalid.ts @@ -0,0 +1,49 @@ +import generator from '@babel/generator'; +import type { Serializable } from '../types'; +import isSerializable from './isSerializable'; + +// Throw if we can't handle the interpolated value +function throwIfInvalid( + value: Error | Function | string | number | Serializable | undefined, + ex: any +): void { + if ( + typeof value === 'function' || + typeof value === 'string' || + (typeof value === 'number' && Number.isFinite(value)) || + isSerializable(value) + ) { + return; + } + + // We can't use instanceof here so let's use duck typing + if (value && typeof value !== 'number' && value.stack && value.message) { + throw ex.buildCodeFrameError( + `An error occurred when evaluating the expression: + + > ${value.message}. + + Make sure you are not using a browser or Node specific API and all the variables are available in static context. + Linaria have to extract pieces of your code to resolve the interpolated values. + Defining styled component or class will not work inside: + - function, + - class, + - method, + - loop, + because it cannot be statically determined in which context you use them. + That's why some variables may be not defined during evaluation. + ` + ); + } + + const stringified = + typeof value === 'object' ? JSON.stringify(value) : String(value); + + throw ex.buildCodeFrameError( + `The expression evaluated to '${stringified}', which is probably a mistake. If you want it to be inserted into CSS, explicitly cast or transform the value to a string, e.g. - 'String(${ + generator(ex.node).code + })'.` + ); +} + +export default throwIfInvalid; diff --git a/@linaria/packages/babel/src/utils/toCSS.ts b/@linaria/packages/babel/src/utils/toCSS.ts new file mode 100644 index 0000000..b396087 --- /dev/null +++ b/@linaria/packages/babel/src/utils/toCSS.ts @@ -0,0 +1,57 @@ +import { unitless } from '../units'; +import type { JSONValue } from '../types'; +import isSerializable from './isSerializable'; +import isBoxedPrimitive from './isBoxedPrimitive'; + +const hyphenate = (s: string) => { + if (s.startsWith('--')) { + // It's a custom property which is already well formatted. + return s; + } + return ( + s + // Hyphenate CSS property names from camelCase version from JS string + .replace(/([A-Z])/g, (match, p1) => `-${p1.toLowerCase()}`) + // Special case for `-ms` because in JS it starts with `ms` unlike `Webkit` + .replace(/^ms-/, '-ms-') + ); +}; + +// Some tools such as polished.js output JS objects +// To support them transparently, we convert JS objects to CSS strings +export default function toCSS(o: JSONValue): string { + if (Array.isArray(o)) { + return o.map(toCSS).join('\n'); + } + + if (isBoxedPrimitive(o)) { + return o.valueOf().toString(); + } + + return Object.entries(o) + .filter( + ([, value]) => + // Ignore all falsy values except numbers + typeof value === 'number' || value + ) + .map(([key, value]) => { + if (isSerializable(value)) { + return `${key} { ${toCSS(value)} }`; + } + + return `${hyphenate(key)}: ${ + typeof value === 'number' && + value !== 0 && + // Strip vendor prefixes when checking if the value is unitless + !( + key.replace( + /^(Webkit|Moz|O|ms)([A-Z])(.+)$/, + (match, p1, p2, p3) => `${p2.toLowerCase()}${p3}` + ) in unitless + ) + ? `${value}px` + : value + };`; + }) + .join(' '); +} diff --git a/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts b/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts new file mode 100644 index 0000000..9a2be8f --- /dev/null +++ b/@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts @@ -0,0 +1,3 @@ +export default function toValidCSSIdentifier(s: string) { + return s.replace(/[^-_a-z0-9\u00A0-\uFFFF]/gi, '_').replace(/^\d/, '_'); +} |