Skip to content

Commit

Permalink
Add Tailwind styling support to resolvedNode rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
dabbott committed Dec 13, 2023
1 parent 211cbff commit 2b8075b
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`renders classnames 1`] = `
"<Box className=\\"flex flex-col items-center justify-center p-10 flex-1 gap-4\\">
<Tag>New</Tag>
<Text fontSize=\\"5xl\\" className=\\"variant-h1 leading-none text-center\\">
Create, iterate, inspire.
</Text>
<Text fontSize=\\"2xl\\" className=\\"variant-h4 leading-none text-center\\">
Turn great ideas into new possibilities.
</Text>
<Box className=\\"flex items-center gap-4 mt-2\\">
<Button>Get Started</Button>
<Link>Learn More</Link>
</Box>
</Box>;
"
`;
exports[`renders default 1`] = `
"<Box
display=\\"flex\\"
Expand Down
50 changes: 35 additions & 15 deletions packages/noya-compiler/src/__tests__/compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,33 +105,53 @@ const ds: DS = {
};

describe('renders', () => {
test('default', () => {
const findComponent: FindComponent = (componentID) => {
return ds.components?.find(
(component) => component.componentID === componentID,
);
};
const findComponent: FindComponent = (componentID) => {
return ds.components?.find(
(component) => component.componentID === componentID,
);
};

const noyaComponent = findComponent(HeroComponent.componentID);

if (!noyaComponent) {
throw new Error(
`Could not find component with id ${HeroComponent.componentID}`,
);
}

const noyaComponent = findComponent(HeroComponent.componentID);
const resolvedNode = createResolvedNode(
findComponent,
noyaComponent.rootElement,
);

if (!noyaComponent) {
throw new Error(
`Could not find component with id ${HeroComponent.componentID}`,
);
}
test('default', () => {
const reactNode = renderResolvedNode({
contentEditable: false,
disableTabNavigation: false,
includeDataProps: false,
system: ChakraDesignSystem,
dsConfig: ds.config,
resolvedNode,
});

const resolvedNode = createResolvedNode(
findComponent,
noyaComponent.rootElement,
const code = createElementCode(
createSimpleElement(reactNode, ChakraDesignSystem)!,
);

const out = clean(print(code));

expect(out).toMatchSnapshot();
});

test('classnames', () => {
const reactNode = renderResolvedNode({
contentEditable: false,
disableTabNavigation: false,
includeDataProps: false,
system: ChakraDesignSystem,
dsConfig: ds.config,
resolvedNode,
stylingMode: 'tailwind',
});

const code = createElementCode(
Expand Down
24 changes: 23 additions & 1 deletion packages/noya-compiler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from './common';
import { generateThemeFile } from './compileTheme';
import { LayoutNode, LayoutNodeAttributes } from './parseComponentLayout';
import { removeEmptyClassNames } from './passes/removeEmptyClassNames';
import { removeEmptyStyles } from './passes/removeEmptyStyles';
import { removeUndefinedStyles } from './passes/removeUndefinedStyles';
import { format, print } from './print';
Expand Down Expand Up @@ -356,6 +357,7 @@ export function compile(configuration: CompilerConfiguration) {
system: DesignSystem,
dsConfig: configuration.ds.config,
resolvedNode,
stylingMode: 'tailwind',
}),
DesignSystem,
);
Expand Down Expand Up @@ -479,6 +481,24 @@ export function compile(configuration: CompilerConfiguration) {
const themeFile = generateThemeFile(DesignSystem, { theme });

const files = {
'src/app/layout.tsx': `import './globals.css'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
`,
'src/app/globals.css': `@tailwind base;
@tailwind components;
@tailwind utilities;
`,
...Object.fromEntries(
componentPageItems.map(({ name, source }) => [
`src/app/components/${getComponentNameIdentifier(
Expand Down Expand Up @@ -515,7 +535,9 @@ export function clean(text: string) {
ts.ScriptKind.TSX,
);

const updated = removeEmptyStyles(removeUndefinedStyles(sourceFile));
const updated = removeEmptyClassNames(
removeEmptyStyles(removeUndefinedStyles(sourceFile)),
);

return format(print(updated));
}
Expand Down
40 changes: 40 additions & 0 deletions packages/noya-compiler/src/passes/removeEmptyClassNames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as ts from 'typescript';

export function removeEmptyClassNames(
sourceFile: ts.SourceFile,
): ts.SourceFile {
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
// Check if the node is a JSX attribute
if (ts.isJsxAttribute(node)) {
// Check if the attribute name is 'className'
if (node.name.escapedText === 'className') {
// Check if the initializer is a string literal with an empty value
if (
node.initializer &&
ts.isStringLiteral(node.initializer) &&
node.initializer.text === ''
) {
// Return nothing to remove the attribute
return;
}
}
}

// Visit children of the node
return ts.visitEachChild(node, visitor, context);
};

// Apply the visitor to the source file
return (node) => ts.visitNode(node, visitor);
};

