summaryrefslogtreecommitdiff
path: root/@linaria/packages/babel/src/visitors/GenerateClassNames.ts
diff options
context:
space:
mode:
Diffstat (limited to '@linaria/packages/babel/src/visitors/GenerateClassNames.ts')
-rw-r--r--@linaria/packages/babel/src/visitors/GenerateClassNames.ts161
1 files changed, 161 insertions, 0 deletions
diff --git a/@linaria/packages/babel/src/visitors/GenerateClassNames.ts b/@linaria/packages/babel/src/visitors/GenerateClassNames.ts
new file mode 100644
index 0000000..9bebf2e
--- /dev/null
+++ b/@linaria/packages/babel/src/visitors/GenerateClassNames.ts
@@ -0,0 +1,161 @@
+/**
+ * This file is a visitor that checks TaggedTemplateExpressions and look for Linaria css or styled templates.
+ * For each template it generates a slug that will be used as a CSS class for particular Template Expression,
+ * and generates a display name for class or styled components.
+ * It saves that meta data as comment above the template, to be later used in templateProcessor.
+ */
+
+import { basename, dirname, relative } from 'path';
+import type { ObjectProperty, TaggedTemplateExpression } from '@babel/types';
+import type { NodePath } from '@babel/traverse';
+import { debug } from '@linaria/logger';
+import type { State, StrictOptions } from '../types';
+import toValidCSSIdentifier from '../utils/toValidCSSIdentifier';
+import slugify from '../utils/slugify';
+import getLinariaComment from '../utils/getLinariaComment';
+import isStyledOrCss from '../utils/isStyledOrCss';
+import { Core } from '../babel';
+
+export default function GenerateClassNames(
+ babel: Core,
+ path: NodePath<TaggedTemplateExpression>,
+ state: State,
+ options: StrictOptions
+) {
+ const { types: t } = babel;
+ const styledOrCss = isStyledOrCss(babel, path, state);
+ if (!styledOrCss) {
+ return;
+ }
+
+ const expressions = path.get('quasi').get('expressions');
+
+ debug('template-parse:identify-expressions', expressions.length);
+
+ // Increment the index of the style we're processing
+ // This is used for slug generation to prevent collision
+ // Also used for display name if it couldn't be determined
+ state.index++;
+
+ let [, slug, displayName, predefinedClassName] = getLinariaComment(path);
+
+ const parent = path.findParent(
+ (p) =>
+ t.isObjectProperty(p) ||
+ t.isJSXOpeningElement(p) ||
+ t.isVariableDeclarator(p)
+ );
+
+ if (!displayName && parent) {
+ const parentNode = parent.node;
+ if (t.isObjectProperty(parentNode)) {
+ if ('name' in parentNode.key) {
+ displayName = parentNode.key.name;
+ } else if ('value' in parentNode.key) {
+ displayName = parentNode.key.value.toString();
+ } else {
+ const keyPath = (parent as NodePath<ObjectProperty>).get('key');
+ displayName = keyPath.getSource();
+ }
+ } else if (
+ t.isJSXOpeningElement(parentNode) &&
+ t.isJSXIdentifier(parentNode.name)
+ ) {
+ displayName = parentNode.name.name;
+ } else if (
+ t.isVariableDeclarator(parentNode) &&
+ t.isIdentifier(parentNode.id)
+ ) {
+ displayName = parentNode.id.name;
+ }
+ }
+
+ if (!displayName) {
+ // Try to derive the path from the filename
+ displayName = basename(state.file.opts.filename);
+
+ if (/^index\.[a-z0-9]+$/.test(displayName)) {
+ // If the file name is 'index', better to get name from parent folder
+ displayName = basename(dirname(state.file.opts.filename));
+ }
+
+ // Remove the file extension
+ displayName = displayName.replace(/\.[a-z0-9]+$/, '');
+
+ if (displayName) {
+ displayName += state.index;
+ } else {
+ throw path.buildCodeFrameError(
+ "Couldn't determine a name for the component. Ensure that it's either:\n" +
+ '- Assigned to a variable\n' +
+ '- Is an object property\n' +
+ '- Is a prop in a JSX element\n'
+ );
+ }
+ }
+
+ // Custom properties need to start with a letter, so we prefix the slug
+ // Also use append the index of the class to the filename for uniqueness in the file
+ slug =
+ slug ||
+ toValidCSSIdentifier(
+ `${displayName.charAt(0).toLowerCase()}${slugify(
+ `${relative(state.file.opts.root, state.file.opts.filename)}:${
+ state.index
+ }`
+ )}`
+ );
+
+ let className = predefinedClassName
+ ? predefinedClassName
+ : options.displayName
+ ? `${toValidCSSIdentifier(displayName!)}_${slug!}`
+ : slug!;
+
+ // The className can be defined by the user either as fn or a string
+ if (typeof options.classNameSlug === 'function') {
+ try {
+ className = toValidCSSIdentifier(
+ options.classNameSlug(slug, displayName)
+ );
+ } catch {
+ throw new Error(`classNameSlug option must return a string`);
+ }
+ }
+
+ if (typeof options.classNameSlug === 'string') {
+ const { classNameSlug } = options;
+
+ // Available variables for the square brackets used in `classNameSlug` options
+ const classNameSlugVars: Record<string, string | null> = {
+ hash: slug,
+ title: displayName,
+ };
+
+ // Variables that were used in the config for `classNameSlug`
+ const optionVariables = classNameSlug.match(/\[.*?]/g) || [];
+ let cnSlug = classNameSlug;
+
+ for (let i = 0, l = optionVariables.length; i < l; i++) {
+ const v = optionVariables[i].slice(1, -1); // Remove the brackets around the variable name
+
+ // Replace the var if it key and value exist otherwise place an empty string
+ cnSlug = cnSlug.replace(`[${v}]`, classNameSlugVars[v] || '');
+ }
+
+ className = toValidCSSIdentifier(cnSlug);
+ }
+
+ const type = styledOrCss === 'css' ? 'css' : 'styled';
+
+ debug(
+ `template-parse:generated-meta:${type}`,
+ `slug: ${slug}, displayName: ${displayName}, className: ${className}`
+ );
+
+ // Save evaluated slug and displayName for future usage in templateProcessor
+ path.addComment(
+ 'leading',
+ `linaria ${type} ${slug} ${displayName} ${className}`
+ );
+}