From 108b2c2f5a187386596ddb5c453a7f74fb0c34a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bertalan=20K=C3=B6rmendy?= Date: Wed, 9 Oct 2024 12:11:21 +0200 Subject: [PATCH] Memoized Tailwind config evaluation (#6500) ## Problem The Tailwind config is only accessible inside `useTailwindCompilation`, which means that the upcoming element style plugins won't be able to access it ## Fix Store the last seen text contents of the Tailwind config file along with the last evaluation result. If the new contents of the Tailwind config file are the same as the last seen contents, the last evaluation result, otherwise, re-evaluate the Tailwind config file, and store it, and return it --- editor/package.json | 1 + editor/pnpm-lock.yaml | 39 +---- .../property-controls-local.ts | 146 ++++++++++-------- .../src/core/tailwind/tailwind-compilation.ts | 27 +++- .../core/tailwind/tailwind.spec.browser2.tsx | 84 +--------- editor/src/core/tailwind/tailwind.spec.ts | 72 +++++++++ .../src/core/tailwind/tailwind.test-utils.ts | 85 ++++++++++ editor/webpack.config.js | 1 + 8 files changed, 274 insertions(+), 181 deletions(-) create mode 100644 editor/src/core/tailwind/tailwind.spec.ts create mode 100644 editor/src/core/tailwind/tailwind.test-utils.ts diff --git a/editor/package.json b/editor/package.json index 9da259561b23..1d4fa750356a 100644 --- a/editor/package.json +++ b/editor/package.json @@ -424,6 +424,7 @@ "source-map-loader": "0.2.3", "string-replace-loader": "2.2.0", "style-loader": "0.18.2", + "tailwindcss": "^3.4.13", "tar": "6.0.5", "terser-webpack-plugin": "5.3.9", "three": "0.140.2", diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml index 9c062e509ccd..fe2bc6505a00 100644 --- a/editor/pnpm-lock.yaml +++ b/editor/pnpm-lock.yaml @@ -318,6 +318,7 @@ specifiers: string-replace-loader: 2.2.0 strip-ansi: 6.0.0 style-loader: 0.18.2 + tailwindcss: ^3.4.13 tar: 6.0.5 terser-webpack-plugin: 5.3.9 three: 0.140.2 @@ -643,6 +644,7 @@ devDependencies: source-map-loader: 0.2.3 string-replace-loader: 2.2.0_webpack@5.88.2 style-loader: 0.18.2 + tailwindcss: 3.4.13 tar: 6.0.5 terser-webpack-plugin: 5.3.9_webpack@5.88.2 three: 0.140.2 @@ -669,7 +671,6 @@ packages: /@alloc/quick-lru/5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - dev: false /@alloc/types/1.3.0: resolution: {integrity: sha512-mH7LiFiq9g6rX2tvt1LtwsclfG5hnsmtIfkZiauAGrm1AwXhoRS0sF2WrN9JGN7eV5vFXqNaB0eXZ3IvMsVi9g==} @@ -4649,7 +4650,7 @@ packages: dependencies: '@types/estree': 0.0.39 estree-walker: 1.0.1 - picomatch: 2.3.0 + picomatch: 2.3.1 dev: true /@rollup/pluginutils/4.2.1: @@ -6428,7 +6429,6 @@ packages: /arg/5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: false /argparse/1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -7459,7 +7459,6 @@ packages: /camelcase-css/2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - dev: false /camelcase/5.3.1: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} @@ -8831,7 +8830,6 @@ packages: /didyoumean/1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dev: false /diff-match-patch/1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -8887,7 +8885,6 @@ packages: /dlv/1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dev: false /dnd-core/16.0.1: resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} @@ -10875,7 +10872,6 @@ packages: engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 - dev: false /glob-to-regexp/0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -10902,7 +10898,6 @@ packages: minipass: 7.1.2 package-json-from-dist: 1.0.0 path-scurry: 1.11.1 - dev: false /glob/3.1.21: resolution: {integrity: sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ==} @@ -12334,7 +12329,6 @@ packages: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: false /jake/10.8.2: resolution: {integrity: sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==} @@ -12972,7 +12966,6 @@ packages: /jiti/1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true - dev: false /jotai-devtools/0.6.2_hgjqizhc26gc665cnflpub44vy: resolution: {integrity: sha512-iHKYt8V2T2Gh2DtGRpvpv2daVoFoHRJXqk5/LHnhFkJy9rMQuIGo4XgVu/v1ZMSvMzwDXdU3hDOQkfQWlDErUQ==} @@ -13477,12 +13470,10 @@ packages: /lilconfig/2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - dev: false /lilconfig/3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} - dev: false /lines-and-columns/1.1.6: resolution: {integrity: sha512-8ZmlJFVK9iCmtLz19HpSsR8HaAMWBT284VMNednLwlIMDP2hJDCIhUp0IZ2xUcZ+Ob6BM0VvCSJwzASDM45NLQ==} @@ -14255,7 +14246,6 @@ packages: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - dev: false /nanoid/3.1.20: resolution: {integrity: sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==} @@ -14537,7 +14527,6 @@ packages: /object-hash/3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: false /object-inspect/1.11.0: resolution: {integrity: sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==} @@ -14851,7 +14840,6 @@ packages: /package-json-from-dist/1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} - dev: false /pako/0.2.9: resolution: {integrity: sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=} @@ -14997,6 +14985,7 @@ packages: /path-parse/1.0.6: resolution: {integrity: sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==} + dev: false /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -15065,11 +15054,6 @@ packages: /picocolors/1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - /picomatch/2.3.0: - resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} - engines: {node: '>=8.6'} - dev: true - /picomatch/2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -15207,7 +15191,6 @@ packages: postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.2 - dev: false /postcss-js/4.0.1_postcss@8.4.27: resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} @@ -15217,7 +15200,6 @@ packages: dependencies: camelcase-css: 2.0.1 postcss: 8.4.27 - dev: false /postcss-load-config/4.0.2_postcss@8.4.27: resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} @@ -15234,7 +15216,6 @@ packages: lilconfig: 3.1.2 postcss: 8.4.27 yaml: 2.5.1 - dev: false /postcss-merge-idents/2.1.7: resolution: {integrity: sha1-TFUwMTwI4dWzu/PSu8dH4njuonA=} @@ -15342,7 +15323,6 @@ packages: dependencies: postcss: 8.4.27 postcss-selector-parser: 6.1.2 - dev: false /postcss-normalize-charset/1.1.1: resolution: {integrity: sha1-757nEhLX/nWceO0WL2HtYrXLk/E=} @@ -15401,7 +15381,6 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: false /postcss-svgo/2.1.6: resolution: {integrity: sha1-tt8YqmE7Zm4TPwittSGcJoSsEI0=} @@ -15426,7 +15405,6 @@ packages: /postcss-value-parser/4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: false /postcss-zindex/2.2.0: resolution: {integrity: sha1-0hCd3AVbka9n/EyzsCWUZjnSryI=} @@ -16997,7 +16975,6 @@ packages: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: pify: 2.3.0 - dev: false /read-only-stream/2.0.0: resolution: {integrity: sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=} @@ -17262,7 +17239,7 @@ packages: resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} dependencies: is-core-module: 2.12.1 - path-parse: 1.0.6 + path-parse: 1.0.7 /resolve/1.22.2: resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} @@ -18475,7 +18452,6 @@ packages: mz: 2.7.0 pirates: 4.0.6 ts-interface-checker: 0.1.13 - dev: false /superstruct/0.8.4: resolution: {integrity: sha512-48Ors8IVWZm/tMr8r0Si6+mJiB7mkD7jqvIzktjJ4+EnP5tBp0qOpiM1J8sCUorKx+TXWrfb3i1UcjdD1YK/wA==} @@ -18621,7 +18597,6 @@ packages: sucrase: 3.35.0 transitivePeerDependencies: - ts-node - dev: false /tapable/1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} @@ -18737,13 +18712,11 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 - dev: false /thenify/3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 - dev: false /three/0.139.2: resolution: {integrity: sha512-gV7q7QY8rogu7HLFZR9cWnOQAUedUhu2WXAnpr2kdXZP9YDKsG/0ychwQvWkZN5PlNw9mv5MoCTin6zNTXoONg==} @@ -18938,7 +18911,6 @@ packages: /ts-interface-checker/0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: false /ts-loader/5.3.3_typescript@5.5.4: resolution: {integrity: sha512-KwF1SplmOJepnoZ4eRIloH/zXL195F51skt7reEsS6jvDqzgc/YSbz9b8E07GxIUwLXdcD4ssrJu6v8CwaTafA==} @@ -20139,7 +20111,6 @@ packages: resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} hasBin: true - dev: false /yargs-parser/20.2.4: resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} diff --git a/editor/src/core/property-controls/property-controls-local.ts b/editor/src/core/property-controls/property-controls-local.ts index c4a4beac7048..ffd05dff6e82 100644 --- a/editor/src/core/property-controls/property-controls-local.ts +++ b/editor/src/core/property-controls/property-controls-local.ts @@ -76,7 +76,7 @@ import { right, sequenceEither, } from '../shared/either' -import { assertNever } from '../shared/utils' +import { assertNever, identity } from '../shared/utils' import type { Imports, ParsedTextFile, @@ -157,73 +157,93 @@ function extendExportsWithInfo(exports: any, toImport: string): any { return exports } -export type ModuleEvaluator = (moduleName: string) => any -export function createModuleEvaluator(editor: EditorState): ModuleEvaluator { - return (moduleName: string) => { - let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} } - let topLevelComponentRendererComponents: { - current: MapLike> - } = { current: {} } - const emptyMetadataContext: UiJsxCanvasContextData = { - current: { - spyValues: { - allElementProps: {}, - metadata: {}, - variablesInScope: {}, - }, +export const createRequireFn = ( + editor: EditorState, + moduleName: string, + transform: (result: any, absoluteFilenameOrPackage: string) => any = identity, +) => { + let mutableContextRef: { current: MutableUtopiaCtxRefData } = { current: {} } + let topLevelComponentRendererComponents: { + current: MapLike> + } = { current: {} } + const emptyMetadataContext: UiJsxCanvasContextData = { + current: { + spyValues: { + allElementProps: {}, + metadata: {}, + variablesInScope: {}, }, - } + }, + } - let resolvedFiles: MapLike> = {} - let resolvedFileNames: Array = [moduleName] + let resolvedFiles: MapLike> = {} + let resolvedFileNames: Array = [moduleName] - const requireFn = editor.codeResultCache.curriedRequireFn(editor.projectContents) - const resolve = editor.codeResultCache.curriedResolveFn(editor.projectContents) + const requireFn = editor.codeResultCache.curriedRequireFn(editor.projectContents) + const resolve = editor.codeResultCache.curriedResolveFn(editor.projectContents) - const customRequire = (importOrigin: string, toImport: string) => { - if (resolvedFiles[importOrigin] == null) { - resolvedFiles[importOrigin] = [] - } - let resolvedFromThisOrigin = resolvedFiles[importOrigin] - - const alreadyResolved = resolvedFromThisOrigin[toImport] !== undefined - const filePathResolveResult = alreadyResolved - ? left('Already resolved') - : resolve(importOrigin, toImport) - - forEachRight(filePathResolveResult, (filepath) => resolvedFileNames.push(filepath)) - - const resolvedParseSuccess: Either> = attemptToResolveParsedComponents( - resolvedFromThisOrigin, - toImport, - editor.projectContents, - customRequire, - mutableContextRef, - topLevelComponentRendererComponents, - moduleName, - editor.canvas.base64Blobs, - editor.hiddenInstances, - editor.displayNoneInstances, - emptyMetadataContext, - NO_OP, - false, - filePathResolveResult, - null, - ) - const result = foldEither( - () => { - // We did not find a ParseSuccess, fallback to standard require Fn - return requireFn(importOrigin, toImport, false) - }, - (scope) => { - // Return an artificial exports object that contains our ComponentRendererComponents - return scope - }, - resolvedParseSuccess, - ) - const absoluteFilenameOrPackage = defaultEither(toImport, filePathResolveResult) - return extendExportsWithInfo(result, absoluteFilenameOrPackage) + const customRequire = (importOrigin: string, toImport: string) => { + if (resolvedFiles[importOrigin] == null) { + resolvedFiles[importOrigin] = [] } + let resolvedFromThisOrigin = resolvedFiles[importOrigin] + + const alreadyResolved = resolvedFromThisOrigin[toImport] !== undefined + const filePathResolveResult = alreadyResolved + ? left('Already resolved') + : resolve(importOrigin, toImport) + + forEachRight(filePathResolveResult, (filepath) => resolvedFileNames.push(filepath)) + + const resolvedParseSuccess: Either> = attemptToResolveParsedComponents( + resolvedFromThisOrigin, + toImport, + editor.projectContents, + customRequire, + mutableContextRef, + topLevelComponentRendererComponents, + moduleName, + editor.canvas.base64Blobs, + editor.hiddenInstances, + editor.displayNoneInstances, + emptyMetadataContext, + NO_OP, + false, + filePathResolveResult, + null, + ) + const result = foldEither( + () => { + // We did not find a ParseSuccess, fallback to standard require Fn + return requireFn(importOrigin, toImport, false) + }, + (scope) => { + // Return an artificial exports object that contains our ComponentRendererComponents + return scope + }, + resolvedParseSuccess, + ) + const absoluteFilenameOrPackage = defaultEither(toImport, filePathResolveResult) + return transform(result, absoluteFilenameOrPackage) + } + + return { + customRequire, + emptyMetadataContext, + mutableContextRef, + topLevelComponentRendererComponents, + } +} + +export type ModuleEvaluator = (moduleName: string) => any +export function createModuleEvaluator(editor: EditorState): ModuleEvaluator { + return (moduleName: string) => { + const { + customRequire, + emptyMetadataContext, + mutableContextRef, + topLevelComponentRendererComponents, + } = createRequireFn(editor, moduleName, extendExportsWithInfo) return createExecutionScope( moduleName, customRequire, diff --git a/editor/src/core/tailwind/tailwind-compilation.ts b/editor/src/core/tailwind/tailwind-compilation.ts index 9e030e1b83ad..78f5ef83eae0 100644 --- a/editor/src/core/tailwind/tailwind-compilation.ts +++ b/editor/src/core/tailwind/tailwind-compilation.ts @@ -1,7 +1,7 @@ import React from 'react' import type { TailwindConfig, Tailwindcss } from '@mhsdesign/jit-browser-tailwindcss' import { createTailwindcss } from '@mhsdesign/jit-browser-tailwindcss' -import type { ProjectContentTreeRoot } from 'utopia-shared/src/types' +import type { ProjectContentTreeRoot, TextFile, TextFileContents } from 'utopia-shared/src/types' import { getProjectFileByFilePath, walkContentsTree } from '../../components/assets' import { interactionSessionIsActive } from '../../components/canvas/canvas-strategies/interaction-state' import { CanvasContainerID } from '../../components/canvas/canvas-types' @@ -16,6 +16,31 @@ import type { RequireFn } from '../shared/npm-dependency-types' import { TailwindConfigPath } from './tailwind-config' import { ElementsToRerenderGLOBAL } from '../../components/canvas/ui-jsx-canvas' import { isFeatureEnabled } from '../../utils/feature-switches' +import type { Config } from 'tailwindcss/types/config' +import type { EditorState } from '../../components/editor/store/editor-state' +import { createRequireFn } from '../property-controls/property-controls-local' + +const LatestConfig: { current: { code: string; config: Config } | null } = { current: null } +export function getTailwindConfigCached(editorState: EditorState): Config | null { + const tailwindConfig = getProjectFileByFilePath(editorState.projectContents, TailwindConfigPath) + if (tailwindConfig == null || tailwindConfig.type !== 'TEXT_FILE') { + return null + } + const cached = + LatestConfig.current == null || LatestConfig.current.code !== tailwindConfig.fileContents.code + ? null + : LatestConfig.current.config + + if (cached != null) { + return cached + } + // FIXME this should use a shared long-lived require function instead of creating a brand new one + const { customRequire } = createRequireFn(editorState, TailwindConfigPath) + const config = importDefault(customRequire('/', TailwindConfigPath)) as Config + LatestConfig.current = { code: tailwindConfig.fileContents.code, config: config } + + return config +} const TAILWIND_INSTANCE: { current: Tailwindcss | null } = { current: null } diff --git a/editor/src/core/tailwind/tailwind.spec.browser2.tsx b/editor/src/core/tailwind/tailwind.spec.browser2.tsx index 8040db905fdc..b95956046c32 100644 --- a/editor/src/core/tailwind/tailwind.spec.browser2.tsx +++ b/editor/src/core/tailwind/tailwind.spec.browser2.tsx @@ -1,88 +1,6 @@ import { renderTestEditorWithModel } from '../../components/canvas/ui-jsx.test-utils' -import { createModifiedProject } from '../../sample-projects/sample-project-utils.test-utils' import { setFeatureForBrowserTestsUseInDescribeBlockOnly } from '../../utils/utils.test-utils' -import { wait } from '../model/performance-scripts' - -const Project = createModifiedProject({ - '/utopia/storyboard.js': `import { Scene, Storyboard } from 'utopia-api' - -export var storyboard = ( - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
- Text Shadow - This is a medium text shadow example - This is a large text shadow example - This has no text shadow -
- - -) -`, - '/src/app.css': ` -@tailwind base; -@tailwind components; -@tailwind utilities; -`, - 'tailwind.config.js': ` -const Tailwind = { - theme: { - colors: { - transparent: 'transparent', - current: 'currentColor', - white: '#ffffff', - purple: '#3f3cbb', - midnight: '#121063', - metal: '#565584', - tahiti: '#3ab7bf', - silver: '#ecebff', - 'bubble-gum': '#ff77e9', - bermuda: '#78dcca', - }, - }, - plugins: [ - function ({ addUtilities }) { - const newUtilities = { - '.text-shadow': { - textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', - }, - '.text-shadow-md': { - textShadow: '3px 3px 6px rgba(0, 0, 0, 0.2)', - }, - '.text-shadow-lg': { - textShadow: '4px 4px 8px rgba(0, 0, 0, 0.3)', - }, - '.text-shadow-none': { - textShadow: 'none', - }, - } - - addUtilities(newUtilities, ['responsive', 'hover']) - }, - ], -} -export default Tailwind -`, -}) +import { Project } from './tailwind.test-utils' describe('rendering tailwind projects in the editor', () => { setFeatureForBrowserTestsUseInDescribeBlockOnly('Tailwind', true) diff --git a/editor/src/core/tailwind/tailwind.spec.ts b/editor/src/core/tailwind/tailwind.spec.ts new file mode 100644 index 000000000000..dcaa26fb51c2 --- /dev/null +++ b/editor/src/core/tailwind/tailwind.spec.ts @@ -0,0 +1,72 @@ +import { Project, TailwindConfigFileContents } from './tailwind.test-utils' +import { renderTestEditorWithModel } from '../../components/canvas/ui-jsx.test-utils' +import { TailwindConfigPath } from './tailwind-config' +import { updateFromCodeEditor } from '../../components/editor/actions/actions-from-vscode' +import { getTailwindConfigCached } from './tailwind-compilation' + +describe('tailwind config file in the editor', () => { + it('is set during editor load', async () => { + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + + expect(getTailwindConfigCached(editor.getEditorState().editor)).toMatchInlineSnapshot(` + Object { + "plugins": Array [ + [Function], + ], + "theme": Object { + "colors": Object { + "bermuda": "#78dcca", + "bubble-gum": "#ff77e9", + "current": "currentColor", + "metal": "#565584", + "midnight": "#121063", + "purple": "#3f3cbb", + "silver": "#ecebff", + "tahiti": "#3ab7bf", + "transparent": "transparent", + "white": "#ffffff", + }, + }, + } + `) + }) + it('is updated in the editor state when the tailwind config is updated', async () => { + const editor = await renderTestEditorWithModel(Project, 'await-first-dom-report') + + await editor.dispatch( + [ + updateFromCodeEditor( + TailwindConfigPath, + TailwindConfigFileContents, + ` +const Tailwind = { + theme: { + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#ffffff', + }, + }, + plugins: [ ], + } + export default Tailwind +`, + ), + ], + true, + ) + + expect(getTailwindConfigCached(editor.getEditorState().editor)).toMatchInlineSnapshot(` + Object { + "plugins": Array [], + "theme": Object { + "colors": Object { + "current": "currentColor", + "transparent": "transparent", + "white": "#ffffff", + }, + }, + } + `) + }) +}) diff --git a/editor/src/core/tailwind/tailwind.test-utils.ts b/editor/src/core/tailwind/tailwind.test-utils.ts new file mode 100644 index 000000000000..80b5952ef0ab --- /dev/null +++ b/editor/src/core/tailwind/tailwind.test-utils.ts @@ -0,0 +1,85 @@ +import { createModifiedProject } from '../../sample-projects/sample-project-utils.test-utils' +import { TailwindConfigPath } from './tailwind-config' + +export const TailwindConfigFileContents = ` +const Tailwind = { + theme: { + colors: { + transparent: 'transparent', + current: 'currentColor', + white: '#ffffff', + purple: '#3f3cbb', + midnight: '#121063', + metal: '#565584', + tahiti: '#3ab7bf', + silver: '#ecebff', + 'bubble-gum': '#ff77e9', + bermuda: '#78dcca', + }, + }, + plugins: [ + function ({ addUtilities }) { + const newUtilities = { + '.text-shadow': { + textShadow: '2px 2px 4px rgba(0, 0, 0, 0.1)', + }, + '.text-shadow-md': { + textShadow: '3px 3px 6px rgba(0, 0, 0, 0.2)', + }, + '.text-shadow-lg': { + textShadow: '4px 4px 8px rgba(0, 0, 0, 0.3)', + }, + '.text-shadow-none': { + textShadow: 'none', + }, + } + + addUtilities(newUtilities, ['responsive', 'hover']) + }, + ], + } + export default Tailwind +` + +export const Project = createModifiedProject({ + '/utopia/storyboard.js': `import { Scene, Storyboard } from 'utopia-api' + + export var storyboard = ( + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Text Shadow + This is a medium text shadow example + This is a large text shadow example + This has no text shadow +
+ + + ) + `, + '/src/app.css': ` + @tailwind base; + @tailwind components; + @tailwind utilities; + `, + [TailwindConfigPath]: TailwindConfigFileContents, +}) diff --git a/editor/webpack.config.js b/editor/webpack.config.js index f0c7d9af5995..c236883873db 100644 --- a/editor/webpack.config.js +++ b/editor/webpack.config.js @@ -189,6 +189,7 @@ const config = { extensions: ['.ts', '.tsx', '.js', '.json', '.ttf'], symlinks: true, // We set this to false as we have symlinked some common code from the website project alias: { + 'tailwindcss/resolveConfig': 'tailwindcss/resolveConfig.js', uuiui: srcPath('uuiui'), 'worker-imports': path.resolve(__dirname, 'src/core/workers/worker-import-utils.ts'), 'uuiui-deps': srcPath('uuiui-deps'),