// Perform the transformation
const result = ts.transform<ts.SourceFile>(sourceFile, [transformer]);
const transformedSourceFile = result.transformed[0];

// Clean up
result.dispose();

return transformedSourceFile;
}
29 changes: 20 additions & 9 deletions packages/noya-component/src/renderResolvedNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
NoyaProp,
NoyaResolvedNode,
ResolvedHierarchy,
StylingMode,
createPatternSVG,
createSVG,
placeholderImage,
Expand Down Expand Up @@ -61,6 +62,7 @@ export function renderResolvedNode({
resolvedNode,
dsConfig,
system,
stylingMode = 'inline',
theme,
}: {
containerWidth?: number;
Expand All @@ -70,6 +72,7 @@ export function renderResolvedNode({
resolvedNode: NoyaResolvedNode;
dsConfig: DSConfig;
system: DesignSystemDefinition;
stylingMode?: StylingMode;
theme?: any; // Passed into components as _theme
}) {
return ResolvedHierarchy.map<ReactNode>(
Expand Down Expand Up @@ -139,7 +142,7 @@ export function renderResolvedNode({
);
});

classNames = extractTailwindClassesByTheme(
let classNamesForRendering = extractTailwindClassesByTheme(
classNames,
dsConfig.colorMode ?? 'light',
);
Expand All @@ -152,11 +155,14 @@ export function renderResolvedNode({

// Keep classNames starting with sm: and md:, but remove the prefixes.
// Remove any classNames starting with lg:, xl:, and 2xl:.
classNames = extractTailwindClassesByBreakpoint(classNames, breakpoint);
classNamesForRendering = extractTailwindClassesByBreakpoint(
classNamesForRendering,
breakpoint,
);

const style = parametersToTailwindStyle(classNames);
const style = parametersToTailwindStyle(classNamesForRendering);

const variantClassName = findLast(classNames, (className) =>
const variantClassName = findLast(classNamesForRendering, (className) =>
className.startsWith('variant-'),
);
const variant = variantClassName
Expand All @@ -175,6 +181,14 @@ export function renderResolvedNode({
dsConfig.colors.primary
] as Theme['colors']['primary'];

const stylingProps = {
...(stylingMode === 'tailwind' &&
classNames.length > 0 && {
className: classNames.join(' '),
}),
...(stylingMode === 'inline' && { style }),
};

// Render SVGs as React elements
if (
srcProp &&
Expand All @@ -191,10 +205,7 @@ export function renderResolvedNode({
key: indexPath.join('/'),
props: {
...rootElement.props,
style: {
...rootElement.props.style,
...style,
},
...stylingProps,
...(includeDataProps && {
'data-path': element.path.join('/'),
}),
Expand All @@ -205,10 +216,10 @@ export function renderResolvedNode({

return (
<PrimitiveComponent
style={style}
// We use the indexPath as the key, since the element ids aren't stable while
// the layout is being generated.
key={indexPath.join('/')}
{...stylingProps}
_passthrough={{
...(includeDataProps && {
'data-path': element.path.join('/'),
Expand Down
2 changes: 2 additions & 0 deletions packages/noya-component/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,5 @@ export type SelectedComponent = {
variantID?: string;
diff?: NoyaDiff;
};

export type StylingMode = 'inline' | 'tailwind';
5 changes: 5 additions & 0 deletions packages/noya-tailwind/src/tailwind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const classGroups = {
flexDirection: /^(flex-row|flex-col)/,
flex: /^(flex-1|flex-auto|flex-none)/,
flexBasis: /^basis-/,
flexWrap: /^(flex-wrap|flex-nowrap)/,
grow: /^grow/,
shrink: /^shrink/,
alignSelf: /^self/,
Expand Down Expand Up @@ -645,6 +646,10 @@ export const resolveTailwindClass = memoize(function resolveTailwindClass(
objectPosition: className.replace('object-', ''),
};
}
case 'flexWrap':
return {
flexWrap: className === 'flex-nowrap' ? 'nowrap' : 'wrap',
};
case 'grow': {
const value = getValue(className);
return {
Expand Down
1 change: 1 addition & 0 deletions packages/site/src/dseditor/DSEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,7 @@ function DSComponentCode({
system,
dsConfig,
resolvedNode,
stylingMode: 'tailwind',
});

const code = createElementCode(createSimpleElement(reactNode, system)!);
Expand Down

0 comments on commit 2b8075b

Please sign in to comment.