summaryrefslogtreecommitdiff
path: root/@linaria/packages/babel/src/utils
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2021-08-23 16:46:06 -0300
committerSebastian <sebasjm@gmail.com>2021-08-23 16:48:30 -0300
commit38acabfa6089ab8ac469c12b5f55022fb96935e5 (patch)
tree453dbf70000cc5e338b06201af1eaca8343f8f73 /@linaria/packages/babel/src/utils
parentf26125e039143b92dc0d84e7775f508ab0cdcaa8 (diff)
downloadnode-vendor-master.tar.gz
node-vendor-master.tar.bz2
node-vendor-master.zip
added web vendorsHEADmaster
Diffstat (limited to '@linaria/packages/babel/src/utils')
-rw-r--r--@linaria/packages/babel/src/utils/getLinariaComment.ts31
-rw-r--r--@linaria/packages/babel/src/utils/getVisitorKeys.ts10
-rw-r--r--@linaria/packages/babel/src/utils/hasImport.ts83
-rw-r--r--@linaria/packages/babel/src/utils/isBoxedPrimitive.ts10
-rw-r--r--@linaria/packages/babel/src/utils/isNode.ts5
-rw-r--r--@linaria/packages/babel/src/utils/isSerializable.ts11
-rw-r--r--@linaria/packages/babel/src/utils/isStyledOrCss.ts67
-rw-r--r--@linaria/packages/babel/src/utils/loadOptions.ts38
-rw-r--r--@linaria/packages/babel/src/utils/peek.ts3
-rw-r--r--@linaria/packages/babel/src/utils/slugify.ts83
-rw-r--r--@linaria/packages/babel/src/utils/stripLines.ts23
-rw-r--r--@linaria/packages/babel/src/utils/throwIfInvalid.ts49
-rw-r--r--@linaria/packages/babel/src/utils/toCSS.ts57
-rw-r--r--@linaria/packages/babel/src/utils/toValidCSSIdentifier.ts3
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/, '_');
+}