diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a1e3d1862..26977cd568 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,10 @@ "javascriptreact", { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } - ] + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } // see https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb // "editor.formatOnSave": true, // "[javascript]": { diff --git a/app/orbit-app/package.json b/app/orbit-app/package.json index 8e804ecb3d..6cafc68b9e 100644 --- a/app/orbit-app/package.json +++ b/app/orbit-app/package.json @@ -50,6 +50,10 @@ "react-shadow": "^17.1.3", "react-use-gesture": "^5.1.2", "reconnecting-websocket": "4.1.10", + "requestidlecallback-polyfill": "1.0.2", + "resize-observer-polyfill": "1.5.1", + "intersection-observer": "0.5.1", + "array-flat-polyfill": "^1.0.1", "typeorm": "^0.3.0-alpha.23", "webpack-runtime-require": "0.3.0" }, diff --git a/app/orbit-app/src/main.tsx b/app/orbit-app/src/main.tsx index bbfb7e3f3d..ed69c819e3 100644 --- a/app/orbit-app/src/main.tsx +++ b/app/orbit-app/src/main.tsx @@ -1,4 +1,5 @@ import '../public/styles/base.css' +import 'requestidlecallback-polyfill' /** * ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ @@ -96,6 +97,8 @@ async function main() { // we've already started, ignore if (getGlobalConfig()) return + await initPolyfills() + // if you want to show a loading screen, do it above here await fetchInitialConfig() @@ -194,6 +197,23 @@ async function startApp(forceRefresh: boolean | 'mode' = false) { } } +async function initPolyfills() { + let polyfills: any[] = [] + // polyfills + if (!Array.prototype.flatMap) { + polyfills.push(import('array-flat-polyfill')) + } + if (!window['IntersectionObserver']) { + polyfills.push(import('intersection-observer')) + } + if (!window['ResizeObserver']) { + polyfills.push(async () => { + window['ResizeObserver'] = (await import('resize-observer-polyfill')).default + }) + } + await Promise.all(polyfills.map(x => x())) +} + // hot reloading if (process.env.NODE_ENV === 'development') { if (typeof module['hot'] !== 'undefined') { diff --git a/app/orbit-desktop/package.json b/app/orbit-desktop/package.json index 5a8c4f9a42..2daf4fdd02 100644 --- a/app/orbit-desktop/package.json +++ b/app/orbit-desktop/package.json @@ -93,7 +93,7 @@ "time-fix-plugin": "^2.0.6", "ts-loader": "^6.0.1", "typeorm": "^0.3.0-alpha.23", - "typescript": "3.7.1-rc", + "typescript": "3.7.2", "url-loader": "^2.1.0", "webpack": "4.41.2", "webpack-dev-middleware": "^3.7.1", diff --git a/package.json b/package.json index 9a378b5b01..462fe991c9 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,8 @@ "eslint-plugin-react": "7.12.4", "global": "^4.3.2", "jest": "24.8.0", + "react-test-renderer": "^16.11.0", + "@testing-library/react": "^9.3.2", "lerna": "^3.16.4", "lerna-changelog": "^0.8.2", "mobx": "^5.14.0", @@ -107,7 +109,7 @@ "sb-promisify": "^2.0.2", "sqlite": "^3.0.3", "ts-jest": "24.0.0", - "typescript": "3.7.1-rc", + "typescript": "3.7.2", "webpack-cli": "^3.3.7" }, "resolutions": { diff --git a/packages/color/src/color.ts b/packages/color/src/color.ts index 6ef94081ca..e11a99a7dc 100644 --- a/packages/color/src/color.ts +++ b/packages/color/src/color.ts @@ -4,6 +4,8 @@ import { inputToRGB } from './format-input' import { HSL, HSLA, HSV, HSVA, RGB, RGBA } from './interfaces' import { bound01, boundAlpha, clamp01 } from './util' +export * from './memoizeOne' + export interface ColorOptions { format?: ColorFormats gradientType?: string diff --git a/packages/crawler/src/crawler.js b/packages/crawler/src/crawler.js index fb9c9f24d9..e657b1ad8b 100644 --- a/packages/crawler/src/crawler.js +++ b/packages/crawler/src/crawler.js @@ -8,6 +8,7 @@ import sanitizeHtml from 'sanitize-html' import upndown from 'upndown' import URI from 'urijs' import { parse } from 'url' + import CrawlerDB from './crawlerDB' // dont use last two cores if possible diff --git a/packages/css/src/constants.ts b/packages/css/src/constants.ts index d19e477051..21684c6c81 100644 --- a/packages/css/src/constants.ts +++ b/packages/css/src/constants.ts @@ -7,11 +7,69 @@ import { CSSPropertySet } from './cssPropertySet' type CSSPropertyKey = keyof CSSPropertySet type ValidCSSPropertyMap = { [key in CSSPropertyKey]: boolean } -export const validCSSAttr: Partial = +export const SHORTHANDS = { + borderLeftRadius: ['borderTopLeftRadius', 'borderBottomLeftRadius'], + borderRightRadius: ['borderTopRightRadius', 'borderBottomRightRadius'], + borderBottomRadius: ['borderBottomLeftRadius', 'borderBottomRightRadius'], + borderTopRadius: ['borderTopRightRadius', 'borderTopLeftRadius'], +} + +export const validCSSAttr: ValidCSSPropertyMap = process.env.RENDER_TARGET === 'node' ? require('./validCSSAttribute.node').default : require('./validCSSAttribute.dom').default +// conversions +export const CAMEL_TO_SNAKE = {} +export const SNAKE_TO_CAMEL = {} +for (const camelKey in validCSSAttr) { + let snakeKey = '' + if (camelKey.indexOf('webkit') === 0) { + snakeKey += '-' + } + for (const letter of camelKey) { + if (letter.toUpperCase() === letter) { + snakeKey += `-${letter.toLowerCase()}` + } else { + snakeKey += letter + } + } + CAMEL_TO_SNAKE[camelKey] = snakeKey + SNAKE_TO_CAMEL[snakeKey] = camelKey +} + +// css attribute key abbreviations +const existing = new Set() +export const cssAttributeAbbreviations = {} +export const cssAbbreviationToAttribute = {} // inverse +for (const key in validCSSAttr) { + let found = '' + if (key.length < 4) { + found = `${key}-` + } else { + let i = 1 + while (true) { + const abbrevs = getAbbrevs(key) + found = abbrevs.slice(0, i).join('') + if (i > abbrevs.length) { + found += `${i}` + } + found += '-' + if (!existing.has(found)) break + i++ + } + } + existing.add(found) + cssAbbreviationToAttribute[found] = key + cssAttributeAbbreviations[key] = found +} +function getAbbrevs(key: string) { + let options = [key[0]] + const uppercases = key.match(/[A-Z]/g) + if (uppercases) options = [...options, ...uppercases] + return options +} + // various helpful constants export const UNDEFINED = 'undefined' @@ -39,13 +97,6 @@ export const TRANSFORM_KEYS_MAP = { export const COMMA_JOINED = new Set(['boxShadow', 'transition']) -export const SHORTHANDS = { - borderLeftRadius: ['borderTopLeftRadius', 'borderBottomLeftRadius'], - borderRightRadius: ['borderTopRightRadius', 'borderBottomRightRadius'], - borderBottomRadius: ['borderBottomLeftRadius', 'borderBottomRightRadius'], - borderTopRadius: ['borderTopRightRadius', 'borderTopLeftRadius'], -} - export const FALSE_VALUES = { background: 'transparent', backgroundColor: 'transparent', diff --git a/packages/css/src/css.ts b/packages/css/src/css.ts index b6e0f1c120..c329dee1e1 100644 --- a/packages/css/src/css.ts +++ b/packages/css/src/css.ts @@ -1,6 +1,5 @@ import { Config } from './config' -import { BORDER_KEY, COLOR_KEYS, COMMA_JOINED, FALSE_VALUES, SHORTHANDS, TRANSFORM_KEYS_MAP, unitlessNumberProperties } from './constants' -import { CAMEL_TO_SNAKE } from './cssNameMap' +import { BORDER_KEY, CAMEL_TO_SNAKE, COLOR_KEYS, COMMA_JOINED, FALSE_VALUES, SHORTHANDS, TRANSFORM_KEYS_MAP, unitlessNumberProperties } from './constants' import { boxShadowItem, boxShadowSyntax } from './cssPropertySet' import { px, stringHash } from './helpers' @@ -8,7 +7,7 @@ import { px, stringHash } from './helpers' export { GlossPropertySet } from './cssPropertySet' export { configureCSS } from './config' -export { validCSSAttr } from './constants' +export { SNAKE_TO_CAMEL, CAMEL_TO_SNAKE, SHORTHANDS, validCSSAttr, cssAttributeAbbreviations } from './constants' export { CSSPropertySet, CSSPropertySetResolved, @@ -146,6 +145,10 @@ export function cssValue(key: string, value: any, recurse = false, options?: CSS if (value.getCSSValue) { return value.getCSSValue() } + // remove any weird looking objects (non-plain) + if (value.constructor.name !== 'Object') { + return + } const res = processObject(key, value, options) if (res !== undefined) { return res diff --git a/packages/css/src/cssNameMap.ts b/packages/css/src/cssNameMap.ts deleted file mode 100644 index c5a7c64a55..0000000000 --- a/packages/css/src/cssNameMap.ts +++ /dev/null @@ -1,175 +0,0 @@ -export const CAMEL_TO_SNAKE = { - pointerEvents: 'pointer-events', - WebkitAppRegion: '-webkit-app-region', - WebkitLineClamp: '-webkit-line-clamp', - WebkitBoxOrient: '-webkit-box-orient', - WebkitBackgroundClip: '-webkit-background-clip', - WebkitTextFillColor: '-webkit-text-fill-color', - WebkitTextStroke: '-webkit-text-fill-color', - WebkitTextStrokeWidth: '-webkit-text-stroke-width', - WebkitTextStrokeColor: '-webkit-text-stroke-color', - WebkitFontSmoothing: '-webkit-font-smoothing', - alignContent: 'align-content', - alignItems: 'align-items', - alignSelf: 'align-self', - animationDelay: 'animation-delay', - animationDirection: 'animation-direction', - animationDuration: 'animation-duration', - animationFillMode: 'animation-fill-mode', - animationIterationCount: 'animation-iteration-count', - animationName: 'animation-name', - animationPlayState: 'animation-play-state', - animationTimingFunction: 'animation-timing-function', - backfaceVisibility: 'backface-visibility', - backgroundAttachment: 'background-attachment', - backgroundBlendMode: 'background-blend-mode', - backgroundClip: 'background-clip', - backgroundColor: 'background-color', - backgroundImage: 'background-image', - backgroundOrigin: 'background-origin', - backgroundPosition: 'background-position', - backgroundRepeat: 'background-repeat', - backgroundSize: 'background-size', - backdropFilter: 'backdrop-filter', - borderBottom: 'border-bottom', - borderBottomColor: 'border-bottom-color', - borderBottomLeftRadius: 'border-bottom-left-radius', - borderBottomRightRadius: 'border-bottom-right-radius', - borderBottomStyle: 'border-bottom-style', - borderBottomWidth: 'border-bottom-width', - borderRadiusSize: 'border-radius-size', - borderCollapse: 'border-collapse', - borderColor: 'border-color', - borderImage: 'border-image', - borderImageOutset: 'border-image-outset', - borderImageRepeat: 'border-image-repeat', - borderImageSlice: 'border-image-slice', - borderImageSource: 'border-image-source', - borderImageWidth: 'border-image-width', - borderLeft: 'border-left', - borderLeftColor: 'border-left-color', - borderLeftStyle: 'border-left-style', - borderLeftWidth: 'border-left-width', - borderRadius: 'border-radius', - borderRight: 'border-right', - borderRightColor: 'border-right-color', - borderRightStyle: 'border-right-style', - borderRightWidth: 'border-right-width', - borderSpacing: 'border-spacing', - borderStyle: 'border-style', - borderTop: 'border-top', - borderTopColor: 'border-top-color', - borderTopLeftRadius: 'border-top-left-radius', - borderTopRightRadius: 'border-top-right-radius', - borderTopStyle: 'border-top-style', - borderTopWidth: 'border-top-width', - borderWidth: 'border-width', - boxShadow: 'box-shadow', - boxSizing: 'box-sizing', - captionSide: 'caption-side', - columnCount: 'column-count', - columnFill: 'column-fill', - columnGap: 'column-gap', - columnRule: 'column-rule', - columnRuleColor: 'column-rule-color', - columnRuleStyle: 'column-rule-style', - columnRuleWidth: 'column-rule-width', - columnSpan: 'column-span', - columnWidth: 'column-width', - counterIncrement: 'counter-increment', - counterReset: 'counter-reset', - emptyCells: 'empty-cells', - flexBasis: 'flex-basis', - flexDirection: 'flex-direction', - flexFlow: 'flex-flow', - flexGrow: 'flex-grow', - flexShrink: 'flex-shrink', - flexWrap: 'flex-wrap', - fontFamily: 'font-family', - fontSize: 'font-size', - fontSizeAdjust: 'font-size-adjust', - fontStretch: 'font-stretch', - fontStyle: 'font-style', - fontVariant: 'font-variant', - fontWeight: 'font-weight', - hangingPunctuation: 'hanging-punctuation', - justifyContent: 'justify-content', - letterSpacing: 'letter-spacing', - lineHeight: 'line-height', - listStyle: 'list-style', - listStyleImage: 'list-style-image', - listStylePosition: 'list-style-position', - listStyleType: 'list-style-type', - marginBottom: 'margin-bottom', - marginLeft: 'margin-left', - marginRight: 'margin-right', - marginTop: 'margin-top', - maxHeight: 'max-height', - maxWidth: 'max-width', - minHeight: 'min-height', - minWidth: 'min-width', - navDown: 'nav-down', - navIndex: 'nav-index', - navLeft: 'nav-left', - navRight: 'nav-right', - navUp: 'nav-up', - outlineColor: 'outline-color', - outlineOffset: 'outline-offset', - outlineStyle: 'outline-style', - outlineWidth: 'outline-width', - overflowX: 'overflow-x', - overflowY: 'overflow-y', - paddingBottom: 'padding-bottom', - paddingLeft: 'padding-left', - paddingRight: 'padding-right', - paddingTop: 'padding-top', - pageBreakAfter: 'page-break-after', - pageBreakBefore: 'page-break-before', - pageBreakInside: 'page-break-inside', - perspectiveOrigin: 'perspective-origin', - tabSize: 'tab-size', - tableLayout: 'table-layout', - textAlign: 'text-align', - textAlignLast: 'text-align-last', - textDecoration: 'text-decoration', - textDecorationColor: 'text-decoration-color', - textDecorationLine: 'text-decoration-line', - textDecorationStyle: 'text-decoration-style', - textIndent: 'text-indent', - textJustify: 'text-justify', - textOverflow: 'text-overflow', - textShadow: 'text-shadow', - textTransform: 'text-transform', - transformOrigin: 'transform-origin', - transformStyle: 'transform-style', - transitionDelay: 'transition-delay', - transitionDuration: 'transition-duration', - transitionProperty: 'transition-property', - transitionTimingFunction: 'transition-timing-function', - unicodeBidi: 'unicode-bidi', - userSelect: 'user-select', - verticalAlign: 'vertical-align', - whiteSpace: 'white-space', - wordBreak: 'word-break', - wordSpacing: 'word-spacing', - wordWrap: 'word-wrap', - zIndex: 'z-index', - gridGap: 'grid-gap', - gridTemplateColumns: 'grid-template-columns', - gridTemplateRows: 'grid-template-rows', - gridAutoRows: 'grid-auto-rows', - scrollSnapCoordinate: 'scroll-snap-coordinate', - scrollSnapDestination: 'scroll-snap-destination', - scrollSnapPointsX: 'scroll-snap-points-x', - scrollSnapPointsY: 'scroll-snap-points-y', - scrollSnapType: 'scroll-snap-type', - scrollSnapTypeX: 'scroll-snap-type-x', - scrollSnapTypeY: 'scroll-snap-type-y', - scrollSnapAlign: 'scroll-snap-align', - scrollSnapStop: 'scroll-snap-stop', -} - -export const SNAKE_TO_CAMEL = Object.keys(CAMEL_TO_SNAKE).reduce( - (acc, cur) => ({ ...acc, [CAMEL_TO_SNAKE[cur]]: cur }), - {}, -) diff --git a/packages/css/src/cssPropertySet.ts b/packages/css/src/cssPropertySet.ts index db0a0f2d04..b99faaf24a 100644 --- a/packages/css/src/cssPropertySet.ts +++ b/packages/css/src/cssPropertySet.ts @@ -1068,7 +1068,7 @@ export type GenerateCSSPropertySet = { position?: CSSPropertyVal quotes?: CSSPropertyVal resize?: CSSPropertyVal - rest?: CSSPropertyVal + props?: CSSPropertyVal restAfter?: CSSPropertyVal restBefore?: CSSPropertyVal right?: CSSPropertyVal diff --git a/packages/css/src/helpers.ts b/packages/css/src/helpers.ts index b97a35e080..1ab962431c 100644 --- a/packages/css/src/helpers.ts +++ b/packages/css/src/helpers.ts @@ -1,4 +1,4 @@ -import { CAMEL_TO_SNAKE, SNAKE_TO_CAMEL } from './cssNameMap' +import { CAMEL_TO_SNAKE, SNAKE_TO_CAMEL } from './constants' export const px = (x: number | string) => typeof x === 'number' ? `${x}px` : `${+x}` === x ? `${x}px` : x diff --git a/packages/css/src/validCSSAttribute.dom.ts b/packages/css/src/validCSSAttribute.dom.ts index 46475ed9dd..349d49b5cf 100644 --- a/packages/css/src/validCSSAttribute.dom.ts +++ b/packages/css/src/validCSSAttribute.dom.ts @@ -8,6 +8,8 @@ const allCSSAttr = {} // add standard ones if (typeof document !== 'undefined') { for (const key in document.body.style) { + // for some reason chrome has 0-11 as valid css attributes + if (+key === +key) continue allCSSAttr[key] = true } } diff --git a/packages/css/src/validCSSAttributeExtra.ts b/packages/css/src/validCSSAttributeExtra.ts index 9ea5505569..2cdc701454 100644 --- a/packages/css/src/validCSSAttributeExtra.ts +++ b/packages/css/src/validCSSAttributeExtra.ts @@ -1,5 +1,3 @@ -import { CAMEL_TO_SNAKE } from './cssNameMap' - export const validCSSAttributeExtra = { borderLeftRadius: true, borderRightRadius: true, @@ -11,7 +9,3 @@ export const validCSSAttributeExtra = { src: false, size: false, } - -for (const key in CAMEL_TO_SNAKE) { - validCSSAttributeExtra[key] = true -} diff --git a/packages/gloss-webpack/src/ast/extractStyles.ts b/packages/gloss-webpack/src/ast/extractStyles.ts index a6ed51a596..683f9c0a2c 100644 --- a/packages/gloss-webpack/src/ast/extractStyles.ts +++ b/packages/gloss-webpack/src/ast/extractStyles.ts @@ -1,21 +1,11 @@ import * as babel from '@babel/core' -import generate from '@babel/generator' -import traverse, { NodePath } from '@babel/traverse' import * as t from '@babel/types' -import literalToAst from 'babel-literal-to-ast' -import { getGlossProps, GlossStaticStyleDescription, GlossView, isGlossView, StaticUtils, ThemeStyleInfo, tracker, validCSSAttr } from 'gloss' -import invariant from 'invariant' +import { ThemeStyleInfo } from 'gloss' import path from 'path' -import util from 'util' import vm from 'vm' import { CacheObject, ExtractStylesOptions } from '../types' -import { evaluateAstNode, EvaluateASTNodeOptions } from './evaluateAstNode' -import { extractStaticTernaries, Ternary } from './extractStaticTernaries' -import { getPropValueFromAttributes } from './getPropValueFromAttributes' -import { getStaticBindingsForScope } from './getStaticBindingsForScope' -import { htmlAttributes } from './htmlAttributes' -import { parse, parserOptions } from './parse' +import { parserOptions } from './parse' export interface Options { cacheObject: CacheObject @@ -43,7 +33,7 @@ const GLOSS_SOURCES = { '@o/ui/test': true, } -type CSSExtracted = { filename: string, content: string } +type CSSExtracted = { filename: string; content: string } // used for later seeing how much we can extract (and to apply theme styles) // gives us information if the themes can be extracted via trackState @@ -51,1007 +41,1007 @@ const viewInformation: { [key: string]: ThemeStyleInfo } = {} let hasParsedViewInformation = false export function extractStyles( - src: string | Buffer, - sourceFileName: string, - { outPath, outRelPath }: any, - { cacheObject }: Options, - options: ExtractStylesOptions, + _src: string | Buffer, + _sourceFileName: string, + _pathOpts: any, + _opts: Options, + _options: ExtractStylesOptions, ): { js: string | Buffer css: CSSExtracted[] ast: t.File map: any // RawSourceMap from 'source-map' } { - if (typeof src !== 'string') { - throw new Error('`src` must be a string of javascript') - } - invariant( - typeof sourceFileName === 'string' && path.isAbsolute(sourceFileName), - '`sourceFileName` must be an absolute path to a .js file', - ) - invariant( - typeof cacheObject === 'object' && cacheObject !== null, - '`cacheObject` must be an object', - ) - - if (!hasParsedViewInformation) { - hasParsedViewInformation = true - for (const key in options.views) { - const view = options.views[key] - if (isGlossView(view)) { - const defaultProps = view.internal.glossProps.defaultProps ?? {} - viewInformation[key] = StaticUtils.getThemeStyles(view, options.defaultTheme, defaultProps) - } - } - } - - const mediaQueryPrefixes = options.mediaQueryKeys.map(x => `${x}-`) - const sourceDir = path.dirname(sourceFileName) - - // Using a map for (officially supported) guaranteed insertion order - const cssMap = new Map() - const ast = parse(src) - let glossSrc = false - const validComponents = {} - // default to using require syntax - let useImportSyntax = false - const shouldPrintDebug = src[0] === '/' && src[1] === '/' && src[2] === '!' - const views: { [key: string]: GlossView } = {} - const JSX_VALID_NAMES = Object.keys(options.views).filter(x => { - return options.views[x] && !!options.views[x].staticStyleConfig - }) - - // we allow things within the ui kit to avoid the more tedious config - const isInternal = options.internalViewsPaths?.some(x => sourceFileName.indexOf(x) === 0) ?? false - - let importsGloss = false - - // Find gloss require in program root - ast.program.body = ast.program.body.filter((item: t.Node) => { - if (t.isImportDeclaration(item)) { - // not imported from gloss? byeeee - if (item.source.value === 'gloss') { - importsGloss = true - } - if (!importsGloss && !isInternal && !GLOSS_SOURCES[item.source.value]) { - return true - } - glossSrc = true - useImportSyntax = true - item.specifiers = item.specifiers.filter(specifier => { - // keep the weird stuff - if ( - !t.isImportSpecifier(specifier) || - !t.isIdentifier(specifier.imported) || - !t.isIdentifier(specifier.local) - ) { - return true - } - if (specifier.local.name[0] !== specifier.local.name[0].toUpperCase()) { - return true - } - if (!JSX_VALID_NAMES.includes(specifier.local.name)) { - return true - } - views[specifier.local.name] = options.views[specifier.local.name] - validComponents[specifier.local.name] = true - if (shouldPrintDebug) { - console.log('found valid component', specifier.local.name) - } - // dont remove the import - return true - }) - } - return true - }) - - /** - * Step 1: Compiled the gloss() style views and remember if they are able to be compiled - * in step 2 - */ - const localStaticViews: { - [key: string]: { - staticDesc: GlossStaticStyleDescription, - propObject: any - defaultProps?: Object - parent?: GlossView - } - } = {} - - if (importsGloss || isInternal) { - traverse(ast, { - VariableDeclaration(path) { - const dec = path.node.declarations[0] - if (!dec || !t.isVariableDeclarator(dec) || !t.isIdentifier(dec.id)) return - const name = dec.id.name - - // traverse and find gloss call - let glossCall: t.CallExpression - let chain = dec.init - while ( - t.isCallExpression(chain) && - t.isMemberExpression(chain.callee) && - t.isCallExpression(chain.callee.object) - ) { - chain = chain.callee.object - } - // verify we found it - if ( - t.isCallExpression(chain) && - t.isIdentifier(chain.callee) && - chain.callee.name === 'gloss' - ) { - // simple gloss without .theme etc - glossCall = chain - } - - if (!glossCall || !glossCall.arguments.length) { - return - } - - validComponents[name] = true - - const localViewName = t.isIdentifier(glossCall.arguments[0]) && glossCall.arguments[0].name - let view: GlossView | null = null - - // parse style objects out and return them as array of [{ ['namespace']: 'className' }] - let staticStyleDesc: GlossStaticStyleDescription | null = null - - // this stuff is used by step 2 (theme functions) - // any props leftover after parsing gloss style props - let restDefaultProps = {} - - const evaluate = createEvaluator(path, sourceFileName) - - glossCall.arguments = glossCall.arguments.map((arg, index) => { - if ((index === 0 || index === 1) && t.isObjectExpression(arg)) { - let propObject = {} - let unevaluated: any[] = [] - try { - const opts: EvaluateASTNodeOptions = { - evaluateFunctions: false, - unevaluated: [] - } - propObject = evaluate(arg, opts) - unevaluated = opts.unevaluated - if (shouldPrintDebug) { - console.log('propObject', propObject) - } - } catch (err) { - console.log('Cant parse style object - this is ok, just de-opt', name, '>', localViewName) - if (shouldPrintDebug) { - console.log('err', err) - } - return arg - } - - // if no error, set local view info - if (localViewName) { - // extends one of our optimizable views - if (views[localViewName]) { - views[name] = options.views[localViewName] - view = views[name] - } - } - - // uses the base styles if necessary, merges just like gloss does - const { styles, conditionalStyles, defaultProps, internalDefaultProps } = getGlossProps( - propObject, - view, - view?.internal?.depth ?? 0 - ) - if (shouldPrintDebug) { - console.log('glossCall.arguments parse gloss props', name, styles, conditionalStyles) - } - - // then put them all into an array so gloss later can use that - const out: GlossStaticStyleDescription = { - className: '', - } - - if (defaultProps.className) { - out.className = defaultProps.className - delete defaultProps.className - } - - const depth = (view?.internal?.depth ?? -1) + 1 - - for (const ns in styles) { - const info = StaticUtils.getStyles(styles[ns], depth, ns) - if (shouldPrintDebug) { - console.log('got static extract', name, ns, info, styles[ns]) - } - if (info) { - cssMap.set(info.className, { css: info.css, commentTexts: [] }) - out.className += ` ${info.className}` - } else { - if (shouldPrintDebug) { - console.log('no info', ns, styles) - } - } - } - - if (conditionalStyles) { - out.conditionalClassNames = {} - for (const prop in conditionalStyles) { - out.conditionalClassNames[prop] = '' - for (const ns in conditionalStyles[prop]) { - const val = conditionalStyles[prop][ns] - const info = StaticUtils.getStyles(val, depth, ns) - if (info) { - cssMap.set(info.className, { css: info.css, commentTexts: [] }) - out.conditionalClassNames[prop] += ` ${info.className}` - } - } - } - } - - localStaticViews[name] = { - staticDesc: out, - propObject, - defaultProps, - parent: view, - } - - if (out.className || out.conditionalClassNames) { - staticStyleDesc = out - } - - // keep any non-style props on the glossProps - let defaultPropsLeave = unevaluated - - if (internalDefaultProps && Object.keys(internalDefaultProps).length) { - const objectLeaveProps = literalToAst(internalDefaultProps) as t.ObjectExpression - defaultPropsLeave = [ - ...defaultPropsLeave, - ...objectLeaveProps.properties - ] - } - - // if we have defaultProps or unevaluated non-extracted items, leave them - if (defaultPropsLeave.length) { - return t.objectExpression(defaultPropsLeave) - } - - return t.nullLiteral() - } - return arg - }) - - // add it to runtime: gloss(View, null, { ...staticStyleDesc }) - if (staticStyleDesc) { - if (glossCall.arguments.length === 1) { - glossCall.arguments.push(t.nullLiteral()) - } - glossCall.arguments.push(literalToAst(staticStyleDesc)) - } - - if (Object.keys(restDefaultProps).length) { - const argIndex = t.isNullLiteral(glossCall.arguments[0]) ? 0 : 1 - console.log('restDefaultProps', restDefaultProps) - glossCall.arguments[argIndex] = literalToAst(restDefaultProps) - } - }, - }) - } - - // gloss isn't included anywhere, so let's bail - if (!glossSrc || !Object.keys(validComponents).length) { - return { - ast, - css: [], - js: src, - map: null, - } - } - - // creates an evaluator to get complex values from babel in this path - function createEvaluator(path: NodePath, sourceFileName: string, defaultOpts?: EvaluateASTNodeOptions) { - // Generate scope object at this level - const staticNamespace = getStaticBindingsForScope( - path.scope, - sourceFileName, - // per-file cache of evaluated bindings - // TODO can be per-module? - {}, - options.whitelistStaticModules, - execFile, - ) - const evalContext = vm.createContext(staticNamespace) - const evalFn = (n: t.Node) => { - // called when evaluateAstNode encounters a dynamic-looking prop - // variable - if (t.isIdentifier(n)) { - invariant( - staticNamespace[n.name], - 'identifier not in staticNamespace', - ) - return staticNamespace[n.name] - } - return vm.runInContext(`(${generate(n).code})`, evalContext) - } - return (n: t.Node, o?: EvaluateASTNodeOptions) => { - return evaluateAstNode(n, evalFn, { ...defaultOpts, ...o }) - } - } - - - /** - * Step 2: Statically extract from JSX < /> nodes - */ - traverse(ast, { - JSXElement: { - enter(traversePath: TraversePath) { - const node = traversePath.node.openingElement - if ( - // skip non-identifier opening elements (member expressions, etc.) - !t.isJSXIdentifier(node.name) || - // skip non-gloss components - !validComponents[node.name.name] - ) { - return - } - - // Remember the source component - const originalNodeName = node.name.name - const localView = localStaticViews[originalNodeName] - let view = views[originalNodeName] - // for parentView config - let extraDepth = 0 - let domNode = 'div' - - let staticStyleConfig: GlossView['staticStyleConfig'] | null = null - if (view) { - staticStyleConfig = view.staticStyleConfig - // lets us have plain functional views like Stack - if (staticStyleConfig.parentView) { - view = staticStyleConfig.parentView - extraDepth = 1 - } - domNode = view.staticStyleConfig?.tagName - ?? view.internal?.glossProps.defaultProps.tagName - ?? 'div' - } - - // Get valid css props - const cssAttributes = staticStyleConfig?.cssAttributes || validCSSAttr - - function isCSSAttribute(name: string) { - if (cssAttributes[name]) return true - if (mediaQueryPrefixes.some(x => name.indexOf(x) === 0)) return true - return false - } - - const attemptEval = evaluateAstNode //createEvaluator(traversePath as any, sourceFileName, { evaluateFunctions: false }) - - let lastSpreadIndex: number = -1 - const flattenedAttributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [] - node.attributes.forEach(attr => { - if (t.isJSXSpreadAttribute(attr)) { - try { - const spreadValue = attemptEval(attr.argument) - - if (typeof spreadValue !== 'object' || spreadValue == null) { - lastSpreadIndex = flattenedAttributes.push(attr) - 1 - } else { - for (const k in spreadValue) { - const value = spreadValue[k] - - if (typeof value === 'number') { - flattenedAttributes.push( - t.jsxAttribute( - t.jsxIdentifier(k), - t.jsxExpressionContainer(t.numericLiteral(value)), - ), - ) - } else if (value === null) { - // why would you ever do this - flattenedAttributes.push( - t.jsxAttribute(t.jsxIdentifier(k), t.jsxExpressionContainer(t.nullLiteral())), - ) - } else { - // toString anything else - // TODO: is this a bad idea - flattenedAttributes.push( - t.jsxAttribute( - t.jsxIdentifier(k), - t.jsxExpressionContainer(t.stringLiteral('' + value)), - ), - ) - } - } - } - } catch (e) { - lastSpreadIndex = flattenedAttributes.push(attr) - 1 - } - } else { - flattenedAttributes.push(attr) - } - }) - - node.attributes = flattenedAttributes - - const staticAttributes: Record = {} - const htmlExtractedAttributes = {} - let inlinePropCount = 0 - const staticTernaries: Ternary[] = [] - const classNameObjects: (t.StringLiteral | t.Expression)[] = [] - - let shouldDeopt = false - - const ogAttributes = node.attributes - node.attributes = node.attributes.filter((attribute, idx) => { - if ( - t.isJSXSpreadAttribute(attribute) || - // keep the weirdos - !attribute.name || - // filter out JSXIdentifiers - typeof attribute.name.name !== 'string' || - // haven't hit the last spread operator - idx < lastSpreadIndex - ) { - if (shouldPrintDebug) console.log('inline prop via non normal attr') - inlinePropCount++ - return true - } - - let name = attribute.name.name - - // for fully deoptimizing certain keys - if (staticStyleConfig) { - if (staticStyleConfig.deoptProps?.includes(name)) { - shouldDeopt = true - return true - } - // for avoiding processing certain keys - if (staticStyleConfig.avoidProps?.includes(name)) { - if (shouldPrintDebug) console.log('inline prop via avoidProps') - inlinePropCount++ - return true - } - } - - let value: any = t.isJSXExpressionContainer(attribute?.value) - ? attribute.value.expression - : attribute.value - - // boolean prop / conditionals - const allConditionalClassNames = { - ...view?.internal?.compiledInfo?.conditionalClassNames ?? null, - ...localView?.staticDesc?.conditionalClassNames ?? null, - } - if (allConditionalClassNames[name]) { - // we can just extract to className - if (value === null) { - // extract but still put it onto staticAttributes for themeFn to use - staticAttributes[name] = true - return false - } - - // if dynamic value we just use it on className - classNameObjects.push( - t.conditionalExpression( - value, - t.stringLiteral(allConditionalClassNames[name]), - t.stringLiteral('') - ) - ) - return false - } - - // boolean props have null value - if (!value) { - inlinePropCount++ - return true - } - - // if one or more spread operators are present and we haven't hit the last one yet, the prop stays inline - if (lastSpreadIndex > -1 && idx <= lastSpreadIndex) { - inlinePropCount++ - return true - } - // pass ref, key, and style props through untouched - if (UNTOUCHED_PROPS[name]) { - return true - } - - if (name === 'ref') { - inlinePropCount++ - return true - } - - const trackState = viewInformation[originalNodeName]?.trackState - if (trackState) { - if (trackState?.nonCSSVariables?.has(name)) { - if (shouldPrintDebug) console.log('inline prop via nonCSSVariables') - inlinePropCount++ - return true - } - } - - if (!isCSSAttribute(name)) { - // we can safely leave html attributes - // TODO make this more customizable / per-tagname - if (htmlAttributes[name]) { - try { - htmlExtractedAttributes[name] = attemptEval(value) - } catch(err) { - // oo fancy! this basically says if we can't eval this safely, and its used by the themeFn - // then we need to deopt here. if it can be evaluated, we're good, we'll run theme here later - if (trackState?.usedProps?.has(name)) { - if (shouldPrintDebug) { - console.log('we use this in this component', name) - } - inlinePropCount++ - return true - } - // console.log('err getting html attr', name, err.message) - // ok - } - return true - } - if (shouldPrintDebug) console.log('inline prop via !isCSSAttribute') - inlinePropCount++ - return true - } - - // allow statically defining a change from one prop to another (see Stack) - if (typeof cssAttributes[name] === 'object') { - if (t.isStringLiteral(value)) { - const definition = cssAttributes[name] - name = definition.name - value = definition.value[value.value] - staticAttributes[name] = value - return false - } else { - console.log('couldnt parse a user defined cssAttribute', name, value) - inlinePropCount++ - return true - } - } - - // if value can be evaluated, extract it and filter it out - try { - staticAttributes[name] = attemptEval(value) - return false - } catch { - // ok - } - - if (t.isConditionalExpression(value)) { - // if both sides of the ternary can be evaluated, extract them - try { - const consequent = attemptEval(value.consequent) - const alternate = attemptEval(value.alternate) - staticTernaries.push({ - alternate, - consequent, - name, - test: value.test, - }) - // mark the prop as extracted - staticAttributes[name] = null - return false - } catch (e) { - // - } - } else if (t.isLogicalExpression(value)) { - // convert a simple logical expression to a ternary with a null alternate - if (value.operator === '&&') { - try { - const consequent = attemptEval(value.right) - staticTernaries.push({ - alternate: null, - consequent, - name, - test: value.left, - }) - staticAttributes[name] = null - return false - } catch (e) { - // - } - } - } - - // if we've made it this far, the prop stays inline - if (shouldPrintDebug) console.log('inline prop via no match') - inlinePropCount++ - return true - }) - - if (shouldPrintDebug) { - console.log(`ok we parsed a JSX: -name: ${node.name.name} -inlinePropCount: ${inlinePropCount} -shouldDeopt: ${shouldDeopt} -domNode: ${domNode} - `) - } - - if (shouldDeopt) { - node.attributes = ogAttributes - return - } - - let classNamePropValue: t.Expression | null = null - - const extractedStaticAttrs = Object.keys(staticAttributes).length > 0 - const classNamePropIndex = node.attributes.findIndex( - attr => !t.isJSXSpreadAttribute(attr) && attr.name && attr.name.name === 'className', - ) - if (classNamePropIndex > -1) { - classNamePropValue = getPropValueFromAttributes('className', node.attributes) - node.attributes.splice(classNamePropIndex, 1) - } - - // used later to generate classname for item - const stylesByClassName: { [key: string]: string } = {} - - const depth = (view?.internal?.depth ?? 1) + (localView?.parent?.internal?.depth ?? 1) + extraDepth - const addStyles = (styleObj: any) => { - const allStyles = StaticUtils.getAllStyles(styleObj, depth) - for (const info of allStyles) { - if (info.css) { - if (shouldPrintDebug) { - console.log('add static styles', info.className, info.css) - } - stylesByClassName[info.className] = info.css - } - } - } - - // capture views where they set it afterwards - // plus any defaults passed through gloss - const viewDefaultProps = { - ...view?.defaultProps, - ...view?.internal?.glossProps?.defaultProps, - } - - if (extractedStaticAttrs) { - const staticStyleProps = { - ...viewDefaultProps, - ...localView?.propObject, - ...htmlExtractedAttributes, - ...staticAttributes, - } - if (shouldPrintDebug) { - // ignoreAttrs is usually huge - const { ignoreAttrs, ...rest } = staticStyleProps - console.log('adding static style props', rest) - } - addStyles(staticStyleProps) - } - - // if all style props have been extracted, gloss component can be - // converted to a div or the specified component - if (inlinePropCount === 0) { - // add in any local static classes - if (localView) { - stylesByClassName[localView.staticDesc.className] = null - } - - const themeFns = view?.internal?.getConfig()?.themeFns - if (themeFns) { - // TODO we need to determine if this theme should deopt using the same proxy/tracker as gloss - try { - const props = { - ...viewDefaultProps, - ...localView?.propObject, - ...htmlExtractedAttributes, - ...staticAttributes, - } - const extracted = StaticUtils.getThemeStyles(view, options.defaultTheme, props).themeStyles - if (shouldPrintDebug) { - delete props['ignoreAttrs'] // ignore this its huge in debug output - console.log('extracting from theme', !!localView, props, extracted) - } - for (const x of extracted) { - stylesByClassName[x.className] = x.css - } - } catch(err) { - console.log('error running theme', sourceFileName, err.message) - return - } - } - - // add any default html props to tag - for (const key in viewDefaultProps) { - const val = viewDefaultProps[key] - if (key === 'className') { - classNameObjects.push(t.stringLiteral(val)) - continue - } - if (htmlAttributes[key]) { - // @ts-ignore - if (!node.attributes.some(x => x?.name?.name === key)) { - // add to start so if its spread onto later its overwritten - node.attributes.unshift( - t.jsxAttribute( - t.jsxIdentifier(key), - t.jsxExpressionContainer(literalToAst(val)) - ) - ) - } - } - } - - // add a data-is="Name" so we can debug it more easily - node.attributes.push( - t.jsxAttribute( - t.jsxIdentifier('data-is'), - t.stringLiteral(node.name.name) - ) - ) - - if (localView) { - node.name.name = domNode - } - - // if they set a staticStyleConfig.parentView (see Stack) - if (!isGlossView(view)) { - if (view?.staticStyleConfig.parentView) { - view = view.staticStyleConfig.parentView - } else { - node.name.name = domNode - } - } - - // if gloss view we may be able to optimize - if (isGlossView(view)) { - // local views we already parsed the css out - const localView = localStaticViews[node.name.name] - if (localView) { - // - } else { - const { staticClasses } = view.internal.glossProps - // internal classes - for (const className of staticClasses) { - const item = tracker.get(className.slice(2)) - const css = `${item.selector} { ${item.style} }` - stylesByClassName[className] = css - } - } - node.name.name = domNode - } - - } else { - if (lastSpreadIndex > -1) { - // if only some style props were extracted AND additional props are spread onto the component, - // add the props back with null values to prevent spread props from incorrectly overwriting the extracted prop value - Object.keys(staticAttributes).forEach(attr => { - node.attributes.push( - t.jsxAttribute(t.jsxIdentifier(attr), t.jsxExpressionContainer(t.nullLiteral())), - ) - }) - } - } - - if (traversePath.node.closingElement) { - // this seems strange - if (t.isJSXMemberExpression(traversePath.node.closingElement.name)) return - traversePath.node.closingElement.name.name = node.name.name - } - - if (shouldPrintDebug) { - console.log('stylesByClassName pre ternaries', stylesByClassName) - } - - const extractedStyleClassNames = Object.keys(stylesByClassName).join(' ') - - if (classNamePropValue) { - try { - const evaluatedValue = attemptEval(classNamePropValue) - classNameObjects.push(t.stringLiteral(evaluatedValue)) - } catch (e) { - classNameObjects.push(classNamePropValue) - } - } - - if (staticTernaries.length > 0) { - const ternaryObj = extractStaticTernaries(staticTernaries, cacheObject) - if (shouldPrintDebug) { - console.log('staticTernaries', staticTernaries, '\nternaryObj', ternaryObj) - } - // ternaryObj is null if all of the extracted ternaries have falsey consequents and alternates - if (ternaryObj !== null) { - // add extracted styles by className to existing object - Object.assign(stylesByClassName, ternaryObj.stylesByClassName) - classNameObjects.push(ternaryObj.ternaryExpression) - } - } - - if (extractedStyleClassNames) { - classNameObjects.push(t.stringLiteral(extractedStyleClassNames)) - if (shouldPrintDebug) { - console.log('extractedStyleClassNames', extractedStyleClassNames) - } - } - - const classNamePropValueForReals = classNameObjects.reduce( - (acc, val) => { - if (acc == null) { - if ( - // pass conditional expressions through - t.isConditionalExpression(val) || - // pass non-null literals through - t.isStringLiteral(val) || - t.isNumericLiteral(val) - ) { - return val - } - return t.logicalExpression('||', val, t.stringLiteral('')) - } - - let inner: t.Expression - if (t.isStringLiteral(val)) { - if (t.isStringLiteral(acc)) { - // join adjacent string literals - return t.stringLiteral(`${acc.value} ${val.value}`) - } - inner = t.stringLiteral(` ${val.value}`) - } else if (t.isLiteral(val)) { - inner = t.binaryExpression('+', t.stringLiteral(' '), val) - } else if (t.isConditionalExpression(val) || t.isBinaryExpression(val)) { - if (t.isStringLiteral(acc)) { - return t.binaryExpression('+', t.stringLiteral(`${acc.value} `), val) - } - inner = t.binaryExpression('+', t.stringLiteral(' '), val) - } else if (t.isIdentifier(val) || t.isMemberExpression(val)) { - // identifiers and member expressions make for reasonable ternaries - inner = t.conditionalExpression( - val, - t.binaryExpression('+', t.stringLiteral(' '), val), - t.stringLiteral(''), - ) - } else { - if (t.isStringLiteral(acc)) { - return t.binaryExpression( - '+', - t.stringLiteral(`${acc.value} `), - t.logicalExpression('||', val, t.stringLiteral('')), - ) - } - // use a logical expression for more complex prop values - inner = t.binaryExpression( - '+', - t.stringLiteral(' '), - t.logicalExpression('||', val, t.stringLiteral('')), - ) - } - return t.binaryExpression('+', acc, inner) - }, - null, - ) - - if (shouldPrintDebug) { - console.log('classNamePropValueForReals', classNamePropValueForReals) - } - - if (classNamePropValueForReals) { - if (t.isStringLiteral(classNamePropValueForReals)) { - node.attributes.push( - t.jsxAttribute( - t.jsxIdentifier('className'), - t.stringLiteral(classNamePropValueForReals.value), - ), - ) - } else { - node.attributes.push( - t.jsxAttribute( - t.jsxIdentifier('className'), - t.jsxExpressionContainer(classNamePropValueForReals), - ), - ) - } - } - - const lineNumbers = - node.loc && - node.loc.start.line + - (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : '') - - const comment = util.format( - '/* %s:%s (%s) */', - sourceFileName.replace(process.cwd(), '.'), - lineNumbers, - originalNodeName, - ) - - for (const className in stylesByClassName) { - if (cssMap.has(className)) { - if (comment) { - const val = cssMap.get(className)! - val.commentTexts.push(comment) - cssMap.set(className, val) - } - } else { - const css = stylesByClassName[className] - if (css) { - if (typeof css !== 'string') { - throw new Error(`CSS is not a string, for ${className}: ${JSON.stringify(stylesByClassName, null, 2)}, ${typeof css}`) - } - cssMap.set(className, { css, commentTexts: [comment] }) - } - } - } - }, - exit(traversePath: TraversePath) { - if (traversePath._complexComponentProp) { - if (t.isJSXElement(traversePath.parentPath)) { - // bump - traversePath.parentPath._complexComponentProp = [].concat( - traversePath.parentPath._complexComponentProp || [], - traversePath._complexComponentProp, - ) - } else { - // find nearest Statement - let statementPath = traversePath - do { - statementPath = statementPath.parentPath - } while (!t.isStatement(statementPath)) - - invariant(t.isStatement(statementPath), 'Could not find a statement') - - const decs = t.variableDeclaration('var', [].concat(traversePath._complexComponentProp)) - - statementPath.insertBefore(decs) - } - traversePath._complexComponentProp = null - } - }, - }, - }) - - const css: CSSExtracted[] = [] - - // Write out CSS using it's className, this gives us de-duping for shared classnames - for (const [className, entry] of cssMap.entries()) { - const content = `${entry.commentTexts.map(txt => `${txt}\n`).join('')}${entry.css}` - const name = `${className}__gloss.css` - const importPath = `${outRelPath}/${name}` - const filename = path.join(outPath, name) - // append require/import statement to the document - if (content !== '') { - css.push({ filename, content }) - if (useImportSyntax) { - ast.program.body.unshift(t.importDeclaration([], t.stringLiteral(importPath))) - } else { - ast.program.body.unshift( - t.expressionStatement( - t.callExpression(t.identifier('require'), [t.stringLiteral(importPath)]), - ), - ) - } - } - } - - const result = generate( - ast, - { - compact: 'auto', - concise: false, - filename: sourceFileName, - // @ts-ignore - quotes: 'single', - retainLines: false, - sourceFileName, - sourceMaps: true, - }, - src, - ) - - if (shouldPrintDebug) { - console.log('output >> ', result.code) - console.log('css output >>', css) - } - - return { - ast, - css, - js: result.code, - map: result.map, - } + return {} as any + // const { cacheObject } = opts + // const { outPath, outRelPath } = pathOpts + // if (typeof src !== 'string') { + // throw new Error('`src` must be a string of javascript') + // } + // invariant( + // typeof sourceFileName === 'string' && path.isAbsolute(sourceFileName), + // '`sourceFileName` must be an absolute path to a .js file', + // ) + // invariant( + // typeof cacheObject === 'object' && cacheObject !== null, + // '`cacheObject` must be an object', + // ) + + // if (!hasParsedViewInformation) { + // hasParsedViewInformation = true + // for (const key in options.views) { + // const view = options.views[key] + // if (isGlossView(view)) { + // const defaultProps = view.internal.glossProps.defaultProps ?? {} + // viewInformation[key] = StaticUtils.getThemeStyles(view, options.defaultTheme, defaultProps) + // } + // } + // } + + // const mediaQueryPrefixes = options.mediaQueryKeys.map(x => `${x}-`) + // const sourceDir = path.dirname(sourceFileName) + + // // Using a map for (officially supported) guaranteed insertion order + // const cssMap = new Map() + // const ast = parse(src) + // let glossSrc = false + // const validComponents = {} + // // default to using require syntax + // let useImportSyntax = false + // const shouldPrintDebug = src[0] === '/' && src[1] === '/' && src[2] === '!' + // const views: { [key: string]: GlossView } = {} + // const JSX_VALID_NAMES = Object.keys(options.views).filter(x => { + // return options.views[x] && !!options.views[x].staticStyleConfig + // }) + + // // we allow things within the ui kit to avoid the more tedious config + // const isInternal = options.internalViewsPaths?.some(x => sourceFileName.indexOf(x) === 0) ?? false + + // let importsGloss = false + + // // Find gloss require in program root + // ast.program.body = ast.program.body.filter((item: t.Node) => { + // if (t.isImportDeclaration(item)) { + // // not imported from gloss? byeeee + // if (item.source.value === 'gloss') { + // importsGloss = true + // } + // if (!importsGloss && !isInternal && !GLOSS_SOURCES[item.source.value]) { + // return true + // } + // glossSrc = true + // useImportSyntax = true + // item.specifiers = item.specifiers.filter(specifier => { + // // keep the weird stuff + // if ( + // !t.isImportSpecifier(specifier) || + // !t.isIdentifier(specifier.imported) || + // !t.isIdentifier(specifier.local) + // ) { + // return true + // } + // if (specifier.local.name[0] !== specifier.local.name[0].toUpperCase()) { + // return true + // } + // if (!JSX_VALID_NAMES.includes(specifier.local.name)) { + // return true + // } + // views[specifier.local.name] = options.views[specifier.local.name] + // validComponents[specifier.local.name] = true + // if (shouldPrintDebug) { + // console.log('found valid component', specifier.local.name) + // } + // // dont remove the import + // return true + // }) + // } + // return true + // }) + + // /** + // * Step 1: Compiled the gloss() style views and remember if they are able to be compiled + // * in step 2 + // */ + // const localStaticViews: { + // [key: string]: { + // staticDesc: GlossStaticStyleDescription, + // propObject: any + // defaultProps?: Object + // parent?: GlossView + // } + // } = {} + + // if (importsGloss || isInternal) { + // traverse(ast, { + // VariableDeclaration(path) { + // const dec = path.node.declarations[0] + // if (!dec || !t.isVariableDeclarator(dec) || !t.isIdentifier(dec.id)) return + // const name = dec.id.name + + // // traverse and find gloss call + // let glossCall: t.CallExpression + // let chain = dec.init + // while ( + // t.isCallExpression(chain) && + // t.isMemberExpression(chain.callee) && + // t.isCallExpression(chain.callee.object) + // ) { + // chain = chain.callee.object + // } + // // verify we found it + // if ( + // t.isCallExpression(chain) && + // t.isIdentifier(chain.callee) && + // chain.callee.name === 'gloss' + // ) { + // // simple gloss without .theme etc + // glossCall = chain + // } + + // if (!glossCall || !glossCall.arguments.length) { + // return + // } + + // validComponents[name] = true + + // const localViewName = t.isIdentifier(glossCall.arguments[0]) && glossCall.arguments[0].name + // let view: GlossView | null = null + + // // parse style objects out and return them as array of [{ ['namespace']: 'className' }] + // let staticStyleDesc: GlossStaticStyleDescription | null = null + + // // this stuff is used by step 2 (theme functions) + // // any props leftover after parsing gloss style props + // let restDefaultProps = {} + + // const evaluate = createEvaluator(path, sourceFileName) + + // glossCall.arguments = glossCall.arguments.map((arg, index) => { + // if ((index === 0 || index === 1) && t.isObjectExpression(arg)) { + // let propObject = {} + // let unevaluated: any[] = [] + // try { + // const opts: EvaluateASTNodeOptions = { + // evaluateFunctions: false, + // unevaluated: [] + // } + // propObject = evaluate(arg, opts) + // unevaluated = opts.unevaluated + // if (shouldPrintDebug) { + // console.log('propObject', propObject) + // } + // } catch (err) { + // console.log('Cant parse style object - this is ok, just de-opt', name, '>', localViewName) + // if (shouldPrintDebug) { + // console.log('err', err) + // } + // return arg + // } + + // // if no error, set local view info + // if (localViewName) { + // // extends one of our optimizable views + // if (views[localViewName]) { + // views[name] = options.views[localViewName] + // view = views[name] + // } + // } + + // // uses the base styles if necessary, merges just like gloss does + // const depth = (view?.internal?.depth ?? -1) + 1 + // const { styles, conditionalStyles, defaultProps, internalDefaultProps } = getGlossProps( + // propObject, + // view, + // ) + // if (shouldPrintDebug) { + // console.log('glossCall.arguments parse gloss props', name, styles, conditionalStyles) + // } + + // // then put them all into an array so gloss later can use that + // const out: GlossStaticStyleDescription = { + // className: '', + // } + + // if (defaultProps.className) { + // out.className = defaultProps.className + // delete defaultProps.className + // } + + // for (const ns in styles) { + // const info = StaticUtils.getStyles(styles[ns], depth, ns) + // if (shouldPrintDebug) { + // console.log('got static extract', name, ns, info, styles[ns]) + // } + // if (info) { + // cssMap.set(info.className, { css: info.css, commentTexts: [] }) + // out.className += ` ${info.className}` + // } else { + // if (shouldPrintDebug) { + // console.log('no info', ns, styles) + // } + // } + // } + + // if (conditionalStyles) { + // out.conditionalClassNames = {} + // for (const prop in conditionalStyles) { + // out.conditionalClassNames[prop] = '' + // for (const ns in conditionalStyles[prop]) { + // const val = conditionalStyles[prop][ns] + // const info = StaticUtils.getStyles(val, depth, ns) + // if (info) { + // cssMap.set(info.className, { css: info.css, commentTexts: [] }) + // out.conditionalClassNames[prop] += ` ${info.className}` + // } + // } + // } + // } + + // localStaticViews[name] = { + // staticDesc: out, + // propObject, + // defaultProps, + // parent: view, + // } + + // if (out.className || out.conditionalClassNames) { + // staticStyleDesc = out + // } + + // // keep any non-style props on the glossProps + // let defaultPropsLeave = unevaluated + + // if (internalDefaultProps && Object.keys(internalDefaultProps).length) { + // const objectLeaveProps = literalToAst(internalDefaultProps) as t.ObjectExpression + // defaultPropsLeave = [ + // ...defaultPropsLeave, + // ...objectLeaveProps.properties + // ] + // } + + // // if we have defaultProps or unevaluated non-extracted items, leave them + // if (defaultPropsLeave.length) { + // return t.objectExpression(defaultPropsLeave) + // } + + // return t.nullLiteral() + // } + // return arg + // }) + + // // add it to runtime: gloss(View, null, { ...staticStyleDesc }) + // if (staticStyleDesc) { + // if (glossCall.arguments.length === 1) { + // glossCall.arguments.push(t.nullLiteral()) + // } + // glossCall.arguments.push(literalToAst(staticStyleDesc)) + // } + + // if (Object.keys(restDefaultProps).length) { + // const argIndex = t.isNullLiteral(glossCall.arguments[0]) ? 0 : 1 + // console.log('restDefaultProps', restDefaultProps) + // glossCall.arguments[argIndex] = literalToAst(restDefaultProps) + // } + // }, + // }) + // } + + // // gloss isn't included anywhere, so let's bail + // if (!glossSrc || !Object.keys(validComponents).length) { + // return { + // ast, + // css: [], + // js: src, + // map: null, + // } + // } + + // // creates an evaluator to get complex values from babel in this path + // function createEvaluator(path: NodePath, sourceFileName: string, defaultOpts?: EvaluateASTNodeOptions) { + // // Generate scope object at this level + // const staticNamespace = getStaticBindingsForScope( + // path.scope, + // sourceFileName, + // // per-file cache of evaluated bindings + // // TODO can be per-module? + // {}, + // options.whitelistStaticModules, + // execFile, + // ) + // const evalContext = vm.createContext(staticNamespace) + // const evalFn = (n: t.Node) => { + // // called when evaluateAstNode encounters a dynamic-looking prop + // // variable + // if (t.isIdentifier(n)) { + // invariant( + // staticNamespace[n.name], + // 'identifier not in staticNamespace', + // ) + // return staticNamespace[n.name] + // } + // return vm.runInContext(`(${generate(n).code})`, evalContext) + // } + // return (n: t.Node, o?: EvaluateASTNodeOptions) => { + // return evaluateAstNode(n, evalFn, { ...defaultOpts, ...o }) + // } + // } + + // /** + // * Step 2: Statically extract from JSX < /> nodes + // */ + // traverse(ast, { + // JSXElement: { + // enter(traversePath: TraversePath) { + // const node = traversePath.node.openingElement + // if ( + // // skip non-identifier opening elements (member expressions, etc.) + // !t.isJSXIdentifier(node.name) || + // // skip non-gloss components + // !validComponents[node.name.name] + // ) { + // return + // } + + // // Remember the source component + // const originalNodeName = node.name.name + // const localView = localStaticViews[originalNodeName] + // let view = views[originalNodeName] + // // for parentView config + // let extraDepth = 0 + // let domNode = 'div' + + // let staticStyleConfig: GlossView['staticStyleConfig'] | null = null + // if (view) { + // staticStyleConfig = view.staticStyleConfig + // // lets us have plain functional views like Stack + // if (staticStyleConfig.parentView) { + // view = staticStyleConfig.parentView + // extraDepth = 1 + // } + // domNode = view.staticStyleConfig?.tagName + // ?? view.internal?.glossProps.defaultProps.tagName + // ?? 'div' + // } + + // // Get valid css props + // const cssAttributes = staticStyleConfig?.cssAttributes || validCSSAttr + + // function isCSSAttribute(name: string) { + // if (cssAttributes[name]) return true + // if (mediaQueryPrefixes.some(x => name.indexOf(x) === 0)) return true + // return false + // } + + // const attemptEval = evaluateAstNode //createEvaluator(traversePath as any, sourceFileName, { evaluateFunctions: false }) + + // let lastSpreadIndex: number = -1 + // const flattenedAttributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [] + // node.attributes.forEach(attr => { + // if (t.isJSXSpreadAttribute(attr)) { + // try { + // const spreadValue = attemptEval(attr.argument) + + // if (typeof spreadValue !== 'object' || spreadValue == null) { + // lastSpreadIndex = flattenedAttributes.push(attr) - 1 + // } else { + // for (const k in spreadValue) { + // const value = spreadValue[k] + + // if (typeof value === 'number') { + // flattenedAttributes.push( + // t.jsxAttribute( + // t.jsxIdentifier(k), + // t.jsxExpressionContainer(t.numericLiteral(value)), + // ), + // ) + // } else if (value === null) { + // // why would you ever do this + // flattenedAttributes.push( + // t.jsxAttribute(t.jsxIdentifier(k), t.jsxExpressionContainer(t.nullLiteral())), + // ) + // } else { + // // toString anything else + // // TODO: is this a bad idea + // flattenedAttributes.push( + // t.jsxAttribute( + // t.jsxIdentifier(k), + // t.jsxExpressionContainer(t.stringLiteral('' + value)), + // ), + // ) + // } + // } + // } + // } catch (e) { + // lastSpreadIndex = flattenedAttributes.push(attr) - 1 + // } + // } else { + // flattenedAttributes.push(attr) + // } + // }) + + // node.attributes = flattenedAttributes + + // const staticAttributes: Record = {} + // const htmlExtractedAttributes = {} + // let inlinePropCount = 0 + // const staticTernaries: Ternary[] = [] + // const classNameObjects: (t.StringLiteral | t.Expression)[] = [] + + // let shouldDeopt = false + + // const ogAttributes = node.attributes + // node.attributes = node.attributes.filter((attribute, idx) => { + // if ( + // t.isJSXSpreadAttribute(attribute) || + // // keep the weirdos + // !attribute.name || + // // filter out JSXIdentifiers + // typeof attribute.name.name !== 'string' || + // // haven't hit the last spread operator + // idx < lastSpreadIndex + // ) { + // if (shouldPrintDebug) console.log('inline prop via non normal attr') + // inlinePropCount++ + // return true + // } + + // let name = attribute.name.name + + // // for fully deoptimizing certain keys + // if (staticStyleConfig) { + // if (staticStyleConfig.deoptProps?.includes(name)) { + // shouldDeopt = true + // return true + // } + // // for avoiding processing certain keys + // if (staticStyleConfig.avoidProps?.includes(name)) { + // if (shouldPrintDebug) console.log('inline prop via avoidProps') + // inlinePropCount++ + // return true + // } + // } + + // let value: any = t.isJSXExpressionContainer(attribute?.value) + // ? attribute.value.expression + // : attribute.value + + // // boolean prop / conditionals + // const allConditionalClassNames = { + // ...view?.internal?.compiledInfo?.conditionalClassNames ?? null, + // ...localView?.staticDesc?.conditionalClassNames ?? null, + // } + // if (allConditionalClassNames[name]) { + // // we can just extract to className + // if (value === null) { + // // extract but still put it onto staticAttributes for themeFn to use + // staticAttributes[name] = true + // return false + // } + + // // if dynamic value we just use it on className + // classNameObjects.push( + // t.conditionalExpression( + // value, + // t.stringLiteral(allConditionalClassNames[name]), + // t.stringLiteral('') + // ) + // ) + // return false + // } + + // // boolean props have null value + // if (!value) { + // inlinePropCount++ + // return true + // } + + // // if one or more spread operators are present and we haven't hit the last one yet, the prop stays inline + // if (lastSpreadIndex > -1 && idx <= lastSpreadIndex) { + // inlinePropCount++ + // return true + // } + // // pass ref, key, and style props through untouched + // if (UNTOUCHED_PROPS[name]) { + // return true + // } + + // if (name === 'ref') { + // inlinePropCount++ + // return true + // } + + // const trackState = viewInformation[originalNodeName]?.trackState + // if (trackState) { + // if (trackState?.nonCSSVariables?.has(name)) { + // if (shouldPrintDebug) console.log('inline prop via nonCSSVariables') + // inlinePropCount++ + // return true + // } + // } + + // if (!isCSSAttribute(name)) { + // // we can safely leave html attributes + // // TODO make this more customizable / per-tagname + // if (htmlAttributes[name]) { + // try { + // htmlExtractedAttributes[name] = attemptEval(value) + // } catch(err) { + // // oo fancy! this basically says if we can't eval this safely, and its used by the themeFn + // // then we need to deopt here. if it can be evaluated, we're good, we'll run theme here later + // if (trackState?.usedProps?.has(name)) { + // if (shouldPrintDebug) { + // console.log('we use this in this component', name) + // } + // inlinePropCount++ + // return true + // } + // // console.log('err getting html attr', name, err.message) + // // ok + // } + // return true + // } + // if (shouldPrintDebug) console.log('inline prop via !isCSSAttribute') + // inlinePropCount++ + // return true + // } + + // // allow statically defining a change from one prop to another (see Stack) + // if (typeof cssAttributes[name] === 'object') { + // if (t.isStringLiteral(value)) { + // const definition = cssAttributes[name] + // name = definition.name + // value = definition.value[value.value] + // staticAttributes[name] = value + // return false + // } else { + // console.log('couldnt parse a user defined cssAttribute', name, value) + // inlinePropCount++ + // return true + // } + // } + + // // if value can be evaluated, extract it and filter it out + // try { + // staticAttributes[name] = attemptEval(value) + // return false + // } catch { + // // ok + // } + + // if (t.isConditionalExpression(value)) { + // // if both sides of the ternary can be evaluated, extract them + // try { + // const consequent = attemptEval(value.consequent) + // const alternate = attemptEval(value.alternate) + // staticTernaries.push({ + // alternate, + // consequent, + // name, + // test: value.test, + // }) + // // mark the prop as extracted + // staticAttributes[name] = null + // return false + // } catch (e) { + // // + // } + // } else if (t.isLogicalExpression(value)) { + // // convert a simple logical expression to a ternary with a null alternate + // if (value.operator === '&&') { + // try { + // const consequent = attemptEval(value.right) + // staticTernaries.push({ + // alternate: null, + // consequent, + // name, + // test: value.left, + // }) + // staticAttributes[name] = null + // return false + // } catch (e) { + // // + // } + // } + // } + + // // if we've made it this far, the prop stays inline + // if (shouldPrintDebug) console.log('inline prop via no match') + // inlinePropCount++ + // return true + // }) + + // if (shouldPrintDebug) { + // console.log(`ok we parsed a JSX: + // name: ${node.name.name} + // inlinePropCount: ${inlinePropCount} + // shouldDeopt: ${shouldDeopt} + // domNode: ${domNode} + // `) + // } + + // if (shouldDeopt) { + // node.attributes = ogAttributes + // return + // } + + // let classNamePropValue: t.Expression | null = null + + // const extractedStaticAttrs = Object.keys(staticAttributes).length > 0 + // const classNamePropIndex = node.attributes.findIndex( + // attr => !t.isJSXSpreadAttribute(attr) && attr.name && attr.name.name === 'className', + // ) + // if (classNamePropIndex > -1) { + // classNamePropValue = getPropValueFromAttributes('className', node.attributes) + // node.attributes.splice(classNamePropIndex, 1) + // } + + // // used later to generate classname for item + // const stylesByClassName: { [key: string]: string } = {} + + // const depth = (view?.internal?.depth ?? 1) + (localView?.parent?.internal?.depth ?? 1) + extraDepth + // const addStyles = (styleObj: any) => { + // const allStyles = StaticUtils.getAllStyles(styleObj, depth) + // for (const info of allStyles) { + // if (info.css) { + // if (shouldPrintDebug) { + // console.log('add static styles', info.className, info.css) + // } + // stylesByClassName[info.className] = info.css + // } + // } + // } + + // // capture views where they set it afterwards + // // plus any defaults passed through gloss + // const viewDefaultProps = { + // ...view?.defaultProps, + // ...view?.internal?.glossProps?.defaultProps, + // } + + // if (extractedStaticAttrs) { + // const staticStyleProps = { + // ...viewDefaultProps, + // ...localView?.propObject, + // ...htmlExtractedAttributes, + // ...staticAttributes, + // } + // if (shouldPrintDebug) { + // // ignoreAttrs is usually huge + // const { ignoreAttrs, ...rest } = staticStyleProps + // console.log('adding static style props', rest) + // } + // addStyles(staticStyleProps) + // } + + // // if all style props have been extracted, gloss component can be + // // converted to a div or the specified component + // if (inlinePropCount === 0) { + // // add in any local static classes + // if (localView) { + // stylesByClassName[localView.staticDesc.className] = null + // } + + // const themeFns = view?.internal?.getConfig()?.themeFns + // if (themeFns) { + // // TODO we need to determine if this theme should deopt using the same proxy/tracker as gloss + // try { + // const props = { + // ...viewDefaultProps, + // ...localView?.propObject, + // ...htmlExtractedAttributes, + // ...staticAttributes, + // } + // const extracted = StaticUtils.getThemeStyles(view, options.defaultTheme, props).themeStyles + // if (shouldPrintDebug) { + // delete props['ignoreAttrs'] // ignore this its huge in debug output + // console.log('extracting from theme', !!localView, props, extracted) + // } + // for (const x of extracted) { + // stylesByClassName[x.className] = x.css + // } + // } catch(err) { + // console.log('error running theme', sourceFileName, err.message) + // return + // } + // } + + // // add any default html props to tag + // for (const key in viewDefaultProps) { + // const val = viewDefaultProps[key] + // if (key === 'className') { + // classNameObjects.push(t.stringLiteral(val)) + // continue + // } + // if (htmlAttributes[key]) { + // // @ts-ignore + // if (!node.attributes.some(x => x?.name?.name === key)) { + // // add to start so if its spread onto later its overwritten + // node.attributes.unshift( + // t.jsxAttribute( + // t.jsxIdentifier(key), + // t.jsxExpressionContainer(literalToAst(val)) + // ) + // ) + // } + // } + // } + + // // add a data-is="Name" so we can debug it more easily + // node.attributes.push( + // t.jsxAttribute( + // t.jsxIdentifier('data-is'), + // t.stringLiteral(node.name.name) + // ) + // ) + + // if (localView) { + // node.name.name = domNode + // } + + // // if they set a staticStyleConfig.parentView (see Stack) + // if (!isGlossView(view)) { + // if (view?.staticStyleConfig.parentView) { + // view = view.staticStyleConfig.parentView + // } else { + // node.name.name = domNode + // } + // } + + // // if gloss view we may be able to optimize + // if (isGlossView(view)) { + // // local views we already parsed the css out + // const localView = localStaticViews[node.name.name] + // if (localView) { + // // + // } else { + // const { staticClasses } = view.internal.glossProps + // // internal classes + // for (const className of staticClasses) { + // const item = tracker.get(className.slice(2)) + // const css = `${item.selector} { ${item.style} }` + // stylesByClassName[className] = css + // } + // } + // node.name.name = domNode + // } + + // } else { + // if (lastSpreadIndex > -1) { + // // if only some style props were extracted AND additional props are spread onto the component, + // // add the props back with null values to prevent spread props from incorrectly overwriting the extracted prop value + // Object.keys(staticAttributes).forEach(attr => { + // node.attributes.push( + // t.jsxAttribute(t.jsxIdentifier(attr), t.jsxExpressionContainer(t.nullLiteral())), + // ) + // }) + // } + // } + + // if (traversePath.node.closingElement) { + // // this seems strange + // if (t.isJSXMemberExpression(traversePath.node.closingElement.name)) return + // traversePath.node.closingElement.name.name = node.name.name + // } + + // if (shouldPrintDebug) { + // console.log('stylesByClassName pre ternaries', stylesByClassName) + // } + + // const extractedStyleClassNames = Object.keys(stylesByClassName).join(' ') + + // if (classNamePropValue) { + // try { + // const evaluatedValue = attemptEval(classNamePropValue) + // classNameObjects.push(t.stringLiteral(evaluatedValue)) + // } catch (e) { + // classNameObjects.push(classNamePropValue) + // } + // } + + // if (staticTernaries.length > 0) { + // const ternaryObj = extractStaticTernaries(staticTernaries, cacheObject) + // if (shouldPrintDebug) { + // console.log('staticTernaries', staticTernaries, '\nternaryObj', ternaryObj) + // } + // // ternaryObj is null if all of the extracted ternaries have falsey consequents and alternates + // if (ternaryObj !== null) { + // // add extracted styles by className to existing object + // Object.assign(stylesByClassName, ternaryObj.stylesByClassName) + // classNameObjects.push(ternaryObj.ternaryExpression) + // } + // } + + // if (extractedStyleClassNames) { + // classNameObjects.push(t.stringLiteral(extractedStyleClassNames)) + // if (shouldPrintDebug) { + // console.log('extractedStyleClassNames', extractedStyleClassNames) + // } + // } + + // const classNamePropValueForReals = classNameObjects.reduce( + // (acc, val) => { + // if (acc == null) { + // if ( + // // pass conditional expressions through + // t.isConditionalExpression(val) || + // // pass non-null literals through + // t.isStringLiteral(val) || + // t.isNumericLiteral(val) + // ) { + // return val + // } + // return t.logicalExpression('||', val, t.stringLiteral('')) + // } + + // let inner: t.Expression + // if (t.isStringLiteral(val)) { + // if (t.isStringLiteral(acc)) { + // // join adjacent string literals + // return t.stringLiteral(`${acc.value} ${val.value}`) + // } + // inner = t.stringLiteral(` ${val.value}`) + // } else if (t.isLiteral(val)) { + // inner = t.binaryExpression('+', t.stringLiteral(' '), val) + // } else if (t.isConditionalExpression(val) || t.isBinaryExpression(val)) { + // if (t.isStringLiteral(acc)) { + // return t.binaryExpression('+', t.stringLiteral(`${acc.value} `), val) + // } + // inner = t.binaryExpression('+', t.stringLiteral(' '), val) + // } else if (t.isIdentifier(val) || t.isMemberExpression(val)) { + // // identifiers and member expressions make for reasonable ternaries + // inner = t.conditionalExpression( + // val, + // t.binaryExpression('+', t.stringLiteral(' '), val), + // t.stringLiteral(''), + // ) + // } else { + // if (t.isStringLiteral(acc)) { + // return t.binaryExpression( + // '+', + // t.stringLiteral(`${acc.value} `), + // t.logicalExpression('||', val, t.stringLiteral('')), + // ) + // } + // // use a logical expression for more complex prop values + // inner = t.binaryExpression( + // '+', + // t.stringLiteral(' '), + // t.logicalExpression('||', val, t.stringLiteral('')), + // ) + // } + // return t.binaryExpression('+', acc, inner) + // }, + // null, + // ) + + // if (shouldPrintDebug) { + // console.log('classNamePropValueForReals', classNamePropValueForReals) + // } + + // if (classNamePropValueForReals) { + // if (t.isStringLiteral(classNamePropValueForReals)) { + // node.attributes.push( + // t.jsxAttribute( + // t.jsxIdentifier('className'), + // t.stringLiteral(classNamePropValueForReals.value), + // ), + // ) + // } else { + // node.attributes.push( + // t.jsxAttribute( + // t.jsxIdentifier('className'), + // t.jsxExpressionContainer(classNamePropValueForReals), + // ), + // ) + // } + // } + + // const lineNumbers = + // node.loc && + // node.loc.start.line + + // (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : '') + + // const comment = util.format( + // '/* %s:%s (%s) */', + // sourceFileName.replace(process.cwd(), '.'), + // lineNumbers, + // originalNodeName, + // ) + + // for (const className in stylesByClassName) { + // if (cssMap.has(className)) { + // if (comment) { + // const val = cssMap.get(className)! + // val.commentTexts.push(comment) + // cssMap.set(className, val) + // } + // } else { + // const css = stylesByClassName[className] + // if (css) { + // if (typeof css !== 'string') { + // throw new Error(`CSS is not a string, for ${className}: ${JSON.stringify(stylesByClassName, null, 2)}, ${typeof css}`) + // } + // cssMap.set(className, { css, commentTexts: [comment] }) + // } + // } + // } + // }, + // exit(traversePath: TraversePath) { + // if (traversePath._complexComponentProp) { + // if (t.isJSXElement(traversePath.parentPath)) { + // // bump + // traversePath.parentPath._complexComponentProp = [].concat( + // traversePath.parentPath._complexComponentProp || [], + // traversePath._complexComponentProp, + // ) + // } else { + // // find nearest Statement + // let statementPath = traversePath + // do { + // statementPath = statementPath.parentPath + // } while (!t.isStatement(statementPath)) + + // invariant(t.isStatement(statementPath), 'Could not find a statement') + + // const decs = t.variableDeclaration('var', [].concat(traversePath._complexComponentProp)) + + // statementPath.insertBefore(decs) + // } + // traversePath._complexComponentProp = null + // } + // }, + // }, + // }) + + // const css: CSSExtracted[] = [] + + // // Write out CSS using it's className, this gives us de-duping for shared classnames + // for (const [className, entry] of cssMap.entries()) { + // const content = `${entry.commentTexts.map(txt => `${txt}\n`).join('')}${entry.css}` + // const name = `${className}__gloss.css` + // const importPath = `${outRelPath}/${name}` + // const filename = path.join(outPath, name) + // // append require/import statement to the document + // if (content !== '') { + // css.push({ filename, content }) + // if (useImportSyntax) { + // ast.program.body.unshift(t.importDeclaration([], t.stringLiteral(importPath))) + // } else { + // ast.program.body.unshift( + // t.expressionStatement( + // t.callExpression(t.identifier('require'), [t.stringLiteral(importPath)]), + // ), + // ) + // } + // } + // } + + // const result = generate( + // ast, + // { + // compact: 'auto', + // concise: false, + // filename: sourceFileName, + // // @ts-ignore + // quotes: 'single', + // retainLines: false, + // sourceFileName, + // sourceMaps: true, + // }, + // src, + // ) + + // if (shouldPrintDebug) { + // console.log('output >> ', result.code) + // console.log('css output >>', css) + // } + + // return { + // ast, + // css, + // js: result.code, + // map: result.map, + // } } const execCache = {} @@ -1070,10 +1060,10 @@ const execFile = (file: string) => { // omg this fixed it... ['@babel/plugin-transform-typescript', { isTSX: true }], '@babel/plugin-transform-react-jsx', - ] + ], }).code const exported = { - exports: {} + exports: {}, } vm.runInContext(out, vm.createContext(exported)) const res = exported.exports diff --git a/packages/gloss/jest.config.js b/packages/gloss/jest.config.js index 1aad2324ab..a2aeba692e 100644 --- a/packages/gloss/jest.config.js +++ b/packages/gloss/jest.config.js @@ -3,4 +3,5 @@ const base = require('../../jest.config.base.js') module.exports = { ...base, roots: [''], + testEnvironment: 'jsdom', } diff --git a/packages/gloss/package.json b/packages/gloss/package.json index 311a9f689b..381c87fef1 100644 --- a/packages/gloss/package.json +++ b/packages/gloss/package.json @@ -1,7 +1,7 @@ { "name": "gloss", "version": "2.6.0", - "description": "Higher order component for nicer component styles", + "description": "Dynamic style system", "main": "_/index.js", "ts:main": "src/index.ts", "sideEffects": [ @@ -25,12 +25,7 @@ }, "devDependencies": { "@babel/generator": "^7.4.4", - "cosmiconfig": "^5.2.1", - "dedent": "0.7.0", - "enhanced-resolve": "^4.1.0", - "loader-utils": "^1.2.3", - "mkdirp": "^0.5.1", - "normalize-path": "^3.0.0" + "enhanced-resolve": "^4.1.0" }, "peerDependencies": { "react": "^16.9.0", diff --git a/packages/gloss/src/StaticUtils.tsx b/packages/gloss/src/StaticUtils.tsx new file mode 100644 index 0000000000..d6fe2ff9c3 --- /dev/null +++ b/packages/gloss/src/StaticUtils.tsx @@ -0,0 +1,102 @@ +import { addRules, getSortedNamespaces, getStylesFromThemeFns, GlossView, mergeStyles } from './gloss' +import { compileThemes } from './helpers/compileThemes' +import { CompiledTheme } from './theme/createTheme' +import { createThemeProxy } from './theme/createThemeProxy' +import { ThemeTrackState } from './theme/useTheme' + +/** + * START external static style block (TODO move out to own thing) + * + * keeping it here for now because dont want to add more fns in sensitive loops above + * this is a really hacky area right now as im just trying to figure out the right way + * to do all of this, once it settles into something working we can set up some tests, + * some performance checks, and then hopefully dedupe this code with the code above + + * split it out and make it all a lot more clearly named/structured. + */ + + export type StaticStyleDesc = { + css: string + className: string + ns: string +} + +function getAllStyles(props: any, _depth = 0, ns = '.') { + if (!props) { + return [] + } + const allStyles = { [ns]: {} } + mergeStyles(ns, allStyles, props) + const styles: StaticStyleDesc[] = [] + const namespaces = getSortedNamespaces(allStyles) + for (const ns of namespaces) { + const styleObj = allStyles[ns] + if (!styleObj) + continue + const info = addRules('', styleObj, ns, false) + if (info) { + // @ts-ignore + styles.push({ ns, ...info, }) + } + } + return styles +} + +/** + * For use externally only (static style extract) + */ +function getStyles(props: any, depth = 0, ns = '.') { + return getAllStyles(props, depth, ns)[0] ?? null +} + +/** + * For use externally only (static style extract) + * see addDynamicStyles equivalent + */ +export type ThemeStyleInfo = { + trackState: ThemeTrackState | null + themeStyles: StaticStyleDesc[] | null +} + +function getThemeStyles(view: GlossView, userTheme: CompiledTheme, props: any, _extraDepth = 0): ThemeStyleInfo { + const themeFns = compileThemes(view) + if (!themeFns) { + return { + themeStyles: null, + trackState: null, + } + } + const trackState: ThemeTrackState = { + theme: userTheme, + hasUsedOnlyCSSVariables: true, + nonCSSVariables: new Set(), + usedProps: new Set() + } + // themes always one above, extraDepth if theres a local view + // const depth = view.internal.depth + extraDepth + const themeStyles: StaticStyleDesc[] = [] + const len = themeFns.length - 1 + const theme = createThemeProxy(userTheme, trackState, props) + for (const [index, themeFnList] of themeFns.entries()) { + // const themeDepth = depth - (len - index) + const styles = getStylesFromThemeFns(themeFnList, theme) + if (Object.keys(styles).length) { + // make an object for each level of theme + const curThemeObj = { ['.']: {} } + mergeStyles('.', curThemeObj, styles, true) + const namespaces = getSortedNamespaces(curThemeObj) + for (const ns of namespaces) { + const styleObj = curThemeObj[ns] + if (!styleObj) + continue + const info = addRules('', styleObj, ns, false) + if (info) { + // @ts-ignore + themeStyles.push({ ns, ...info, }) + } + } + } + } + return { themeStyles, trackState } +} + +export const StaticUtils = { getAllStyles, getStyles, getThemeStyles } diff --git a/packages/gloss/src/babel/addDisplayName.ts b/packages/gloss/src/babel/addDisplayName.ts index 37f79fac54..7681c51b1c 100644 --- a/packages/gloss/src/babel/addDisplayName.ts +++ b/packages/gloss/src/babel/addDisplayName.ts @@ -5,9 +5,9 @@ import { isGlossView } from './utils' export function addDisplayName(path, glossFnName: string, references, file, babel) { const { types: t, template } = babel // extra safe because babel or webpack or something can leave behind old ones of these - const buildBuiltInWithConfig = template(` - if (IDENTIFIER) { IDENTIFIER.displayName = "DISPLAY_NAME" } - `) + const buildBuiltInWithConfig = template( + `\nif (IDENTIFIER) IDENTIFIER.displayName = "DISPLAY_NAME";`, + ) for (const reference of references) { const displayName = getDisplayName(reference) diff --git a/packages/gloss/src/babel/index.ts b/packages/gloss/src/babel/index.ts index c71f68dbf7..adefa9b1b0 100644 --- a/packages/gloss/src/babel/index.ts +++ b/packages/gloss/src/babel/index.ts @@ -31,14 +31,17 @@ const glossPlugin = (babel): babel.PluginObj => { } } +const defaultNames = ['gloss'] +const defaultImports = ['gloss', '../gloss'] + function traverseGlossBlocks(babel, state) { const references = new Set() const res: babel.Visitor = { ImportDeclaration(path) { const fileName = path.hub.file.opts.filename // options - const matchNames: string[] = state.opts.matchNames || ['gloss'] - const matchImports: string[] = state.opts.matchImports || ['gloss'] + const matchNames: string[] = state.opts.matchNames || defaultNames + const matchImports: string[] = state.opts.matchImports || defaultImports if (matchImports.indexOf(path.node.source.value) === -1) { return } diff --git a/packages/gloss/src/blocks/Flex.tsx b/packages/gloss/src/blocks/Flex.tsx index b927ea63c6..c2f797696a 100644 --- a/packages/gloss/src/blocks/Flex.tsx +++ b/packages/gloss/src/blocks/Flex.tsx @@ -9,8 +9,8 @@ export type FlexProps = GlossProps export const Flex = gloss(Box, { ignoreAttrs: baseIgnoreAttrs, }).theme( - // css props - propsToStyles, - // for text opacity + // for text opacity alphaColorTheme, + // all css props + propsToStyles, ) diff --git a/packages/gloss/src/gloss.tsx b/packages/gloss/src/gloss.tsx index 18a3741d92..b8e5af4a0b 100644 --- a/packages/gloss/src/gloss.tsx +++ b/packages/gloss/src/gloss.tsx @@ -1,18 +1,19 @@ -import { CSSPropertySet, CSSPropertySetLoose, cssStringWithHash, stringHash, validCSSAttr } from '@o/css' -import React from 'react' +import { CAMEL_TO_SNAKE, cssAttributeAbbreviations, CSSPropertySet, CSSPropertySetLoose, cssValue, SHORTHANDS, SNAKE_TO_CAMEL, stringHash, validCSSAttr } from '@o/css' +import React, { useMemo } from 'react' import { createElement, isValidElement, memo, useEffect, useRef } from 'react' import { Config } from './configureGloss' -import { createGlossIsEqual } from './createGlossIsEqual' +import { compileConfig } from './helpers/compileConfig' +import { compileThemes } from './helpers/compileThemes' +import { createGlossIsEqual } from './helpers/createGlossIsEqual' +import { styleKeysSort } from './helpers/styleKeysSort' import { validPropLoose, ValidProps } from './helpers/validProp' -import { styleKeysSort } from './styleKeysSort' import { GarbageCollector, StyleTracker } from './stylesheet/gc' import { StyleSheet } from './stylesheet/sheet' import { CompiledTheme } from './theme/createTheme' -import { createThemeProxy } from './theme/createThemeProxy' import { pseudoProps } from './theme/pseudos' import { themeVariableManager } from './theme/ThemeVariableManager' -import { ThemeTrackState, UnwrapThemeSymbol, useTheme } from './theme/useTheme' +import { UnwrapThemeSymbol, useTheme } from './theme/useTheme' import { defaultTheme } from './themes/defaultTheme' import { GlossProps, GlossPropsPartial, GlossThemeProps, GlossViewConfig } from './types' @@ -58,7 +59,6 @@ export interface ThemeFn { } type GlossInternals = { - depth: number parent: any targetElement: any themeFns: ThemeFn[] | null @@ -70,13 +70,14 @@ type GlossInternals = { } } +type ClassNamesByNs = ({ [key: string]: ClassNames } | null) + type GlossParsedProps = { - staticClasses: string[] | null + statics: ClassNamesByNs[] config: GlossViewConfig | null defaultProps: Partial | null internalDefaultProps: any styles: Object | null - conditionalStyles: Object | null } export type GlossStaticStyleDescription = { @@ -88,7 +89,7 @@ export type GlossStaticStyleDescription = { const GlossComponentSymbol = Symbol('__GLOSS_SIMPLE_COMPONENT__') as any export const tracker: StyleTracker = new Map() -export const sheet = new StyleSheet(true) +export const sheet = new StyleSheet(false) const gc = new GarbageCollector(sheet, tracker) // helpful global to let us add debugging in dev mode anywhere in here @@ -139,27 +140,23 @@ export function gloss< // in this case assume were 1 depth because it accepts gloss className typeof target !== 'string' ? 1 : 0 + const targetElement = hasGlossyParent ? target.internal.targetElement : target - const glossProps = getGlossProps(glossPropsObject ?? null, hasGlossyParent ? target : null, depth) + const glossProps = getGlossProps(glossPropsObject ?? null, hasGlossyParent ? target : null) const config = glossProps.config - const getEl = config?.getElement - const staticClassNames = glossProps.staticClasses?.join(' ') ?? '' + + // calc before render const ignoreAttrs = glossProps.defaultProps?.ignoreAttrs ?? (hasGlossyParent && target.ignoreAttrs) ?? baseIgnoreAttrs - // static compilation information - const { compiledClassName, conditionalClassNames } = getCompiledClasses(target, compiledInfo || null, depth) + const getEl = config?.getElement + // static compilation information // just add conditional classnames right away, they are small - for (const key in glossProps.conditionalStyles) { - const names = addStyles(glossProps.conditionalStyles[key], depth + 1) - if (names) { - conditionalClassNames[key] = names.join(' ') - } - } + const { compiledClassName, conditionalClassNames } = getCompiledClasses(target, compiledInfo || null, depth) // put the "rest" of non-styles onto defaultProps GlossView.defaultProps = glossProps.defaultProps - let themeFns: ThemeFn[][] | null = null + let themeFns: ThemeFn[][] = [] let hasCompiled = false let shouldUpdateMap: WeakMap @@ -169,13 +166,14 @@ export function gloss< hasCompiled = true shouldUpdateMap = GlossView['shouldUpdateMap'] themeFns = compileThemes(ThemedView) + GlossView['displayName'] = ThemedView.displayName } setTimeout(compile, 0) // debug if (isDeveloping && glossPropsObject?.['debug']) { - console.warn('gloss info', { glossProps, depth }) + console.warn('gloss info', { glossProps }) } /** @@ -203,10 +201,7 @@ export function gloss< const last = useRef<{ props: Object; theme: CompiledTheme }>() let shouldAvoidStyleUpdate = false if (!last.current) { - last.current = { - props, - theme, - } + last.current = { props, theme } } else { // ensure update on theme change if (last.current.theme !== theme) { @@ -244,39 +239,13 @@ export function gloss< // set up final props with filtering for various attributes let finalProps: any = {} - let avoidStyles = false - if (config?.shouldAvoidProcessingStyles) { - avoidStyles = config.shouldAvoidProcessingStyles(props) - } - - const dynStyles = addDynamicStyles( - ThemedView.displayName, - dynClasses.current, - depth, - theme as any, - themeFns, - avoidStyles, - ) - dynClasses.current = curDynClassNames + const dynClassNames = [] + dynClasses.current = dynClassNames const isDOMElement = typeof element === 'string' || (config ? config.isDOMElement : false) - let className = staticClassNames - if (props.className) { - className += ` ${props.className}` - } - if (curDynClassNames.length) { - className += ' ' + curDynClassNames.join(' ') - } - if (compiledClassName) { - className += compiledClassName - } - for (const key in props) { - if (props[key] === true && conditionalClassNames[key]) { - className += ` ${conditionalClassNames[key]} ` - continue - } + if (conditionalClassNames[key]) continue if (isDOMElement) { if (ignoreAttrs[key]) continue // TODO: need to figure out this use case: when a valid prop attr, but invalid val @@ -293,7 +262,13 @@ export function gloss< } } - finalProps.className = className + const classNames = getClassNames(theme, themeFns, glossProps.statics, props['debug']) + if (shouldDebug) { + console.log('classNames', classNames) + } + finalProps.className = Object.values(classNames).join(' ') + ( + typeof props.className === 'string' ? ' ' + props.className : '' + ) if (isDeveloping) { finalProps['data-is'] = finalProps['data-is'] || ThemedView.displayName @@ -302,19 +277,13 @@ export function gloss< // hook: setting your own props const postProcessProps = config && config.postProcessProps if (postProcessProps) { - // TODO could hoist this cb fn - postProcessProps(props, finalProps, () => { - return { - ...glossProps.styles?.['.'], - ...dynStyles['.'], - } - }) + postProcessProps(props, finalProps, () => getStylesForClassNames(classNames)) } if (isDeveloping && shouldDebug) { const styles = finalProps.className .split(' ') - .map(x => tracker.get(x.slice(2))) + .map(x => tracker.get(x)) .filter(Boolean) console.log('styles\n', styles, '\nprops\n', props, '\noutProps\n', finalProps) shouldDebug = false @@ -334,7 +303,6 @@ export function gloss< themeFns: null, parent, targetElement, - depth, getConfig: () => ({ displayName: ThemedView.displayName || '', themeFns, @@ -359,6 +327,21 @@ export function gloss< return ThemedView as any } +const CNCache = new WeakMap() +const getStylesForClassNames = (classNames: ClassNames): Object => { + if (CNCache.has(classNames)) return CNCache.get(classNames) + const style = {} + for (let key in classNames) { + const cn = classNames[key] + key = SNAKE_TO_CAMEL[key] + if (typeof cn === 'string') { + style[key] = tracker.get(cn)?.value + } + } + CNCache.set(classNames, style) + return style +} + function createGlossView(GlossView, config) { const { isEqual, shouldUpdateMap } = createGlossIsEqual() // @ts-ignore @@ -374,43 +357,8 @@ function createGlossView(GlossView, config) { return res } -// takes a style object, adds it to stylesheet, returns classnames -function addStyles( - styles: any, - depth: number, - displayName?: string, - prevClassNames?: string[] | null, - selectorPrefix?: string -) { - const namespaces = getSortedNamespaces(styles) - let classNames: string[] | null = null - for (const ns of namespaces) { - const style = styles[ns] - // they may return falsy, conditional '&:hover': active ? hoverStyle : null - if (!style) continue - - // add the stylesheets and classNames - // TODO could do a simple "diff" so that fast-changing styles only change the "changing" props - // it would likely help things like when you animate based on mousemove, may be slower in default case - const className = addRules(displayName, style, ns, depth || 0, selectorPrefix, true) - - if (className) { - classNames = classNames || [] - classNames.push(className) - // if this is the first mount render or we didn't previously have this class then add it as new - if (!prevClassNames || !prevClassNames.includes(className)) { - gc.registerClassUse(className.slice(2)) - } - } - } - if (isDeveloping && shouldDebug) { - console.log('addStyles sorted', classNames, namespaces, styles) - } - return classNames -} - // sort pseudos into priority -const getSortedNamespaces = (styles: any) => { +export const getSortedNamespaces = (styles: any) => { const keys = Object.keys(styles) if (keys.length > 1) { keys.sort(styleKeysSort) @@ -432,128 +380,46 @@ function mergePropStyles(styles: Object, propStyles: Object, props: Object) { function deregisterClassName(name: string) { // slice 2 to remove specifity - gc.deregisterClassUse(name.slice(2)) -} - -let curDynClassNames: string[] = [] -function addDynamicStyles( - displayName: string = 'g', - prevClassNames: string[] | null, - depth: number, - props: GlossThemeProps, - themeFns?: ThemeFn[][] | null, - avoidStyles?: boolean, -) { - const dynStyles = {} - curDynClassNames = [] - - if (!avoidStyles) { - if (props && themeFns) { - const len = themeFns.length - 1 - for (const [index, themeFnList] of themeFns.entries()) { - const themeDepth = depth - (len - index) - const themeStyles = getStylesFromThemeFns(themeFnList, props) - // TODO is this bad perf? now that we always create an object for themes - // the next block would always execute, but there are times themes do nothing - // not sure its worth checking keys here but it avoids object creation in the block - // i really wish js added shit like themeStyles.keysLength or something - if (Object.keys(themeStyles).length) { - dynStyles['.'] = dynStyles['.'] || {} - // make an object for each level of theme - const curThemeObj = { ['.']: {} } - mergeStyles('.', curThemeObj, themeStyles, true) - // TODO console.log this see if we can optimize - Object.assign(dynStyles['.'], curThemeObj['.']) - // `html ` prefix makes it slightly stronger than the glossProp styles - const dynClassNames = addStyles(curThemeObj, themeDepth, displayName, prevClassNames, 'html ') - if (dynClassNames) { - for (const cn of dynClassNames) { - curDynClassNames.push(cn) - } - } - } - } - } - } - - // de-register removed classNames - if (prevClassNames) { - for (const className of prevClassNames) { - if (!curDynClassNames.includes(className)) { - deregisterClassName(className) - } - } - } - - return dynStyles + gc.deregisterClassUse(name) } const isSubStyle = (x: string) => x[0] === '&' || x[0] === '@' -// -// this... THIS... -// ... this is a tricky function -// because its used on initial mount AND during renders -// which is actually useful, yes, because you want the logic the same -// BUT its also used nested! See themeFn => mergePropStyles -// likely can be refactored, but just need to study it a bit before you do -// -function mergeStyles( +export function mergeStyles( id: string, - baseStyles: Object, + styles: { [key: string]: any }, nextStyles?: CSSPropertySet | null | void, overwrite?: boolean, rest?: Object ): Object | undefined { if (!nextStyles) return - // this is just for the conditional prop styles - let propStyles for (const key in nextStyles) { // dont overwrite as we go down - if (overwrite !== true && baseStyles[id][key] !== undefined) { + if (overwrite !== true && styles[id] && styles[id][key] !== undefined) { continue } if (key === 'conditional') { - // propStyles - // definition: gloss({ isTall: { height: '100%' } }) - // usage: - for (const pKey in nextStyles[key]) { - let subStyles = nextStyles[key][pKey] - propStyles = propStyles || {} - propStyles[pKey] = {} - // they can nest (media queries/psuedo), split it out, eg: - for (const sKey in subStyles) { - // key = isTall - // sKey = &:before - if (isSubStyle(sKey)) { - // keep all sub-styles on their key - propStyles[pKey] = propStyles[pKey] || {} - propStyles[pKey][sKey] = subStyles[sKey] - } else { - // we put base styles here, see 'base' check above - propStyles[pKey]['.'] = propStyles[pKey]['.'] || {} - propStyles[pKey]['.'][sKey] = subStyles[sKey] - } - } - } + styles.conditional = styles.conditional || {} + styles.conditional = getConditionalStyles(nextStyles[key]) continue } if (validCSSAttr[key]) { // valid regular attr - baseStyles[id][key] = nextStyles[key] + styles[id] = styles[id] || {} + styles[id][key] = nextStyles[key] } else if (isSubStyle(key)) { for (const sKey in nextStyles[key]) { - if (overwrite === true || !baseStyles[key] || baseStyles[key][sKey] === undefined) { - baseStyles[key] = baseStyles[key] || {} - baseStyles[key][sKey] = nextStyles[key][sKey] + if (overwrite === true || !styles[key] || styles[key][sKey] === undefined) { + styles[key] = styles[key] || {} + styles[key][sKey] = nextStyles[key][sKey] } } } else { const pseudoKey = pseudoProps[key] if (pseudoKey) { // merge in case they defined it two different ways - baseStyles[pseudoKey] = baseStyles[pseudoKey] || {} - Object.assign(baseStyles[pseudoKey], nextStyles[key]) + styles[pseudoKey] = styles[pseudoKey] || {} + Object.assign(styles[pseudoKey], nextStyles[key]) continue } @@ -565,8 +431,8 @@ function mergeStyles( const mediaSelector = Config.mediaQueries[mediaName] if (mediaSelector) { const styleKey = key.slice(index + 1) - baseStyles[mediaSelector] = baseStyles[mediaSelector] || {} - baseStyles[mediaSelector][styleKey] = nextStyles[key] + styles[mediaSelector] = styles[mediaSelector] || {} + styles[mediaSelector][styleKey] = nextStyles[key] continue } } @@ -578,21 +444,67 @@ function mergeStyles( } } } +} +// conditional +// const Component = gloss({ conditional: { isTall: { height: '100%' } } }) +// usage: +function getConditionalStyles(conditionalStyles: Object) { + let propStyles + for (const pKey in conditionalStyles) { + const subStyles = conditionalStyles[pKey] + propStyles = propStyles || {} + propStyles[pKey] = {} + // they can nest (media queries/psuedo), split it out, eg: + for (const sKey in subStyles) { + // key = isTall + // sKey = &:before + if (isSubStyle(sKey)) { + // keep all sub-styles on their key + propStyles[pKey] = propStyles[pKey] || {} + propStyles[pKey][sKey] = subStyles[sKey] + } else { + // we put base styles here, see 'base' check above + propStyles[pKey]['.'] = propStyles[pKey]['.'] || {} + propStyles[pKey]['.'][sKey] = subStyles[sKey] + } + } + } return propStyles } +function stylesToClassNamesByNS(stylesByNs: any) { + if (!stylesByNs) return null + const statics: { [key: string]: ClassNames } = {} + for (const ns in stylesByNs) { + const styles = stylesByNs[ns] + if (ns === 'conditional') { + for (const condition in styles) { + const next = stylesToClassNamesByNS(styles[condition]) + if (next) { + statics[condition] = next + } + } + } else { + statics[ns] = addRules('', styles, ns, true) + } + } + return statics +} + // happens once at initial gloss() call, so not as perf intense // get all parent styles and merge them into a big object -// const staticClasses: string[] | null = addStyles(glossProps.styles, depth) -export function getGlossProps(allProps: GlossProps | null, parent: GlossView | null, depth: number): GlossParsedProps { - const { config = null, ...rest } = allProps || {} - const styles = { - '.': {}, - } - // all the "rest" go onto default props +export function getGlossProps(allProps: GlossProps | null, parent: GlossView | null): GlossParsedProps { + const { config = null, ...glossProp } = allProps || {} + // all the "glossProp" go onto default props let defaultProps: any = getGlossDefaultProps(allProps) - let conditionalStyles = mergeStyles('.', styles, rest, false, defaultProps) ?? null + + const styles = {} + mergeStyles('.', styles, glossProp, true, defaultProps) + const hasStyles = Object.keys(styles).length + const staticStyleDesc = hasStyles ? stylesToClassNamesByNS(styles) : null + const statics = [staticStyleDesc, ...(parent?.internal.glossProps.statics ?? [])] + const internalDefaultProps = defaultProps // merge parent config if (parent?.internal) { @@ -603,158 +515,168 @@ export function getGlossProps(allProps: GlossProps | null, parent: GlossView | n ...defaultProps, } } - const parentPropStyles = parentGlossProps.conditionalStyles - if (parentPropStyles) { - for (const key in parentPropStyles) { - conditionalStyles = conditionalStyles || {} - conditionalStyles[key] = conditionalStyles[key] || {} - conditionalStyles[key] = { - ...parentPropStyles[key], - ...conditionalStyles[key], - } - } - } } - // merge together the parent chain of static classes - const curStaticClasses = addStyles(styles, depth) || [] - const parentStaticClasses = parent?.internal?.glossProps.staticClasses || [] + + if (allProps?.['debug']) { + console.log('debug getGlossProps', { styles, staticStyleDesc, statics, defaultProps }) + } + return { - staticClasses: [...parentStaticClasses, ...curStaticClasses], + statics, config: compileConfig(config, parent), styles, - conditionalStyles, defaultProps, internalDefaultProps, } } -// excludes gloss internal props and leaves style/html props -function getGlossDefaultProps(props: any) { - const x = {} - for (const key in props) { - if (key === 'conditional' || key === 'config') continue - if (isSubStyle(key)) continue - x[key] = props[key] - } - return x +type ClassNames = { + [key: string]: string | ClassNames } -/** - * We need to compile a few things to get the config right: - * 1. get all the parents postProcessProps until: - * 2. encounter a parent with getElement (and use that isDOMElement) - * 3. stop there, don't keep going higher - */ -function compileConfig( - config: GlossViewConfig | null, - parent: GlossView | null, -): GlossViewConfig { - const compiledConf: GlossViewConfig = { ...config } - let cur = parent - while (cur?.internal) { - const parentConf = cur.internal.glossProps.config - if (parentConf) { - if (parentConf.postProcessProps) { - // merge the postProcessProps - const og = compiledConf.postProcessProps - if (parentConf.postProcessProps !== og) { - compiledConf.postProcessProps = og - ? (a, b, c) => { - og(a, b, c) - parentConf.postProcessProps!(a, b, c) - } - : parentConf.postProcessProps - } - } - // find the first getElement and break here - if (parentConf.getElement) { - compiledConf.getElement = parentConf.getElement - compiledConf.isDOMElement = parentConf.isDOMElement - break +function getClassNames(props: any, themes: ThemeFn[][], styles: ClassNamesByNs[], shouldDebug: boolean): ClassNames { + const classNames: ClassNames = {} + const depth = themes.length + for (let i = 0; i < depth; i++) { + const themeStyles = !!themes[i]?.length && getStylesFromThemeFns(themes[i], props) + const staticStyles = styles[i] + if (shouldDebug) { + console.log('level', i, { themeStyles, staticStyles }) + } + if (themeStyles) { + for (const key in themeStyles) { + mergeStyle(key, themeStyles[key], classNames) } } - cur = cur.internal.parent - } - return compiledConf -} - -// compile theme from parents -function compileThemes(viewOG: GlossView) { - let cur = viewOG - const hasOwnTheme = cur.internal.themeFns - - // this is a list of a list of theme functions - // we run theme functions from parents before, working down to ours - // the parent ones have a lower priority, so we want them first - const added = new Set() - let all: ThemeFn[][] = [] - const hoisted: ThemeFn[] = [] - - // get themes in order from most important (current) to least important (grandparent) - while (cur) { - const conf = cur.internal - if (conf.themeFns) { - let curThemes: ThemeFn[] = [] - for (const fn of conf.themeFns) { - if (added.has(fn)) { - continue // prevent duplicates in parents + if (staticStyles) { + for (const ns in staticStyles) { + if (ns !== '.') { + // TODO + // console.log('skipping ns for now until figured out', ns) + continue } - added.add(fn) - if (fn.hoistTheme) { - hoisted.push(fn) - } else { - curThemes.push(fn) + const styleClasses = staticStyles[ns] + if (shouldDebug) { + console.log('level', i, 'add', ns, styleClasses, classNames) + } + for (const key in styleClasses) { + if (shouldDebug) { + console.log('key', key, styleClasses[key]) + } + if (!classNames[key]) { + classNames[key] = styleClasses[key] + } } - } - if (curThemes.length) { - all.push(curThemes) } } - cur = conf.parent } + return classNames +} - // reverse so we have [grandparent, parent, cur] - all.reverse() +function mergeStyle(key: string, val: any, classNames: ClassNames, _namespace = '.') { + // check for validity + if (validCSSAttr[key]) { + addStyleRule(key, val, '.', classNames) + return + } + // will be captured next in isSubStyle + if (pseudoProps[key]) { + key = pseudoProps[key] + } + if (isSubStyle(key)) { + classNames[key] = classNames[key] || {} + // console.log('what is', val) + for (const skey in val[key]) { + if (classNames[key][skey]) continue + addStyleRule(skey, val[key][skey], key, classNames[key] as ClassNames) + } + return + } +} - // we need to make sure theme priority is either above or below the static styles, depending. - // so if: - // const Parent = gloss({ background: 'red' }).theme(changeBg) - // changeBg *should* override background - // but if: - // const Child = gloss(Parent, { background: 'green' }) - // background will *always* be green - // ALSO if changeBg.hoistTheme = true, we need to be sure its hoisted all the way up - // by putting an empty theme at the front if there are child themes, but no current theme, - // we ensure that the hoisted will always go at the top, as well as ensuring the depth/priority - // is kept as it should be - if (all.length && !hasOwnTheme) { - all.push([]) +function addStyleRule(key: string, val: string, namespace: string, classNames: ClassNames) { + const finalValue = cssValue(key, val, false, cssOpts) + if (SHORTHANDS[key]) { + for (let k of SHORTHANDS[key]) { + addRule(k, finalValue, namespace, classNames, true) + } + } else { + addRule(key, finalValue, namespace, classNames, true) } +} - // hoisted always go onto starting of the cur theme - if (hoisted.length) { - all[all.length - 1] = [ - ...hoisted, - ...all[all.length - 1] - ] +function addRule(key: string, value: string, namespace: string, classNames: ClassNames, insert: any, displayName?: string) { + const abbrev = cssAttributeAbbreviations[key] + key = CAMEL_TO_SNAKE[key] || key + // already added + if (classNames[key]) { + return + } + const isMediaQuery = namespace[0] === '@' + const style = `${key}:${value};` + const className = abbrev + stringHash(`${value}`) + let selector = `.${className}` + if (namespace[0] === '&' || namespace.indexOf('&') !== -1) { + selector = namespace.split(',').map(part => `.${className} ${part.replace('&', '')}`).join(',') } + const css = isMediaQuery ? `${namespace} {${selector} {${style}}}` : `${selector} {${style}}` + if (className !== undefined) { + classNames[key] = className + if (insert === true) { + // this is the first time we've found this className + if (!tracker.has(className)) { + if (shouldDebug) { + console.log('adding style', displayName) + } + // insert the new style text + tracker.set(className, { + namespace, + selector, + style, + value, + className, + }) + sheet.insert(isMediaQuery ? namespace : selector, css) + } + } + } +} - const themes = all.filter(Boolean) +const getPrefix = (cn: string) => cn.slice(0, cn.indexOf('-')) - if (!themes.length) { - return null +function getUniqueStylesByClassName(classNames: string[]) { + const res: string[] = [] + const usedPrefix = {} + for (const cn of classNames) { + const prefix = getPrefix(cn) + if (usedPrefix[prefix]) continue + usedPrefix[prefix] = true + res.push(cn) } + return res +} - return themes +// excludes gloss internal + style props, leaves html +function getGlossDefaultProps(props: any) { + const x = {} + for (const key in props) { + if (key === 'conditional' || key === 'config') continue + // if (validCSSAttr[key]) continue + // if (isSubStyle(key)) continue + x[key] = props[key] + } + return x } -function getStylesFromThemeFns(themeFns: ThemeFn[], themeProps: Object) { +export function getStylesFromThemeFns(themeFns: ThemeFn[], themeProps: Object) { let styles: CSSPropertySetLoose = {} for (const themeFn of themeFns) { const next = themeFn(themeProps as any, styles) if (next) { styles = styles || {} - Object.assign(styles, next) + for (const key in next) { + styles[key] = styles[key] || next[key] + } } } return styles @@ -768,108 +690,38 @@ const cssOpts = { resolveFunctionValue: val => val(curTheme) } -const nicePostfix = { - '&:hover': 'hover', - '&:active': 'active', - '&:disabled': 'disabled', - '&:focus': 'focus', - '&:focus-within': 'focuswithin', -} - -const parentKeys = {} -const createParentKey = (k: string) => { - const next = '-' + k.replace(/\s+/g, '') - parentKeys[k] = next - return next -} - -function addRules( +// : (A extends true ? string[] : { +// css: string, +// className: string +// }) | null +export function addRules( displayName = '_', rules: BaseRules, namespace: string, - depth: number, - selectorPrefix?: string, insert?: A, -): (A extends true ? string : { - css: string, - className: string -}) | null { - const [hash, style] = cssStringWithHash(rules, cssOpts) - - // empty object, no need to add anything - if (!hash) { - return null - } - - let className = `${hash}` - // build the class name with the display name of the styled component and a unique id based on the css and namespace - // ensure we are unique for unique namespaces - if (namespace !== '.') { - const postfix = nicePostfix[namespace] || stringHash(namespace) - className += `-${postfix}` - } - if (selectorPrefix) { - className += parentKeys[selectorPrefix] || createParentKey(selectorPrefix) - } - - const isMediaQuery = namespace[0] === '@' - const selector = getSelector(className, namespace, selectorPrefix) - const css = isMediaQuery ? `${namespace} {${selector} {${style}}}` : `${selector} {${style}}` - const finalClassName = `g${depth}${className}` - - if (insert === true) { - // this is the first time we've found this className - if (!tracker.has(className)) { - // insert the new style text - tracker.set(className, { - displayName, - namespace, - rules, - selector, - style, - className, - }) - sheet.insert(isMediaQuery ? namespace : selector, css) +): ClassNames { + const classNames: ClassNames = {} + for (let key in rules) { + if (!cssAttributeAbbreviations[key]) { + // console.warn('what the key', key) + continue + } + const val = cssValue(key, rules[key], false, cssOpts) + if (val === undefined) continue + if (SHORTHANDS[key]) { + for (let k of SHORTHANDS[key]) { + addRule(k, val, namespace, classNames, insert, displayName) + } + } else { + addRule(key, val, namespace, classNames, insert, displayName) } - // @ts-ignore - return finalClassName } + return classNames // @ts-ignore - return { css, className: finalClassName } -} - -// has to return a .s-id and .id selector for use in parents passing down styles -function getSelector(className: string, namespace: string, selectorPrefix = '') { - if (namespace[0] === '@') { - // media queries need stronger binding, we'll do html selector - return getSpecificSelectors(className, selectorPrefix + 'body') - } - if (namespace[0] === '&' || namespace.indexOf('&') !== -1) { - // namespace === '&:hover, &:focus, & > div' - const namespacedSelectors = namespace - .split(',') - .flatMap(part => { - return getSpecificSelectors(className, selectorPrefix, part.replace('&', '')) - }) - .join(',') - return namespacedSelectors - } - return getSpecificSelectors(className, selectorPrefix) -} - -// for now, assume now more than 6 levels nesting (css = 🤮) -const depths = [0, 1, 2, 3, 4, 5] -const dSelectors = depths.map(i => i === 0 ? '' : `._g${i}`.repeat(i)) -function getSpecificSelectors(base: string, parent = '', after = '') { - let s: string[] = [] - for (const i of depths) { - s.push(`${parent}${dSelectors[i]} .g${i}${base}${after}`) - } - return s.join(',') + // return { css, className: finalClassName } } - // some internals we can export if (typeof window !== 'undefined') { window['gloss'] = window['gloss'] || { @@ -923,100 +775,3 @@ function getCompiledClasses(parent: GlossView | any, compiledInfo: GlossStaticSt const replaceDepth = (className: string, depth: number) => { return className[0] === 'g' && +className[1] == +className[1] ? `g${depth}${className.slice(2)}` : className } - -/** - * START external static style block (TODO move out to own thing) - * - * keeping it here for now because dont want to add more fns in sensitive loops above - * this is a really hacky area right now as im just trying to figure out the right way - * to do all of this, once it settles into something working we can set up some tests, - * some performance checks, and then hopefully dedupe this code with the code above + - * split it out and make it all a lot more clearly named/structured. - */ - -export type StaticStyleDesc = { - css: string, - className: string; - ns: string -} - -function getAllStyles(props: any, depth = 0, ns = '.') { - if (!props) { - return [] - } - const allStyles = { [ns]: {} } - mergeStyles(ns, allStyles, props) - const styles: StaticStyleDesc[] = [] - const namespaces = getSortedNamespaces(allStyles) - for (const ns of namespaces) { - const styleObj = allStyles[ns] - if (!styleObj) continue - const info = addRules('', styleObj, ns, depth, '', false) - if (info) { - styles.push({ ns, ...info, }) - } - } - return styles -} - -/** - * For use externally only (static style extract) - */ -function getStyles(props: any, depth = 0, ns = '.') { - return getAllStyles(props, depth, ns)[0] ?? null -} - -/** - * For use externally only (static style extract) - * see addDynamicStyles equivalent - */ -export type ThemeStyleInfo = { - trackState: ThemeTrackState | null, - themeStyles: StaticStyleDesc[] | null -} - -function getThemeStyles(view: GlossView, userTheme: CompiledTheme, props: any, extraDepth = 0): ThemeStyleInfo { - const themeFns = compileThemes(view) - if (!themeFns) { - return { - themeStyles: null, - trackState: null, - } - } - const trackState: ThemeTrackState = { - theme: userTheme, - hasUsedOnlyCSSVariables: true, - nonCSSVariables: new Set(), - usedProps: new Set() - } - // themes always one above, extraDepth if theres a local view - const depth = view.internal.depth + 1 + extraDepth - const themeStyles: StaticStyleDesc[] = [] - const len = themeFns.length - 1 - const theme = createThemeProxy(userTheme, trackState, props) - for (const [index, themeFnList] of themeFns.entries()) { - const themeDepth = depth - (len - index) - const styles = getStylesFromThemeFns(themeFnList, theme) - if (Object.keys(styles).length) { - // make an object for each level of theme - const curThemeObj = { ['.']: {} } - mergeStyles('.', curThemeObj, styles, true) - const namespaces = getSortedNamespaces(curThemeObj) - for (const ns of namespaces) { - const styleObj = curThemeObj[ns] - if (!styleObj) continue - const info = addRules('', styleObj, ns, themeDepth, 'html ', false) - if (info) { - themeStyles.push({ ns, ...info, }) - } - } - } - } - return { themeStyles, trackState } -} - -export const StaticUtils = { getAllStyles, getStyles, getThemeStyles } - -/** - * END external static style block - */ diff --git a/packages/gloss/src/helpers/compileConfig.ts b/packages/gloss/src/helpers/compileConfig.ts new file mode 100644 index 0000000000..3e57ec3de9 --- /dev/null +++ b/packages/gloss/src/helpers/compileConfig.ts @@ -0,0 +1,41 @@ +import { GlossView } from '../gloss' +import { GlossViewConfig } from '../types' + +/** + * We need to compile a few things to get the config right: + * 1. get all the parents postProcessProps until: + * 2. encounter a parent with getElement (and use that isDOMElement) + * 3. stop there, don't keep going higher + */ +export function compileConfig( + config: GlossViewConfig | null, + parent: GlossView | null, +): GlossViewConfig { + const compiledConf: GlossViewConfig = { ...config } + let cur = parent + while (cur?.internal) { + const parentConf = cur.internal.glossProps.config + if (parentConf) { + if (parentConf.postProcessProps) { + // merge the postProcessProps + const og = compiledConf.postProcessProps + if (parentConf.postProcessProps !== og) { + compiledConf.postProcessProps = og + ? (a, b, c) => { + og(a, b, c) + parentConf.postProcessProps!(a, b, c) + } + : parentConf.postProcessProps + } + } + // find the first getElement and break here + if (parentConf.getElement) { + compiledConf.getElement = parentConf.getElement + compiledConf.isDOMElement = parentConf.isDOMElement + break + } + } + cur = cur.internal.parent + } + return compiledConf +} diff --git a/packages/gloss/src/helpers/compileThemes.tsx b/packages/gloss/src/helpers/compileThemes.tsx new file mode 100644 index 0000000000..ec9032adf6 --- /dev/null +++ b/packages/gloss/src/helpers/compileThemes.tsx @@ -0,0 +1,44 @@ +import { GlossView, ThemeFn } from '../gloss' + +// compile theme from parents +export function compileThemes(viewOG: GlossView) { + let cur = viewOG + + // this is a list of a list of theme functions + // we run theme functions from parents before, working down to ours + // the parent ones have a lower priority, so we want them first + const added = new Set() + let all: ThemeFn[][] = [] + const hoisted: ThemeFn[] = [] + + // get themes in order from most important (current) to least important (grandparent) + while (cur) { + const curThemes: ThemeFn[] = [] + all.push(curThemes) + const conf = cur.internal + if (conf.themeFns) { + for (const fn of conf.themeFns) { + if (added.has(fn)) { + continue // prevent duplicates in parents + } + added.add(fn) + if (fn.hoistTheme) { + hoisted.push(fn) + } else { + curThemes.push(fn) + } + } + if (curThemes.length) { + all.push(curThemes) + } + } + cur = conf.parent + } + + // hoisted always go onto starting of the cur theme + if (hoisted.length) { + all[0] = [...hoisted, ...all[0]] + } + + return all +} diff --git a/packages/gloss/src/createGlossIsEqual.tsx b/packages/gloss/src/helpers/createGlossIsEqual.tsx similarity index 100% rename from packages/gloss/src/createGlossIsEqual.tsx rename to packages/gloss/src/helpers/createGlossIsEqual.tsx diff --git a/packages/gloss/src/styleKeysSort.tsx b/packages/gloss/src/helpers/styleKeysSort.tsx similarity index 96% rename from packages/gloss/src/styleKeysSort.tsx rename to packages/gloss/src/helpers/styleKeysSort.tsx index 8f401326ef..3a5b01ecb7 100644 --- a/packages/gloss/src/styleKeysSort.tsx +++ b/packages/gloss/src/helpers/styleKeysSort.tsx @@ -1,4 +1,4 @@ -import { Config } from './configureGloss' +import { Config } from '../configureGloss' /** * Sorts styles so the pseudo keys go in their logical override diff --git a/packages/gloss/src/helpers/WeakKeys.ts b/packages/gloss/src/helpers/weakKey.ts similarity index 100% rename from packages/gloss/src/helpers/WeakKeys.ts rename to packages/gloss/src/helpers/weakKey.ts diff --git a/packages/gloss/src/index.ts b/packages/gloss/src/index.ts index dd61d1b919..a2742c25b8 100644 --- a/packages/gloss/src/index.ts +++ b/packages/gloss/src/index.ts @@ -21,6 +21,7 @@ export * from './blocks/InlineBlock' export * from './blocks/InlineFlex' // configureGloss +export { ThemeStyleInfo, StaticUtils } from './StaticUtils' export { GlossThemeProps, GlossProps, diff --git a/packages/gloss/src/stylesheet/gc.ts b/packages/gloss/src/stylesheet/gc.ts index 6a9736edce..a7cd0fefbf 100644 --- a/packages/gloss/src/stylesheet/gc.ts +++ b/packages/gloss/src/stylesheet/gc.ts @@ -15,10 +15,7 @@ export type StyleTracker = Map< { displayName?: string namespace: string - rules: BaseRules - // we can compile it later and use it for speed - // only using in the motion View.tsx stuff for now - styleObject?: Object + value: any selector: string style: string className: string diff --git a/packages/gloss/src/stylesheet/sheet.ts b/packages/gloss/src/stylesheet/sheet.ts index a4936e64e8..26dd3ab930 100644 --- a/packages/gloss/src/stylesheet/sheet.ts +++ b/packages/gloss/src/stylesheet/sheet.ts @@ -51,6 +51,13 @@ export class StyleSheet { } insert(key: string, rule: string) { + // this helps catch a nasty class of bugs where you insert an accidental huge string/object + // that you didn't meant to pass into css, causing big slowdowns + if (process.env.NODE_ENV === 'development') { + if (rule.length > 2000) { + debugger + } + } const tag = this.tag if (!tag) { return diff --git a/packages/gloss/src/theme/selectThemeSubset.ts b/packages/gloss/src/theme/selectThemeSubset.ts index 17daacba70..94c277d698 100644 --- a/packages/gloss/src/theme/selectThemeSubset.ts +++ b/packages/gloss/src/theme/selectThemeSubset.ts @@ -1,6 +1,6 @@ import { ThemeObject } from '@o/css' -import { weakKey } from '../helpers/WeakKeys' +import { weakKey } from '../helpers/weakKey' import { CompiledTheme } from './createTheme' import { ThemeSelect } from './Theme' diff --git a/packages/gloss/src/themes/alphaColorTheme.ts b/packages/gloss/src/themes/alphaColorTheme.ts index 7154b50f68..4627c8a748 100644 --- a/packages/gloss/src/themes/alphaColorTheme.ts +++ b/packages/gloss/src/themes/alphaColorTheme.ts @@ -45,7 +45,6 @@ export const alphaColorTheme: ThemeFn = (props, previous) => { if (color) { if (props.applyThemeColor) { if ( - typeof color !== 'string' && color.originalInput !== 'inherit' && typeof alpha === 'number' ) { diff --git a/packages/gloss/src/types.ts b/packages/gloss/src/types.ts index 92da0ed9b1..13828c4b97 100644 --- a/packages/gloss/src/types.ts +++ b/packages/gloss/src/types.ts @@ -86,7 +86,6 @@ export type GlossBaseProps = { export type GlossViewConfig = { displayName?: string ignoreAttrs?: { [key: string]: boolean } - shouldAvoidProcessingStyles?: (props: Props) => boolean postProcessProps?: (curProps: Props, nextProps: any, getFinalStyles: () => CSSPropertySet) => any getElement?: (props: Props) => any isDOMElement?: boolean diff --git a/packages/gloss/tests/transform.test.ts b/packages/gloss/tests/transform.test.ts deleted file mode 100644 index b35f1c1ff3..0000000000 --- a/packages/gloss/tests/transform.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import dedent from 'dedent' - -import { transform } from '../src/transform' - -it('works', async () => { - const { cssText } = await transform( - dedent` - import { gloss } from 'gloss' - - export const View = gloss({ - background: [0,0,0], - transform: { - y: 10 - } - }) - `, - { - filename: './test.js', - outputFilename: '../.gloss-cache/test.css', - }, - ) - - expect(cssText).toBe(`background:rgb(0,0,0);transform:translateY(10px);`) - // expect(cssText).toMatchSnapshot() -}) diff --git a/packages/gloss/tests/transform.test.tsx b/packages/gloss/tests/transform.test.tsx new file mode 100644 index 0000000000..871ce0efed --- /dev/null +++ b/packages/gloss/tests/transform.test.tsx @@ -0,0 +1,36 @@ +import { cleanup, render } from '@testing-library/react' +import React from 'react' + +import { gloss } from '../_' + +afterEach(cleanup) + +it('outputs proper styles in simple case', () => { + const Box = gloss({ + display: 'flex', + boxSizing: 'border-box', + }) + const LinkRow = gloss(Box, { + flexDirection: 'row', + flex: 1, + alignItems: 'center', + background: 'red', + borderLeftRadius: 100, + }) + + render() + + const node = document.getElementById('test1') + const style = window.getComputedStyle(node) + + // inherited from Box + expect(style.display).toBe('flex') + expect(style.boxSizing).toBe('border-box') + // sets own styles + expect(style.flexDirection).toBe('row') + expect(style.flex).toBe('1') + expect(style.background).toBe('red') + expect(style.alignItems).toBe('center') + // sets shorthand styles + expect(style.borderTopLeftRadius).toBe('100px') +}) diff --git a/packages/gloss/tsconfig.test.json b/packages/gloss/tsconfig.test.json new file mode 100644 index 0000000000..3706663ecf --- /dev/null +++ b/packages/gloss/tsconfig.test.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.base" +} diff --git a/packages/ui/src/Surface.tsx b/packages/ui/src/Surface.tsx index 7a67844d08..c9675ab4ac 100644 --- a/packages/ui/src/Surface.tsx +++ b/packages/ui/src/Surface.tsx @@ -521,7 +521,7 @@ export const Surface = themeable(function Surface(direct: SurfaceProps) { } if (props.debug) { - console.log('ok', showElement, props, surfaceFrameProps) + console.log('debug surface', showElement, props, surfaceFrameProps) } return useBreadcrumbReset( @@ -588,7 +588,7 @@ const SurfaceFrame = gloss(View, { }).theme(pseudoFunctionThemes, pseudoStyleTheme, (props, prev) => { // todo fix types here const marginStyle = marginTheme(props as any) - const { fontSize, lineHeight } = textSizeTheme(props) + const { fontSize, lineHeight } = textSizeTheme(props as any) if (prev && props.chromeless) { delete prev.hoverStyle diff --git a/packages/ui/src/View/View.tsx b/packages/ui/src/View/View.tsx index 895b9315e1..8608ad3694 100644 --- a/packages/ui/src/View/View.tsx +++ b/packages/ui/src/View/View.tsx @@ -22,7 +22,6 @@ export const View = gloss(Flex, { }, config: { - // shouldAvoidProcessingStyles: shouldRenderToMotion, postProcessProps(inProps, outProps, getStyles) { if (shouldRenderToMotion(inProps)) { let style = css(getStyles(), { snakeCase: false }) diff --git a/packages/ui/src/lists/ListItemSimple.tsx b/packages/ui/src/lists/ListItemSimple.tsx index 56f065c470..341a5474d3 100644 --- a/packages/ui/src/lists/ListItemSimple.tsx +++ b/packages/ui/src/lists/ListItemSimple.tsx @@ -273,7 +273,11 @@ const ListItemInner = memo(function ListItemInner(props: ListItemSimpleProps) { )} - {coat ? {childrenElement} : childrenElement} + {coat && childrenElement ? ( + {childrenElement} + ) : ( + childrenElement + )} {hasAfterTitle && ( <> diff --git a/packages/ui/src/text/textSizeTheme.ts b/packages/ui/src/text/textSizeTheme.ts index 1eb8a152eb..450d5403b7 100644 --- a/packages/ui/src/text/textSizeTheme.ts +++ b/packages/ui/src/text/textSizeTheme.ts @@ -11,13 +11,12 @@ export type TextSizeProps = { fontSize?: Size size?: Size scale?: Size - marginTop?: number - marginBottom?: number + marginTop?: any + marginBottom?: any } -export function textSizeTheme(props: TextSizeProps) { +export const textSizeTheme = (props: TextSizeProps) => { const res = getTextSizeTheme(props) - // media query size // TODO this whole loop needs rethinking if (hasMediaQueries) { @@ -37,7 +36,6 @@ export function textSizeTheme(props: TextSizeProps) { } } } - return res } diff --git a/projects/playground/src/TestCSSVariables.tsx b/projects/playground/src/TestCSSVariables.tsx index 641cede999..703820a402 100644 --- a/projects/playground/src/TestCSSVariables.tsx +++ b/projects/playground/src/TestCSSVariables.tsx @@ -11,9 +11,9 @@ export function TestCSSVariables() { - {/* + - */} + diff --git a/projects/playground/src/TestMediaQueries.tsx b/projects/playground/src/TestMediaQueries.tsx new file mode 100644 index 0000000000..85b4838c98 --- /dev/null +++ b/projects/playground/src/TestMediaQueries.tsx @@ -0,0 +1,5 @@ +import { View } from '@o/ui' + +export function TestMediaQueries() { + return +} diff --git a/projects/playground/src/TestUI.tsx b/projects/playground/src/TestUI.tsx index 7d42f452c2..962bd91e6e 100644 --- a/projects/playground/src/TestUI.tsx +++ b/projects/playground/src/TestUI.tsx @@ -1,8 +1,6 @@ -import { Button, CardSimple, Parallax, Stack, Title, View } from '@o/ui' -import _ from 'lodash' import * as React from 'react' -import { TestUIMotion } from './TestUIMotion' +import { TestCSSVariables } from './TestCSSVariables' export function TestUI() { return ( @@ -11,254 +9,10 @@ export function TestUI() { {/* */} {/* */} {/* */} - + {/* */} {/* */} {/* */} - {/* */} + ) } - -export function TestMediaQueries() { - return -} - -const logPass = x => { - console.log(x) - return x -} - -export function TestUIParallax() { - console.log('render me') - - const views = index => ( - <> - {/* - helloooooo {index} - */} - - offset 0.75 speed 0.25 index {index} - - - offset 0 speed 0.25 index {index} - - { - return { - y: geometry - .useParallaxIntersection({ speed: 3, offset: 0.5, clamp: [-1, 1.5] }) - .transform([-1, -0.4, 0, 1, 1.4, 1.5], [-1, -0.1, 0, 0.1, 0.2, 0.8]) - .transform(geometry.transforms.scrollParent), - - opacity: geometry - .useParallaxIntersection({ speed: 3, offset: 0.5, clamp: [-1, 1.5] }) - .transform([-1, -0.4, 0, 1, 1.4, 1.5], [-1, 1, 1, 1, 1, 0.8]), - } - }} - > - Hello {index} - - - offset 0.5 speed 2 - - - ({ - opacity: geometry - .useParallaxIntersection({ speed: 1, offset: 0 }) - .transform([-1, 0.5, 0.5, 1], [0, 1, 1, -1]), - })} - > - opacity in/out - - - ({ - opacity: geometry.useParallaxIntersection(), - })} - > - opacity - - - ({ - opacity: geometry.useParallaxIntersection(), - })} - > - offset 0 speed 1 - - { - return { - y: geometry - .useParallaxIntersection({ - speed: 1, - relativeTo: 'frame', - }) - .transform([-1, -0.2, 0, 0.2, 1], [-3, -0.05, 0, 0.05, 3]) - .transform([-1, -0.2, 0, 0.2, 1], [-3, -0.05, 0, 0.05, 3]) - .transform(x => x * 4), - } - }} - > - Hellow Orld - - - ) - - function getParallax({ speed = 1 } = {}) { - const startClamp = [-1, -0.15, 0, 0.3, 1.5] - return geometry => ({ - y: geometry - .useParallaxIntersection({ - speed, - relativeTo: 'frame', - }) - .transform(startClamp, [-2, -0.025, 0, 0.025, 0]) - .transform(x => x * 250) - .transform(x => Math.max(-300, Math.min(x, 300))), - opacity: geometry - .useParallaxIntersection({ - speed: 1, - relativeTo: 'frame', - }) - .transform(startClamp, [-3, 1, 1, 1, 1]) - .transform(x => Math.max(0, Math.min(x, 1))), - }) - } - - return ( - <> - - {views(0)} - - - - - Hello World - - - Hello World - - - Hello World - - - - - {views(2)} - - - {views(3)} - - - {views(4)} - - - {views(5)} - - - ) -} - -export function TestUIGlossSpeed() { - const [key, setKey] = React.useState(0) - - console.time('render') - console.time('write') - React.useLayoutEffect(() => { - console.timeEnd('write') - }, []) - - const items = ( - - - - {_.fill(new Array(50), 0).map((_, index) => ( - - lorem ipsume sit amet - - ))} - - - ) - console.timeEnd('render') - return items -} diff --git a/projects/playground/src/TestUIGlossSpeed.tsx b/projects/playground/src/TestUIGlossSpeed.tsx new file mode 100644 index 0000000000..62d448c7ea --- /dev/null +++ b/projects/playground/src/TestUIGlossSpeed.tsx @@ -0,0 +1,25 @@ +import { Button, CardSimple, Stack } from '@o/ui' +import _ from 'lodash' + +export function TestUIGlossSpeed() { + const [key, setKey] = React.useState(0) + console.time('render') + console.time('write') + React.useLayoutEffect(() => { + console.timeEnd('write') + }, []) + const items = ( + + + + {_.fill(new Array(50), 0).map((_, index) => ( + + lorem ipsume sit amet + + ))} + + + ) + console.timeEnd('render') + return items +} diff --git a/projects/playground/src/TestUIParallax.tsx b/projects/playground/src/TestUIParallax.tsx new file mode 100644 index 0000000000..8b7a89b950 --- /dev/null +++ b/projects/playground/src/TestUIParallax.tsx @@ -0,0 +1,190 @@ +import { Parallax, Stack, Title } from '@o/ui' + +export function TestUIParallax() { + console.log('render me') + const views = index => ( + <> + + offset 0.75 speed 0.25 index {index} + + + offset 0 speed 0.25 index {index} + + { + return { + y: geometry + .useParallaxIntersection({ speed: 3, offset: 0.5, clamp: [-1, 1.5] }) + .transform([-1, -0.4, 0, 1, 1.4, 1.5], [-1, -0.1, 0, 0.1, 0.2, 0.8]) + .transform(geometry.transforms.scrollParent), + opacity: geometry + .useParallaxIntersection({ speed: 3, offset: 0.5, clamp: [-1, 1.5] }) + .transform([-1, -0.4, 0, 1, 1.4, 1.5], [-1, 1, 1, 1, 1, 0.8]), + } + }} + > + Hello {index} + + + offset 0.5 speed 2 + + + ({ + opacity: geometry + .useParallaxIntersection({ speed: 1, offset: 0 }) + .transform([-1, 0.5, 0.5, 1], [0, 1, 1, -1]), + })} + > + opacity in/out + + + ({ + opacity: geometry.useParallaxIntersection(), + })} + > + opacity + + + ({ + opacity: geometry.useParallaxIntersection(), + })} + > + offset 0 speed 1 + + { + return { + y: geometry + .useParallaxIntersection({ + speed: 1, + relativeTo: 'frame', + }) + .transform([-1, -0.2, 0, 0.2, 1], [-3, -0.05, 0, 0.05, 3]) + .transform([-1, -0.2, 0, 0.2, 1], [-3, -0.05, 0, 0.05, 3]) + .transform(x => x * 4), + } + }} + > + Hellow Orld + + + ) + function getParallax({ speed = 1 } = {}) { + const startClamp = [-1, -0.15, 0, 0.3, 1.5] + return geometry => ({ + y: geometry + .useParallaxIntersection({ + speed, + relativeTo: 'frame', + }) + .transform(startClamp, [-2, -0.025, 0, 0.025, 0]) + .transform(x => x * 250) + .transform(x => Math.max(-300, Math.min(x, 300))), + opacity: geometry + .useParallaxIntersection({ + speed: 1, + relativeTo: 'frame', + }) + .transform(startClamp, [-3, 1, 1, 1, 1]) + .transform(x => Math.max(0, Math.min(x, 1))), + }) + } + return ( + <> + + {views(0)} + + + + + Hello World + + + Hello World + + + Hello World + + + + + {views(2)} + + + {views(3)} + + + {views(4)} + + + {views(5)} + + + ) +} diff --git a/projects/site/package.json b/projects/site/package.json index 1c01cfa64d..03c36e6e9c 100644 --- a/projects/site/package.json +++ b/projects/site/package.json @@ -4,8 +4,8 @@ "private": true, "main": "./src/index.tsx", "scripts": { - "start": "mcro-build --port 5000 --extract-static-styles --target web", - "start:prod": "mcro-build --extract-static-styles --prod --port 5000 --report --target web", + "start": "mcro-build --port 5000 --target web", + "start:prod": "OPTIMIZE_REACT=1 mcro-build --extract-static-styles --prod --port 5000 --report --target web", "build": "OPTIMIZE_REACT=1 ANALYZE_BUNDLE=1 mcro-build --extract-static-styles --prod --build --target web", "build:no-min": "NO_OPTIMIZE=1 mcro-build --prod --build --target web", "build:debug": "NODE_ENV=development mcro-build --prod --build --target web", diff --git a/projects/site/src/SiteRoot.tsx b/projects/site/src/SiteRoot.tsx index 14a467becd..df98846ee7 100644 --- a/projects/site/src/SiteRoot.tsx +++ b/projects/site/src/SiteRoot.tsx @@ -1,24 +1,32 @@ // //! -// import { ProvideUI, View, Stack } from '@o/ui' -// // +// import { ProvideUI, SimpleText } from '@o/ui' +// import { gloss } from 'gloss' // const React = require('react') // const { themes } = require('./themes') -// const height = eval(`1`) // export const SiteRoot = () => { // return ( // -// -// {}} -// flexDirection="row" -// alignItems="center" -// position="relative" -// justifyContent="center" -// /> +// +// Amazing internal tools +// // // ) // } +// const titleSize = 9 +// const TextFitTitle = gloss(SimpleText, { +// userSelect: 'text', +// lineHeight: '95%', +// fontSize: `${titleSize}vw`, +// 'lg-fontSize': titleSize * 11.5, +// }) import { ErrorBoundary, ProvideUI } from '@o/ui' import React, { StrictMode, Suspense } from 'react' import { Router, View } from 'react-navi' diff --git a/projects/site/src/pages/DocsPage/DocsButton.tsx b/projects/site/src/pages/DocsPage/DocsButton.tsx index 93b262d0d3..4b9a72c687 100644 --- a/projects/site/src/pages/DocsPage/DocsButton.tsx +++ b/projects/site/src/pages/DocsPage/DocsButton.tsx @@ -6,7 +6,7 @@ export let Basic = () => { return ( - +