diff --git a/.changeset/named-import-helper-order.md b/.changeset/named-import-helper-order.md new file mode 100644 index 00000000..02a1a47e --- /dev/null +++ b/.changeset/named-import-helper-order.md @@ -0,0 +1,5 @@ +--- +'babel-plugin-styled-components': patch +--- + +Helpers imported before `styled` in a named import are recognized now. In `import { css, styled } from 'styled-components'`, `css` and `keyframes` are minified, and with pure annotations turned on they are marked pure. diff --git a/.changeset/require-non-tag-exports.md b/.changeset/require-non-tag-exports.md new file mode 100644 index 00000000..00d3e95b --- /dev/null +++ b/.changeset/require-non-tag-exports.md @@ -0,0 +1,5 @@ +--- +'babel-plugin-styled-components': patch +--- + +A binding that is the styled factory keeps display names and component ids for properties this plugin does not recognize. On styled-components v7 that factory is a proxy, so `styled.box`, `styled.foo`, and `styled.feNotAFilter` are real components. `const styled = require('styled-components')`, `import sc from 'styled-components'`, `sc.default`, `ns.default`, and `__importDefault(require('styled-components')).default` are that factory. Known non-tag exports stay plain, including `sc.css`, `sc.keyframes`, `sc.createGlobalStyle`, `sc['createGlobalStyle']`, `sc.isStyledComponent`, and `sc.default.css`. Names on `Object.prototype` or `Function.prototype` stay plain too, including `sc.hasOwnProperty`, `sc.toString`, `sc.bind`, `sc.__lookupGetter__`, `sc.name`, and `sc.__proto__`. A computed key is resolved when it is a string, a template whose parts all fold to strings, or a const initialized to one of those, so `const name = 'css'; sc[name]`, `` sc[`css`] ``, and `` sc[`${'css'}`] `` stay plain, while `const css = 'div'; sc[css]` and `sc.default[css]` keep their ids. A key this plugin cannot resolve stays plain on `require()` and on an alias that is not named `styled`, so `let dynamic = 'css'; sc[dynamic]` returns the helper instead of throwing. `styled[dynamic]` on an import named `styled` is still decorated. A `let` or `var` that starts as the factory keeps its display name on a straight-line use written before the reassignment. A use inside a nested function, or inside a loop that also reassigns the binding, is left alone, so that function and the next loop iteration do not throw. diff --git a/AGENTS.md b/AGENTS.md index 4ab3ea1f..32319c8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ Commands - `pnpm build`: compile `src/` to `lib/`; run it to verify every set of changes. Don't start a dev server unless asked - `pnpm bench [--base ] [--config default|all|bare] [--rounds N] [--normalize-aliases]`: interleaved A/B of the plugin at `` (default `origin/main`) against the working tree over generated inputs; fails when outputs differ - `pnpm bench:profile [--input small|medium|large|large-cjs]`: CPU profile with the plugin frames that drive Babel's work +- `node scripts/compat.js [--base ]`: styled-components v5, v6, and v7 prerelease smoke against the working tree and `` (default `origin/main`) - `pnpm style`: Prettier over `src/` - `pnpm changeset`: add a release note @@ -86,6 +87,15 @@ Testing - Avoid snapshot churn. A change rewrites only the snapshots its behavior requires, and a hand-written expected output is changed only when the expectation itself was wrong, with the reason in the commit. - Jest `roots` is `test/`, so agent worktrees under `.claude/worktrees/` stay out of the run. +Review + +- Every behavior change is checked against styled-components v5 (latest 5.x), v6 (latest 6.x), and v7 (latest prerelease dist-tag). `node scripts/compat.js` compiles smoke programs with the branch and with main, then executes the output (`renderToString` plus `ServerStyleSheet`) against each version. Generated CSS, displayName, and componentId match main except the intended change. No runtime errors. +- Module formats: the plugin loads from CJS configs (`babel.config.js`, `.babelrc`) and ESM configs (`babel.config.mjs`, a `"type": "module"` project). Input covers ESM (default, named, aliased, and namespace imports) and CJS (`require`, `require(...).default`, destructured require). Babel's modules transform is checked both ways (preset-env `modules: false` and `'commonjs'`). Transformed output runs as native ESM and as CJS. +- Known interop facts: v5 and v6 expose styled as `module.exports.default` and have no `exports` map, so a default import under native Node ESM yields the module object (a packaging limit, not a plugin bug). v7 `require()` returns the callable styled and that function is a proxy, so an unknown key is a component. A binding that is the factory (require, a default import, a named `styled` import, `.default` / `['default']` of a require or a namespace, and an interop default helper) decorates unknown properties and skips known non-tag exports and any key for which `name in Object.prototype || name in Function.prototype` (no DOM or SVG tag is on those prototypes). A namespace import uses the same member rules, because a CommonJS transform binds it to the module exports. A local that shadows the binding, or a use that appears after a reassignment, never receives `.withConfig`. A straight-line use in the binding's own scope that is written before the reassignment keeps its display name and component id. A use inside a nested function, or inside a loop that also reassigns the binding, stays plain, because that code can run after the assignment. A computed key resolves when it is a string literal, a template whose parts all fold to strings, or a const binding of those, and then uses the skip set. A key that does not resolve stays plain on a require or on an alias whose name is not the styled import's local name. `styled[dynamic]` and a default import's own local name stay decorated, matching the previous plugin, and that can throw if the runtime value is a helper. +- A change to `isStyled` or the helper detectors is judged only after generating the full shape matrix: every import form (default, named, aliased, namespace, require, interop helper) x access form (dot, `.default`, computed literal, computed variable, call, `.attrs` / `.withConfig` chain, sequence) x export (intrinsic tags, known helpers, object methods, unknown names) x option preset. `node scripts/compat.js` runs a compact version of that matrix against v5, v6, and the latest v7 prerelease. A difference from the last release is a regression unless it is the intended fix. +- Output changes (hash shifts, newly recognized forms, minify differences) need a changeset line even when intended. +- Perf: run `pnpm bench` on main and the branch for changes that touch visitors or detectors. A slower branch is a regression. + Performance - Hot-path code earns microbenchmarks before claiming it is optimized: A/B against the prior code at several input sizes (`pnpm bench`). Its run-to-run noise is large on big inputs, so repeat a run before trusting a single-digit difference. Babel plugins run per file per build, so per-node allocation and repeated AST walks matter. diff --git a/scripts/compat.js b/scripts/compat.js new file mode 100644 index 00000000..80dd900e --- /dev/null +++ b/scripts/compat.js @@ -0,0 +1,938 @@ +// Compiles a handful of styled-components programs with the working tree and a +// base ref, then runs renderToString + ServerStyleSheet on styled-components +// v5, v6, and the latest v7 prerelease. Dev-only: those packages install into +// a temp cache, not this repo. Run with `node scripts/compat.js`. +const fs = require('fs') +const os = require('os') +const path = require('path') +const { spawnSync } = require('child_process') +const { parseArgs } = require('util') +const babel = require('@babel/core') +const presetEnvModule = require('@babel/preset-env') +const { buildRef, buildWorkingTree } = require('./bench/build-ref') +const { ROOT, withWorkDir } = require('./bench/shared') + +const presetEnv = presetEnvModule.default || presetEnvModule +const VIRTUAL_FILE = path.join(ROOT, 'compat-src', 'code.js') +const CACHE = path.join(os.tmpdir(), 'bpsc-styled-compat') +// v5 accepts React 18, and React 17 has no ESM entry Node can load for react-dom/server. +const REACT = { 5: '18.3.1', 6: '18.3.1' } + +const CASES = [ + { + name: 'cjs require, default tag and helpers', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['isStyledComponent.withConfig', 'createTheme.withConfig', 'extractCSS.withConfig'], + code: ` +const sc = require('styled-components') +const { isStyledComponent } = sc +const Comp = sc.default.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (sc.isStyledComponent(Comp) !== true) throw new Error('member helper rejected the tag') + if (isStyledComponent(Comp) !== true) throw new Error('destructured helper rejected the tag') + if (typeof sc.createTheme === 'function') sc.createTheme({ color: 'red' }) + if (typeof sc.extractCSS === 'function') sc.extractCSS() +} +`, + }, + { + name: 'cjs require, direct tag', + sourceType: 'script', + majors: [7], + plain: ['isStyledComponent.withConfig'], + code: ` +const sc = require('styled-components') +const Comp = sc.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (sc.isStyledComponent(Comp) !== true) throw new Error('member helper rejected the tag') +} +`, + }, + { + name: 'cjs require bound as styled', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['isStyledComponent.withConfig'], + code: ` +const styled = require('styled-components') +const Comp = styled.default.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (styled.isStyledComponent(Comp) !== true) throw new Error('member helper rejected the tag') +} +`, + }, + { + name: 'esm default and named helpers via commonjs transform', + sourceType: 'module', + modules: 'commonjs', + majors: [5, 6, 7], + plain: ['isStyledComponent.withConfig'], + code: ` +import styled, { css, isStyledComponent } from 'styled-components' +export const helper = css\`color: blue;\` +export const Comp = styled.div\`color: red;\` +export function check() { + if (isStyledComponent(Comp) !== true) throw new Error('named helper rejected the tag') +} +`, + }, + { + name: 'esm aliased styled via commonjs transform', + sourceType: 'module', + modules: 'commonjs', + majors: [6, 7], + plain: ['isStyledComponent.withConfig'], + code: ` +import { styled as sc, isStyledComponent } from 'styled-components' +export const Comp = sc.div\`color: red;\` +export function check() { + if (isStyledComponent(Comp) !== true) throw new Error('alias rejected the tag') +} +`, + }, + { + name: 'esm namespace via commonjs transform', + sourceType: 'module', + modules: 'commonjs', + majors: [5, 6, 7], + plain: [ + 'isStyledComponent.withConfig', + "['createTheme'].withConfig", + 'css.withConfig', + "['css'].withConfig", + ], + code: ` +import * as ns from 'styled-components' +export const Comp = ns.default.div\`color: red;\` +export function check() { + if (ns.isStyledComponent(Comp) !== true) throw new Error('namespace helper rejected the tag') + if (typeof ns.default.isStyledComponent === 'function') { + if (ns.default.isStyledComponent(Comp) !== true) { + throw new Error('namespace default helper rejected the tag') + } + } + if (typeof ns.default.createTheme === 'function') ns.default['createTheme']({ color: 'red' }) + if (typeof ns['css'] === 'function') ns['css']\`color: red;\` + if (typeof ns.default.css === 'function') ns.default.css\`color: blue;\` +} +`, + }, + { + name: 'cjs require, default member and computed helpers', + sourceType: 'script', + majors: [5, 6, 7], + plain: [ + 'isStyledComponent.withConfig', + "['isStyledComponent'].withConfig", + 'createTheme.withConfig', + "['createTheme'].withConfig", + 'css.withConfig', + "['css'].withConfig", + ], + code: ` +const sc = require('styled-components') +const Comp = sc.default.div\`color: red;\` +const ViaComputedTag = sc.default['div']\`color: blue;\` +exports.Comp = Comp +exports.ViaComputedTag = ViaComputedTag +exports.check = () => { + if (sc['isStyledComponent'](Comp) !== true) throw new Error('computed helper rejected the tag') + if (sc['isStyledComponent'](ViaComputedTag) !== true) { + throw new Error('computed helper rejected the computed tag') + } + if (typeof sc.default.isStyledComponent === 'function') { + if (sc.default.isStyledComponent(Comp) !== true) { + throw new Error('default member helper rejected the tag') + } + } + if (typeof sc.default.createTheme === 'function') sc.default['createTheme']({ color: 'red' }) + if (typeof sc['css'] === 'function') sc['css']\`color: red;\` + if (typeof sc.default.css === 'function') sc.default.css\`color: blue;\` + if (typeof sc['keyframes'] === 'function') sc['keyframes']\`from { opacity: 0; } to { opacity: 1; }\` +} +`, + }, + { + name: 'cjs require, computed tag', + sourceType: 'script', + majors: [7], + plain: ['isStyledComponent.withConfig'], + code: ` +const sc = require('styled-components') +const Comp = sc['div']\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (sc['isStyledComponent'](Comp) !== true) throw new Error('computed helper rejected the tag') +} +`, + }, + { + name: 'native esm, default import through .default', + sourceType: 'module', + modules: false, + majors: [5, 6, 7], + plain: ['isStyledComponent.withConfig'], + code: ` +import styled from 'styled-components' +export const Comp = styled.default.div\`color: red;\` +export function check() { + if (styled.isStyledComponent(Comp) !== true) throw new Error('esm helper rejected the tag') +} +`, + }, + { + name: 'native esm, direct tag', + sourceType: 'module', + modules: false, + majors: [5, 6, 7], + packagingLimitBelow: 7, + plain: ['isStyledComponent.withConfig'], + code: ` +import styled from 'styled-components' +export const Comp = styled.div\`color: red;\` +export function check() { + if (styled.isStyledComponent(Comp) !== true) throw new Error('esm helper rejected the tag') +} +`, + }, +] + +// Compact import x access x export matrix. Compile-only: the full generator is +// the audit harness, and this is the slice `node scripts/compat.js` always runs. +// Every factory binding uses open mode: unknown names are decorated, and object +// methods plus known non-tag exports are not. A namespace import uses the same +// member rules because a CommonJS transform binds it to the module exports. +const MATRIX_OPEN_SKIP = new Set([ + 'isStyledComponent', + 'css', + 'hasOwnProperty', + 'toString', +]) +const MATRIX_IMPORTS = [ + { + name: 'require sc', + sourceType: 'script', + bind: 'sc', + head: `const sc = require('styled-components')`, + }, + { + name: 'import styled', + sourceType: 'module', + bind: 'styled', + head: `import styled from 'styled-components'`, + }, + { + name: 'import sc', + sourceType: 'module', + bind: 'sc', + head: `import sc from 'styled-components'`, + }, + { + name: 'import { styled as s }', + sourceType: 'module', + bind: 's', + head: `import { styled as s } from 'styled-components'`, + }, + { + name: 'import * as ns', + sourceType: 'module', + bind: 'ns', + head: `import * as ns from 'styled-components'`, + }, +] +const MATRIX_ACCESS = [ + { name: 'dot', expr: (b, x) => `${b}.${x}` }, + { name: 'default.dot', expr: (b, x) => `${b}.default.${x}` }, + { name: 'computed', expr: (b, x) => `${b}['${x}']` }, + { name: 'default.computed', expr: (b, x) => `${b}.default['${x}']` }, + { name: 'computed-default.dot', expr: (b, x) => `${b}['default'].${x}` }, +] +const MATRIX_EXPORTS = [ + 'div', + 'View', + 'isStyledComponent', + 'css', + 'hasOwnProperty', + 'somethingNew', + 'box', + 'foo', + 'feNotAFilter', + 'marquee', +] +const matrixMemberDecorated = name => !MATRIX_OPEN_SKIP.has(name) + +for (const imp of MATRIX_IMPORTS) { + for (const access of MATRIX_ACCESS) { + for (const name of MATRIX_EXPORTS) { + const decorated = matrixMemberDecorated(name) + CASES.push({ + name: `matrix ${imp.name} ${access.name} ${name}`, + sourceType: imp.sourceType, + compileOnly: true, + expectWithConfig: decorated, + code: `${imp.head}\nconst Comp = ${access.expr(imp.bind, name)}\`color: red;\`\n`, + }) + } + } + for (const call of [ + { name: 'call', expr: b => `${b}(Base)`, absent: [] }, + { name: 'default.call', expr: b => `${b}.default(Base)`, absent: [] }, + { + name: 'computed-default.call', + expr: b => `${b}['default'](Base)`, + absent: [`['default'].withConfig`], + present: [`['default'](Base).withConfig`], + }, + ]) { + CASES.push({ + name: `matrix ${imp.name} ${call.name}`, + sourceType: imp.sourceType, + compileOnly: true, + expectWithConfig: true, + absent: call.absent, + present: call.present, + code: `${imp.head}\nfunction Base(props) { return props.children || null }\nconst Comp = ${call.expr(imp.bind)}` + '`color: red;`\n', + }) + } +} + +CASES.push( + { + name: 'matrix shadow param is not the require', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + absent: ['sc.div.withConfig'], + code: ` +const sc = require('styled-components') +function make(sc) { return sc.div\`color: red;\` } +const Comp = make({ div: s => () => null }) +const Real = sc.default.div\`color: blue;\` +`, + }, + { + name: 'matrix reassigned require is not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + absent: ['sc.div.withConfig'], + code: ` +const real = require('styled-components') +let sc = require('styled-components') +sc = { div: s => () => null } +const Comp = sc.div\`color: red;\` +const Real = real.default.div\`color: blue;\` +`, + }, + { + name: 'matrix computed variable key stays decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['sc.default[css].withConfig'], + code: ` +const sc = require('styled-components') +const css = 'div' +const Comp = sc.default[css]\`color: red;\` +`, + }, + { + name: 'matrix object method and unknown export do not crash', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['hasOwnProperty.withConfig', 'toString.withConfig'], + code: ` +const sc = require('styled-components') +const Comp = sc.default.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + const has = sc.hasOwnProperty('css') + if (typeof has !== 'boolean') throw new Error('hasOwnProperty was rewritten') + if (typeof sc.toString() !== 'string') throw new Error('toString was rewritten') +} +`, + }, + { + name: 'matrix computed default call decorates the result', + sourceType: 'script', + majors: [5, 6, 7], + plain: ["['default'].withConfig"], + present: ["['default'](Base).withConfig"], + code: ` +const sc = require('styled-components') +function Base(props) { return props.children || null } +const Comp = sc['default'](Base)\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (!Comp.styledComponentId) throw new Error('computed default call was not decorated') +} +`, + }, + { + name: 'matrix computed const helper key is not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: false, + code: ` +const sc = require('styled-components') +const name = 'css' +const Comp = sc[name]\`color: red;\` +const Other = sc[\`css\`]\`color: blue;\` +const Third = sc.default[name]\`color: green;\` +`, + }, + { + name: 'matrix computed const tag and spelled helper are decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['sc[tag].withConfig', 'sc[css].withConfig'], + absent: ['sc[name].withConfig', 'sc[`css`].withConfig'], + code: ` +const sc = require('styled-components') +const tag = 'box' +const css = 'div' +const name = 'css' +const Comp = sc[tag]\`color: red;\` +const ViaDiv = sc[css]\`color: blue;\` +const Skipped = sc[name]\`color: green;\` +const SkippedTpl = sc[\`css\`]\`color: orange;\` +`, + }, + { + name: 'matrix unresolved computed key on require is not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: false, + code: ` +const sc = require('styled-components') +let dynamic = 'css' +const bag = { key: 'css' } +const { key: fromBag } = bag +const Comp = sc[dynamic]\`color: red;\` +const FromBag = sc[fromBag]\`color: blue;\` +const Folded = sc[\`\${'css'}\`]\`color: green;\` +`, + }, + { + name: 'matrix unresolved computed key on styled import is decorated', + sourceType: 'module', + compileOnly: true, + expectWithConfig: true, + present: ['styled[dynamic].withConfig', 's2[dynamic].withConfig'], + absent: ['styled[css].withConfig'], + code: ` +import styled, { css } from 'styled-components' +const s2 = styled +let dynamic = 'div' +const Comp = styled[dynamic]\`color: red;\` +const Alias = s2[dynamic]\`color: blue;\` +const Helper = styled[css]\`color: green;\` +`, + }, + { + name: 'matrix unresolved computed key on styled alias is not decorated', + sourceType: 'module', + compileOnly: true, + expectWithConfig: true, + present: ['s.box.withConfig'], + absent: ['s[dynamic].withConfig', 's[themeKey].withConfig'], + code: ` +import { styled as s, ThemeProvider as themeKey } from 'styled-components' +let dynamic = 'div' +const Comp = s.box\`color: red;\` +const Skipped = s[dynamic]\`color: blue;\` +const SkippedImport = s[themeKey]\`color: green;\` +`, + }, + { + name: 'matrix prototype members are not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['sc.div.withConfig'], + absent: [ + '__lookupGetter__.withConfig', + '__lookupSetter__.withConfig', + '__proto__.withConfig', + '.bind.withConfig', + '.apply.withConfig', + '.call.withConfig', + '.length.withConfig', + '.name.withConfig', + '.arguments.withConfig', + '.caller.withConfig', + ], + code: ` +const sc = require('styled-components') +const Comp = sc.div\`color: red;\` +const Lookup = sc.__lookupGetter__\`x\` +const LookupSet = sc.__lookupSetter__\`x\` +const Proto = sc.__proto__\`x\` +const Bound = sc.bind\`x\` +const Applied = sc.apply\`x\` +const Called = sc.call\`x\` +const Len = sc.length\`x\` +const Named = sc.name\`x\` +const Args = sc.arguments\`x\` +const Caller = sc.caller\`x\` +`, + }, + { + name: 'matrix require use before reassignment stays decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['Before = sc.div.withConfig'], + absent: ['After = sc.div.withConfig'], + code: ` +let sc = require('styled-components') +const Before = sc.div\`color: red;\` +sc = { div: s => () => null } +const After = sc.div\`color: blue;\` +`, + }, + { + name: 'matrix require use in nested function is not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['Before = sc.div.withConfig'], + absent: ['sc.span.withConfig', 'After = sc.div.withConfig'], + code: ` +let sc = require('styled-components') +const Before = sc.div\`color: red;\` +function writtenAbove() { return sc.span\`color: blue;\` } +sc = { span: s => () => null, div: s => () => null } +const Late = writtenAbove() +const After = sc.div\`color: green;\` +`, + }, + { + name: 'matrix require use inside reassignment loop is not decorated', + sourceType: 'script', + compileOnly: true, + expectWithConfig: true, + present: ['First = sc.div.withConfig'], + absent: ['Comp = sc.div.withConfig', 'After = sc.div.withConfig'], + code: ` +let sc = require('styled-components') +const First = sc.div\`color: red;\` +for (let i = 0; i < 2; i++) { + const Comp = sc.div\`color: blue;\` + sc = { div: s => () => null } +} +const After = sc.div\`color: green;\` +`, + }, + { + name: 'closure over reassigned require does not throw', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['sc.span.withConfig'], + present: ['sc.default.div.withConfig'], + code: ` +let sc = require('styled-components') +const real = sc +const Comp = sc.default.div\`color: red;\` +function writtenAbove() { return sc.span\`color: blue;\` } +exports.Comp = Comp +exports.check = () => { + if (typeof real.span !== 'function') return + sc = { span: s => () => null, div: s => () => null } + const Late = writtenAbove() + if (typeof Late !== 'function') throw new Error('closure did not return the stand-in') +} +`, + }, + { + name: 'loop that reassigns require does not throw', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['sc.div.withConfig'], + present: ['sc.default.div.withConfig'], + code: ` +let sc = require('styled-components') +const Comp = sc.default.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (typeof sc.div !== 'function') return + for (let i = 0; i < 2; i++) { + sc.div\`color: blue;\` + sc = { div: s => () => null } + } +} +`, + }, + { + name: 'prototype members do not throw', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['__lookupGetter__.withConfig', '.bind.withConfig'], + present: ['sc.default.div.withConfig'], + code: ` +const sc = require('styled-components') +const Comp = sc.default.div\`color: red;\` +exports.Comp = Comp +exports.check = () => { + if (typeof sc.__lookupGetter__ !== 'function' || typeof sc.bind !== 'function') return + sc.__lookupGetter__\`x\` + const bound = sc.bind\`x\` + if (typeof bound !== 'function') throw new Error('bind was decorated') +} +`, + }, + { + name: 'unresolved require computed key does not throw', + sourceType: 'script', + majors: [5, 6, 7], + plain: ['sc[dynamic].withConfig'], + present: ['sc.default.div.withConfig', 'sc.default[css].withConfig'], + code: ` +const sc = require('styled-components') +const css = 'div' +let dynamic = 'css' +const Comp = sc.default.div\`color: red;\` +const Kept = sc.default[css]\`color: blue;\` +exports.Comp = Comp +exports.check = () => { + const helper = sc[dynamic]\`color: green;\` + if (helper == null) throw new Error('unresolved key did not return the helper') + if (!Kept.styledComponentId) throw new Error('folded default key lost its id') +} +`, + } +) + +const parseOptions = argv => { + const { values } = parseArgs({ + args: argv, + strict: true, + options: { base: { type: 'string', default: 'origin/main' } }, + }) + return values +} + +const npmJson = args => { + const result = spawnSync('npm', args, { encoding: 'utf8' }) + if (result.status !== 0) { + throw new Error( + (result.stderr || result.stdout || `npm ${args.join(' ')} failed`).trim() + ) + } + return JSON.parse(result.stdout) +} + +const latestOfMajor = major => { + const data = npmJson(['view', `styled-components@${major}`, 'version', '--json']) + const versions = Array.isArray(data) ? data : [data] + return versions[versions.length - 1] +} + +const latestV7Prerelease = () => { + const tags = npmJson(['view', 'styled-components', 'dist-tags', '--json']) + const matches = Object.entries(tags).filter( + ([, version]) => version.startsWith('7.') && version.includes('-') + ) + if (!matches.length) throw new Error('no v7 prerelease dist-tag on styled-components') + matches.sort((left, right) => (left[1] < right[1] ? -1 : 1)) + return matches[matches.length - 1][1] +} + +const latestReact19 = () => { + const data = npmJson(['view', 'react@19', 'version', '--json']) + const versions = Array.isArray(data) ? data : [data] + return versions[versions.length - 1] +} + +const loadPlugin = entry => { + const loaded = require(entry) + return loaded.default || loaded +} + +const compile = (plugin, source) => { + const options = { + babelrc: false, + configFile: false, + filename: VIRTUAL_FILE, + sourceType: source.sourceType, + ast: false, + plugins: [[plugin, {}]], + } + if (source.modules !== undefined) { + options.presets = [[presetEnv, { modules: source.modules, targets: { node: '22' } }]] + } + return babel.transformSync(source.code, options).code +} + +const ensureRuntime = (spec, react) => { + const dir = path.join(CACHE, spec.replace(/[^a-z0-9.-]+/gi, '_')) + const stampPath = path.join(dir, '.compat-stamp') + const stamp = `${spec}\n${react}` + if (fs.existsSync(stampPath) && fs.readFileSync(stampPath, 'utf8') === stamp) return dir + fs.rmSync(dir, { recursive: true, force: true }) + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ private: true })) + const result = spawnSync( + 'npm', + [ + 'install', + '--ignore-scripts', + '--no-audit', + '--no-fund', + `styled-components@${spec}`, + `react@${react}`, + `react-dom@${react}`, + `react-is@${react}`, + ], + { cwd: dir, encoding: 'utf8', timeout: 180000 } + ) + if (result.status !== 0) { + throw new Error( + `install styled-components@${spec} failed\n${result.stderr || result.stdout}` + ) + } + fs.writeFileSync(stampPath, stamp) + return dir +} + +const CJS_RUNNER = ` +const React = require('react') +const { renderToString } = require('react-dom/server') +const sc = require('styled-components') +const compiled = require('./compiled.cjs') +const ServerStyleSheet = sc.ServerStyleSheet +const sheet = new ServerStyleSheet() +let html = null +let css = null +let renderError = null +try { + html = renderToString(sheet.collectStyles(React.createElement(compiled.Comp))) + css = sheet.getStyleTags() +} catch (error) { + renderError = error && error.message +} finally { + if (typeof sheet.seal === 'function') sheet.seal() +} +let checkError = null +try { + compiled.check() +} catch (error) { + checkError = error && error.message +} +const displayName = compiled.Comp && compiled.Comp.displayName +const ids = css ? css.match(/sc-[a-z0-9]+-\\d+/g) : null +process.stdout.write(JSON.stringify({ renderError, checkError, displayName, ids, css, html: html ? html.length : 0 })) +` + +const ESM_RUNNER = ` +import React from 'react' +import { renderToString } from 'react-dom/server' +import sc from 'styled-components' +import { Comp, check } from './compiled.mjs' +const ServerStyleSheet = sc.ServerStyleSheet +const sheet = new ServerStyleSheet() +let html = null +let css = null +let renderError = null +try { + html = renderToString(sheet.collectStyles(React.createElement(Comp))) + css = sheet.getStyleTags() +} catch (error) { + renderError = error && error.message +} finally { + if (typeof sheet.seal === 'function') sheet.seal() +} +let checkError = null +try { + check() +} catch (error) { + checkError = error && error.message +} +const displayName = Comp && Comp.displayName +const ids = css ? css.match(/sc-[a-z0-9]+-\\d+/g) : null +process.stdout.write(JSON.stringify({ renderError, checkError, displayName, ids, css, html: html ? html.length : 0 })) +` + +const execute = (runtimeDir, code, nativeEsm) => { + const dir = nativeEsm ? path.join(runtimeDir, 'esm') : runtimeDir + fs.mkdirSync(dir, { recursive: true }) + if (nativeEsm) { + fs.writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ private: true, type: 'module' }) + ) + const link = path.join(dir, 'node_modules') + if (!fs.existsSync(link)) { + fs.symlinkSync(path.join(runtimeDir, 'node_modules'), link, 'dir') + } + fs.writeFileSync(path.join(dir, 'compiled.mjs'), code) + fs.writeFileSync(path.join(dir, 'runner.mjs'), ESM_RUNNER) + } else { + fs.writeFileSync(path.join(dir, 'compiled.cjs'), code) + fs.writeFileSync(path.join(dir, 'runner.cjs'), CJS_RUNNER) + } + const runner = nativeEsm ? 'runner.mjs' : 'runner.cjs' + const result = spawnSync(process.execPath, [runner], { + cwd: dir, + encoding: 'utf8', + timeout: 30000, + }) + if (result.stdout && result.stdout.trim().startsWith('{')) { + return JSON.parse(result.stdout) + } + const detail = `${result.stderr || ''}${result.stdout || ''}`.trim() + const brief = detail.split('\n').filter(Boolean).slice(0, 2).join(' ') + return { + renderError: brief || `runner exited ${result.status}`, + checkError: null, + displayName: null, + ids: null, + css: null, + } +} + +const idsEqual = (left, right) => JSON.stringify(left || []) === JSON.stringify(right || []) + +const assertHeadShape = (name, code) => { + const source = CASES.find(item => item.name === name) + const problems = [] + if (source.expectWithConfig === false) { + if (code.includes('withConfig')) problems.push('unexpected withConfig') + } else if (!code.includes('withConfig')) { + problems.push('plugin did not add withConfig to a tag') + } + for (const marker of source.plain || []) { + if (code.includes(marker)) problems.push(`head output contains ${marker}`) + } + for (const marker of source.absent || []) { + if (code.includes(marker)) problems.push(`head output contains ${marker}`) + } + for (const marker of source.present || []) { + if (!code.includes(marker)) problems.push(`head output missing ${marker}`) + } + return problems +} + +const reportRun = (label, outcome) => { + const render = outcome.renderError ? `render FAIL ${outcome.renderError}` : 'render ok' + const check = outcome.checkError ? `check FAIL ${outcome.checkError}` : 'check ok' + const id = outcome.ids && outcome.ids.length ? outcome.ids.join(',') : 'none' + console.log( + ` ${label}: ${render}; ${check}; displayName=${outcome.displayName || 'none'}; componentId=${id}` + ) +} + +const compareRuns = (base, head) => { + if (base.renderError || head.renderError) return null + if (base.displayName !== head.displayName) { + return `displayName ${base.displayName} vs ${head.displayName}` + } + if (!idsEqual(base.ids, head.ids)) { + return `componentId ${(base.ids || []).join(',')} vs ${(head.ids || []).join(',')}` + } + if (base.css !== head.css) return 'generated CSS differs' + return null +} + +const loadFromConfig = (filename, contents) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bpsc-compat-config-')) + const configFile = path.join(dir, filename) + fs.writeFileSync(configFile, contents) + if (filename.endsWith('.mjs')) { + fs.writeFileSync( + path.join(dir, 'package.json'), + JSON.stringify({ private: true, type: 'module' }) + ) + } + const output = babel.transformSync('const sc = require("styled-components"); sc.div`color: red;`', { + babelrc: false, + configFile, + filename: VIRTUAL_FILE, + sourceType: 'script', + ast: false, + }) + fs.rmSync(dir, { recursive: true, force: true }) + if (!output.code || !output.code.includes('withConfig')) { + throw new Error(`plugin did not run from ${filename}`) + } +} + +const checkConfigs = pluginEntry => { + const pluginPath = JSON.stringify(pluginEntry) + loadFromConfig('babel.config.js', `module.exports = { plugins: [${pluginPath}] }\n`) + loadFromConfig('.babelrc', JSON.stringify({ plugins: [pluginEntry] })) + loadFromConfig('babel.config.mjs', `export default { plugins: [${pluginPath}] }\n`) + console.log('configs: babel.config.js, .babelrc, and babel.config.mjs load the plugin') +} + +const main = async () => { + const options = parseOptions(process.argv.slice(2)) + const versions = [ + { major: 5, spec: latestOfMajor(5), react: REACT[5] }, + { major: 6, spec: latestOfMajor(6), react: REACT[6] }, + { major: 7, spec: latestV7Prerelease(), react: latestReact19() }, + ] + console.log( + versions.map(item => `v${item.major} ${item.spec} (react ${item.react})`).join(', ') + ) + const runtimes = new Map( + versions.map(item => [item.major, ensureRuntime(item.spec, item.react)]) + ) + + await withWorkDir(async workDir => { + const baseEntry = buildRef(options.base, path.join(workDir, 'base')) + const headEntry = buildWorkingTree(path.join(workDir, 'head')) + const basePlugin = loadPlugin(baseEntry) + const headPlugin = loadPlugin(headEntry) + checkConfigs(headEntry) + + for (const source of CASES) { + console.log(source.name) + const headCode = compile(headPlugin, source) + for (const problem of assertHeadShape(source.name, headCode)) { + console.error(` head: ${problem}`) + process.exitCode = 1 + } + if (source.compileOnly) continue + const baseCode = compile(basePlugin, source) + for (const version of versions.filter(item => source.majors.includes(item.major))) { + console.log(` v${version.major} ${version.spec}`) + const nativeEsm = source.modules === false + const baseRun = execute(runtimes.get(version.major), baseCode, nativeEsm) + const headRun = execute(runtimes.get(version.major), headCode, nativeEsm) + reportRun('base', baseRun) + reportRun('head', headRun) + const knownLimit = + source.packagingLimitBelow && version.major < source.packagingLimitBelow + if (knownLimit && (headRun.renderError || headRun.checkError)) { + console.log(' known packaging limit on this version, not a plugin failure') + continue + } + if (headRun.renderError || headRun.checkError) process.exitCode = 1 + const mismatch = compareRuns(baseRun, headRun) + if (mismatch) { + console.error(` mismatch: ${mismatch}`) + process.exitCode = 1 + } else if (!baseRun.renderError && !headRun.renderError) { + console.log(' css, displayName, and componentId match') + } + } + } + }) + + if (process.exitCode) { + console.error('compat: failed') + } else { + console.log('compat: ok') + } +} + +main().catch(error => { + console.error(error && error.stack ? error.stack : error) + process.exitCode = 1 +}) diff --git a/src/utils/detectors.js b/src/utils/detectors.js index 219edf12..b5e8ba13 100644 --- a/src/utils/detectors.js +++ b/src/utils/detectors.js @@ -18,15 +18,37 @@ const isTypeOnly = node => const IMPORT_NAMES_CACHE = 'styled-components-import-local-names' const INJECTED_DEFAULT_NAME = 'styled-components-injected-default-name' +const REQUIRED_NAMES = 'styled-components-required-names' + +// Names whose require() declarator assignStyledRequired has already walked. +// The css-prop pre-pass runs mid-traversal, and a require that sits below the +// first css prop must not be numbered there. +export const noteStyledRequired = (state, name) => { + if (!name) return + let names = state.file.get(REQUIRED_NAMES) + if (!names) { + names = new Set() + state.file.set(REQUIRED_NAMES, names) + } + names.add(name) +} + +const styledRequireVisited = (state, name) => { + const names = state.file.get(REQUIRED_NAMES) + return !!name && !!names && names.has(name) +} // The default binding the css-prop transform injected, which can be a // `require` (script files) that no ImportDeclaration scan would find export const injectedDefaultName = state => state.file.get(INJECTED_DEFAULT_NAME) +const LEGACY_DEFAULT_LOCAL = 'styled-components-legacy-default-local' + export const registerInjectedDefault = (state, name) => { state.file.set(INJECTED_DEFAULT_NAME, name) state.file.set(IMPORT_NAMES_CACHE, null) + state.file.set(LEGACY_DEFAULT_LOCAL, null) } export const importLocalName = (name, state) => { @@ -92,42 +114,385 @@ export const importLocalName = (name, state) => { return localName } -const NAMESPACE_NAMES_CACHE = 'styled-components-import-namespace-names' -const NO_NAMESPACE_NAMES = [] +// Known non-tag exports. Object and function prototype names are not listed +// here; `isBuiltinPrototypeKey` covers them. None of those names are DOM or +// SVG tags (`name`, `length`, `caller`, and `arguments` included). +const OPEN_SKIP = new Set([ + 'NativeStyleContext', + 'ParentContext', + 'SafeAreaInsetsProvider', + 'ServerStyleSheet', + 'StyleSheetConsumer', + 'StyleSheetContext', + 'StyleSheetManager', + 'ThemeConsumer', + 'ThemeContext', + 'ThemeProvider', + '__PRIVATE__', + '__testOnly_verticalAlignCompanions', + 'createGlobalStyle', + 'createTheme', + 'css', + 'extractCSS', + 'isStyledComponent', + 'keyframes', + 'matchMedia', + 'setAnimationDebug', + 'styled', + 'stylisPluginRSC', + 'toStyleSheet', + 'useBreakpoint', + 'useContainer', + 'useContainerQuery', + 'useMediaEnv', + 'useMediaQuery', + 'useNativeStyleContext', + 'useParentContext', + 'useTheme', + 'version', + 'withTheme', +]) -// Local names bound by `import * as X from 'styled-components'`. Kept apart -// from `importLocalName('default')` so a default import injected later (the -// css-prop transform) does not displace the namespace binding. -const importNamespaceNames = state => { - if (state.customImportName) return NO_NAMESPACE_NAMES +const INTEROP_REQUIRE = new Set(['_interopRequireDefault', '__importDefault']) + +// A computed key whose value is known at compile time: a string, a template +// whose parts all fold to strings, or a const binding of one of those. +// `let`, `var`, destructuring, and imports do not fold. +const resolveStaticKey = (t, node, scope, seen) => { + const current = unwrapType(node) + if (!current) return null + if (t.isStringLiteral(current)) return current.value + if (t.isTemplateLiteral(current)) { + const quasis = current.quasis || [] + const expressions = current.expressions || [] + let out = '' + for (let i = 0; i < quasis.length; i++) { + const cooked = quasis[i] && quasis[i].value.cooked + if (cooked == null) return null + out += cooked + if (i >= expressions.length) continue + const part = resolveStaticKey(t, expressions[i], scope, seen) + if (part == null) return null + out += part + } + return out + } + if (!t.isIdentifier(current) || !scope) return null + const binding = scope.getBinding(current.name) + if ( + !binding || + binding.kind !== 'const' || + !binding.path.isVariableDeclarator() + ) { + return null + } + if (seen && seen.has(binding)) return null + const next = seen || new Set() + next.add(binding) + const resolved = resolveStaticKey( + t, + binding.path.node.init, + binding.scope, + next + ) + next.delete(binding) + return resolved +} - let names = state.file.get(NAMESPACE_NAMES_CACHE) - if (names) return names +// `sc.div` / `sc['div']` / `sc[\`div\`]` / `const tag = 'div'; sc[tag]` have a +// static name. Anything else is a dynamic lookup. The identifier's spelling +// is not a key. +const memberKey = (t, member, scope) => { + if (!t.isMemberExpression(member)) return null + if (member.computed) { + const resolved = resolveStaticKey(t, member.property, scope) + return resolved == null + ? { kind: 'dynamic' } + : { kind: 'static', name: resolved } + } + return t.isIdentifier(member.property) + ? { kind: 'static', name: member.property.name } + : null +} - names = [] - for (const node of state.file.path.node.body) { - if (node.type !== 'ImportDeclaration') continue - if (isTypeOnly(node)) continue - if (!isValidTopLevelImport(node.source.value, state)) continue +const unwrapType = node => { + let current = node + while ( + current && + (current.type === 'TSAsExpression' || + current.type === 'TSTypeAssertion' || + current.type === 'TSSatisfiesExpression') + ) { + current = current.expression + } + return current +} - for (const specifier of node.specifiers) { - if (specifier.type === 'ImportNamespaceSpecifier') { - names.push(specifier.local.name) +const isStyledRequireCall = (t, node, state) => { + if (!t.isCallExpression(node) || !t.isIdentifier(node.callee)) return false + if ( + INTEROP_REQUIRE.has(node.callee.name) && + node.arguments && + node.arguments[0] + ) { + return isStyledRequireCall(t, node.arguments[0], state) + } + const arg = node.arguments && node.arguments[0] + return ( + node.callee.name === 'require' && + !!arg && + t.isStringLiteral(arg) && + isValidTopLevelImport(arg.value, state) + ) +} + +// Identifier bound with `require('styled-components')` or `.default` / `['default']` +// of that call (including the `_interopRequireDefault` / `__importDefault` wrap). +export const requireBindingName = (t, id, init, state) => { + if (!t.isIdentifier(id)) return null + let node = unwrapType(init) + if (!node) return null + const peeled = staticDefaultObject(t, node) + if (peeled) node = peeled + return isStyledRequireCall(t, node, state) ? id.name : null +} + +// `.default` / `['default']`, or null when this member is not that key. +const staticDefaultObject = (t, node, scope) => { + if (!t.isMemberExpression(node)) return null + const key = memberKey(t, node, scope) + if (!key || key.kind !== 'static' || key.name !== 'default') return null + return node.object +} + +const referencePathFor = (binding, referenceNode) => { + const paths = binding.referencePaths + if (!paths || !referenceNode) return null + for (let i = 0; i < paths.length; i++) { + if (paths[i].node === referenceNode) return paths[i] + } + return null +} + +const isLoopPath = path => + path.isForStatement() || + path.isWhileStatement() || + path.isDoWhileStatement() || + path.isForInStatement() || + path.isForOfStatement() + +// A function nested inside the binding's scope can run after a reassignment +// even when the function is written above it. +const inNestedFunction = (binding, refPath) => { + let scope = refPath.scope + while (scope && scope !== binding.scope) { + if (scope.path && scope.path.isFunction()) return true + scope = scope.parent + } + return false +} + +// A loop that both reads the binding and assigns it runs the read again +// after the assignment. A read in a loop whose assignment is outside the +// loop still sees the factory on every iteration. +const loopReassigns = (binding, refPath) => { + const stop = binding.scope && binding.scope.path + const violations = binding.constantViolations || [] + let path = refPath.parentPath + while (path && path !== stop) { + if (isLoopPath(path)) { + for (let i = 0; i < violations.length; i++) { + const violation = violations[i] + if (violation === path || path.isAncestor(violation)) return true + } + } + path = path.parentPath + } + return false +} + +const assignmentPrecedes = (binding, referenceNode) => { + if (!referenceNode || typeof referenceNode.start !== 'number') return true + return binding.constantViolations.some(violation => { + const start = violation.node && violation.node.start + return typeof start === 'number' && start < referenceNode.start + }) +} + +// True when this reference must not be treated as the factory. Straight-line +// uses in the binding's scope that sit before the assignment stay decorated. +const reassignmentUnsafe = (binding, referenceNode) => { + if (!binding || binding.constant !== false) return false + const refPath = referencePathFor(binding, referenceNode) + if (refPath) { + if (inNestedFunction(binding, refPath)) return true + if (loopReassigns(binding, refPath)) return true + } + return assignmentPrecedes(binding, referenceNode) +} + +const importedNameOf = (t, specifier) => { + const imported = specifier.imported + if (t.isIdentifier(imported)) return imported.name + if (t.isStringLiteral(imported)) return imported.value + return null +} + +// 'open': the styled factory. Unknown members are decorated. Object methods +// and known non-tag exports are not. This is require(), a default import, a +// named `styled` import (any local name), `.default` of those, and the +// `_interopRequireDefault` / `__importDefault` helper. +// 'namespace': `import * as ns`. Same members as 'open': a CommonJS transform +// binds `ns` to the module exports, which is the factory on these packages. +// The call `ns(Comp)` is decorated too. A native ESM namespace is not +// callable, so that call throws with or without `.withConfig`. +// null: this name is not a styled binding at `referenceNode`. +const styledBindingKind = (name, scope, state, t, seen, referenceNode) => { + if (!name || !scope) return null + const token = `${scope.uid}:${name}` + if (seen && seen.has(token)) return null + + const binding = scope.getBinding(name) + if (!binding) { + if (state.customImportName && name === state.customImportName.name) { + return 'open' + } + // css-prop injects `var _styled = interop(require()).default` and then + // calls it. That binding is not visible to scope.getBinding, so the name + // registered for the injection is the styled function. + if (injectedDefaultName(state) === name) return 'open' + // A bundler global named `styled` next to a require() of the package. + if (name === 'styled' && state.styledRequired) return 'open' + return null + } + + if (reassignmentUnsafe(binding, referenceNode)) return null + + const bpath = binding.path + if ( + bpath.isImportDefaultSpecifier() || + bpath.isImportSpecifier() || + bpath.isImportNamespaceSpecifier() + ) { + const decl = bpath.parentPath + if (!decl.isImportDeclaration()) return null + if (isTypeOnly(decl.node) || isTypeOnly(bpath.node)) return null + if (!isValidTopLevelImport(decl.node.source.value, state)) return null + if (bpath.isImportNamespaceSpecifier()) return 'namespace' + if (bpath.isImportSpecifier()) { + const importedName = importedNameOf(t, bpath.node) + if (importedName !== 'styled' && importedName !== 'default') return null + } + return 'open' + } + + if (!bpath.isVariableDeclarator() || !t.isIdentifier(bpath.node.id)) { + return null + } + + const init = unwrapType(bpath.node.init) + if (!init) return null + // Noted only once assignStyledRequired has walked the declarator, so a + // require that sits below the first css prop is not numbered in that + // pre-pass. `.default` / `['default']` of the call is the same binding. + const peeled = staticDefaultObject(t, init, binding.scope) + if ( + isStyledRequireCall(t, init, state) || + (peeled && isStyledRequireCall(t, peeled, state)) + ) { + return styledRequireVisited(state, bpath.node.id.name) ? 'open' : null + } + + const chain = seen || new Set() + chain.add(token) + + if (peeled) { + // `const styled = sc.default` where `sc` is a require or a namespace. + if (t.isIdentifier(peeled)) { + const baseKind = styledBindingKind( + peeled.name, + binding.scope, + state, + t, + chain, + peeled + ) + if (baseKind === 'open' || baseKind === 'namespace') return 'open' + } + return null + } + + if (t.isIdentifier(init)) { + return styledBindingKind(init.name, binding.scope, state, t, chain, init) + } + + return null +} + +// `in` is safe here: these prototypes are ordinary objects, and the check +// does not read the property. A name that is also a real tag would be kept, +// but no DOM or SVG tag is on either prototype. +const isBuiltinPrototypeKey = name => + name in Object.prototype || name in Function.prototype + +// 2.3.0's default local: the string `'styled'` whenever a require() of the +// package exists, then overwritten by top-level imports. A named `styled` +// import sets the string `'styled'`, not the local alias. A default import +// sets that import's local name. A namespace sets its local name only when +// nothing else did. +const legacyDefaultLocal = (state, t) => { + if (state.customImportName) return state.customImportName.name + + const hadRequired = !!state.styledRequired + const cached = state.file.get(LEGACY_DEFAULT_LOCAL) + if (cached && cached.hadRequired === hadRequired) return cached.value + + let localName = hadRequired ? 'styled' : null + const body = + state.file.path && state.file.path.node && state.file.path.node.body + if (body) { + for (const node of body) { + if (node.type !== 'ImportDeclaration') continue + if (isTypeOnly(node)) continue + if (!isValidTopLevelImport(node.source.value, state)) continue + for (const specifier of node.specifiers) { + if (isTypeOnly(specifier)) continue + const imported = specifier.imported + const importedName = !imported + ? null + : t.isIdentifier(imported) + ? imported.name + : t.isStringLiteral(imported) + ? imported.value + : null + if (specifier.type === 'ImportSpecifier' && importedName === 'styled') { + localName = 'styled' + } + if (specifier.type === 'ImportDefaultSpecifier') { + localName = specifier.local.name + } + if ( + specifier.type === 'ImportSpecifier' && + importedName === 'default' + ) { + localName = specifier.local.name + } + if (specifier.type === 'ImportNamespaceSpecifier' && !localName) { + localName = specifier.local.name + } } } } - state.file.set(NAMESPACE_NAMES_CACHE, names) - return names + state.file.set(LEGACY_DEFAULT_LOCAL, { hadRequired, value: localName }) + return localName } -// Follow `const X = Y` (and TS `const X = Y as Type`) chains so a local -// re-binding of the styled import still resolves to it. Lazy: only walks -// the chain when the direct-name check misses, so the hot path is unchanged. const resolvesToDefaultLocal = (name, defaultLocal, state, t) => { if (!name || !defaultLocal) return false if (name === defaultLocal) return true - const scope = state.file.path.scope + const scope = state.file && state.file.path && state.file.path.scope + if (!scope) return false const visited = new Set([name]) let current = name while (true) { @@ -152,83 +517,129 @@ const resolvesToDefaultLocal = (name, defaultLocal, state, t) => { } } -// With only a styled `require` in the file, the default name is the required -// variable. A global `styled` (a bundler-provided identifier) keeps resolving -// as the default too. -const globalStyledAlias = (state, defaultLocal) => - state.styledRequired && defaultLocal === state.styledRequired - ? 'styled' - : null +const legacyDecoratesUnresolved = (objectName, state, t) => + resolvesToDefaultLocal(objectName, legacyDefaultLocal(state, t), state, t) + +const decorateKeyedMember = (kind, key, property, state, t, objectName) => { + if (!kind || !key) return false + // An unresolved key is decorated only for the names 2.3.0 decorated + // (`styled[dynamic]`, a default import's local name). A require named + // something else, and an alias that is not that name, stay plain. + if (key.kind === 'dynamic') { + if (!legacyDecoratesUnresolved(objectName, state, t)) return false + return !isHelper(t)(property, state) + } + if (key.kind !== 'static' || key.name === 'default') return false + if (isBuiltinPrototypeKey(key.name)) return false + if (kind === 'open' || kind === 'namespace') return !OPEN_SKIP.has(key.name) + return false +} -// `ns.default`: the callable default export reached through a namespace -const isNamespaceDefault = (t, node, defaultLocal, state) => - t.isMemberExpression(node) && - node.property.name === 'default' && - (node.object.name === defaultLocal || - node.object.name === globalStyledAlias(state, defaultLocal) || - importNamespaceNames(state).includes(node.object.name)) +const isDefaultMember = (t, node, scope) => { + if (!t.isMemberExpression(node) || !t.isIdentifier(node.object)) return false + const key = memberKey(t, node, scope) + return !!key && key.kind === 'static' && key.name === 'default' +} -export const isStyled = t => (tag, state) => { - if ( - t.isCallExpression(tag) && - t.isMemberExpression(tag.callee) && - tag.callee.property.name !== 'default' - ) { - return isStyled(t)(tag.callee.object, state) +export const isStyled = t => (tag, state, scope) => { + const lookup = + scope || (state.file && state.file.path && state.file.path.scope) + + if (t.isCallExpression(tag) && t.isMemberExpression(tag.callee)) { + const key = memberKey(t, tag.callee, lookup) + // `.attrs()` / `.withConfig()` chains. `['default']` is the styled + // function, same as `.default`, so it is not peeled off here. + if (!(key && key.kind === 'static' && key.name === 'default')) { + return isStyled(t)(tag.callee.object, state, lookup) + } } else if ( t.isCallExpression(tag) && t.isSequenceExpression(tag.callee) && - t.isMemberExpression(getSequenceExpressionValue(tag.callee)) && - getSequenceExpressionValue(tag.callee).property.name !== 'default' + t.isMemberExpression(getSequenceExpressionValue(tag.callee)) ) { - return isStyled(t)(getSequenceExpressionValue(tag.callee), state) - } else { - const defaultLocal = importLocalName('default', state) - const alias = globalStyledAlias(state, defaultLocal) - const matchesDefault = name => - resolvesToDefaultLocal(name, defaultLocal, state, t) || - resolvesToDefaultLocal(name, alias, state, t) - return ( - (t.isMemberExpression(tag) && - tag.property.name !== 'default' && - matchesDefault(tag.object.name) && - !isHelper(t)(tag.property, state)) || - (t.isCallExpression(tag) && matchesDefault(tag.callee.name)) || - (t.isCallExpression(tag) && - t.isSequenceExpression(tag.callee) && - matchesDefault(getSequenceExpressionValue(tag.callee).name)) || - // styled.default.div``, styled.default.something() — require() forms - (state.styledRequired && - t.isMemberExpression(tag) && - t.isMemberExpression(tag.object) && - tag.object.property.name === 'default' && - tag.object.object.name === state.styledRequired) || - (state.styledRequired && - t.isCallExpression(tag) && - t.isMemberExpression(tag.callee) && - tag.callee.property.name === 'default' && - tag.callee.object.name === state.styledRequired) || - (state.styledRequired && - t.isCallExpression(tag) && - t.isSequenceExpression(tag.callee) && - t.isMemberExpression(getSequenceExpressionValue(tag.callee)) && - getSequenceExpressionValue(tag.callee).property.name === 'default' && - getSequenceExpressionValue(tag.callee).object.name === - state.styledRequired) || - (t.isMemberExpression(tag) && - isNamespaceDefault(t, tag.object, defaultLocal, state)) || - (t.isCallExpression(tag) && - isNamespaceDefault(t, tag.callee, defaultLocal, state)) || - (t.isCallExpression(tag) && - t.isSequenceExpression(tag.callee) && - isNamespaceDefault( - t, - getSequenceExpressionValue(tag.callee), - defaultLocal, - state - )) + const value = getSequenceExpressionValue(tag.callee) + const key = memberKey(t, value, lookup) + if (!(key && key.kind === 'static' && key.name === 'default')) { + return isStyled(t)(value, state, lookup) + } + } + + if (t.isCallExpression(tag)) { + const callee = t.isSequenceExpression(tag.callee) + ? getSequenceExpressionValue(tag.callee) + : tag.callee + if (t.isIdentifier(callee)) { + const kind = styledBindingKind( + callee.name, + lookup, + state, + t, + null, + callee + ) + return kind === 'open' || kind === 'namespace' + } + if (isDefaultMember(t, callee, lookup)) { + const kind = styledBindingKind( + callee.object.name, + lookup, + state, + t, + null, + callee.object + ) + return kind === 'open' || kind === 'namespace' + } + return false + } + + if (!t.isMemberExpression(tag)) return false + + if ( + isDefaultMember(t, tag.object, lookup) && + t.isIdentifier(tag.object.object) + ) { + const kind = styledBindingKind( + tag.object.object.name, + lookup, + state, + t, + null, + tag.object.object + ) + if (!kind) return false + const outer = memberKey(t, tag, lookup) + // `.default` is the factory, so its members use the open skip set. + // An unresolved computed key stays decorated, including when the + // variable is spelled like a helper. + if (outer && outer.kind === 'dynamic') return true + return decorateKeyedMember( + 'open', + outer, + tag.property, + state, + t, + tag.object.object.name ) } + + if (!t.isIdentifier(tag.object)) return false + const kind = styledBindingKind( + tag.object.name, + lookup, + state, + t, + null, + tag.object + ) + return decorateKeyedMember( + kind, + memberKey(t, tag, lookup), + tag.property, + state, + t, + tag.object.name + ) } export const isCSSHelper = t => (tag, state) => diff --git a/src/visitors/assignStyledRequired.js b/src/visitors/assignStyledRequired.js index 2af37b40..3c158c4a 100644 --- a/src/visitors/assignStyledRequired.js +++ b/src/visitors/assignStyledRequired.js @@ -1,30 +1,8 @@ -import { isValidTopLevelImport } from '../utils/detectors' +import { noteStyledRequired, requireBindingName } from '../utils/detectors' export default t => (path, state) => { - let init = path.node.init - - if ( - t.isCallExpression(path.node.init) && - t.isIdentifier(path.node.init.callee) && - // `_interopRequireDefault` is Babel's CJS interop helper; `__importDefault` - // is TypeScript's. Both wrap a require() so the caller can reach `.default`. - (init.callee.name === '_interopRequireDefault' || - init.callee.name === '__importDefault') && - init.arguments && - init.arguments[0] - ) { - init = path.node.init.arguments[0] - } - - if ( - t.isCallExpression(init) && - t.isIdentifier(init.callee) && - init.callee.name === 'require' && - init.arguments && - init.arguments[0] && - t.isStringLiteral(init.arguments[0]) && - isValidTopLevelImport(init.arguments[0].value, state) - ) { - state.styledRequired = path.node.id.name - } + const name = requireBindingName(t, path.node.id, path.node.init, state) + if (!name) return + state.styledRequired = name + noteStyledRequired(state, name) } diff --git a/src/visitors/displayNameAndId.js b/src/visitors/displayNameAndId.js index 8b59aeab..7f282911 100644 --- a/src/visitors/displayNameAndId.js +++ b/src/visitors/displayNameAndId.js @@ -232,8 +232,12 @@ const calleeNeedsConfig = (t, callee) => { // styled(x)({}) if (!t.isMemberExpression(callee.callee)) return true - const method = callee.callee.property && callee.callee.property.name - if (!method) return false + const property = callee.callee.property + // `styled['default'](x)({})` has a string key, so `.name` is empty. + const method = + property && + (property.name || (t.isStringLiteral(property) ? property.value : null)) + if (!method) return true // styled(x).attrs()({}) if (method !== 'withConfig') return true @@ -253,8 +257,8 @@ export default t => { const { tag, callee } = path.node const needsConfig = tag - ? isStyledNode(tag, state) - : isStyledNode(callee, state) && calleeNeedsConfig(t, callee) + ? isStyledNode(tag, state, path.scope) + : isStyledNode(callee, state, path.scope) && calleeNeedsConfig(t, callee) if (!needsConfig) return diff --git a/src/visitors/minify.js b/src/visitors/minify.js index b0140b60..e80b3688 100644 --- a/src/visitors/minify.js +++ b/src/visitors/minify.js @@ -5,7 +5,8 @@ import { minifyRawValues, minifyCookedValues } from '../minify' export default t => (path, state) => { if ( useMinify(state) && - (isStyled(t)(path.node.tag, state) || isHelper(t)(path.node.tag, state)) + (isStyled(t)(path.node.tag, state, path.scope) || + isHelper(t)(path.node.tag, state)) ) { const templateLiteral = path.node.quasi const quasisLength = templateLiteral.quasis.length diff --git a/src/visitors/pure.js b/src/visitors/pure.js index 8c9626d9..6ddcd86c 100644 --- a/src/visitors/pure.js +++ b/src/visitors/pure.js @@ -6,8 +6,8 @@ import { isStyled, isPureHelper } from '../utils/detectors' export default t => (path, state) => { if (usePureAnnotation(state)) { if ( - isStyled(t)(path.node, state) || - isStyled(t)(path.node.callee, state) || + isStyled(t)(path.node, state, path.scope) || + isStyled(t)(path.node.callee, state, path.scope) || isPureHelper(t)(path.node.tag || path.node.callee, state) ) { if ( diff --git a/src/visitors/templateLiterals/transpile.js b/src/visitors/templateLiterals/transpile.js index a7716673..a1801895 100644 --- a/src/visitors/templateLiterals/transpile.js +++ b/src/visitors/templateLiterals/transpile.js @@ -1,7 +1,10 @@ import { isStyled, isHelper } from '../../utils/detectors' export default t => (path, state) => { - if (isStyled(t)(path.node.tag, state) || isHelper(t)(path.node.tag, state)) { + if ( + isStyled(t)(path.node.tag, state, path.scope) || + isHelper(t)(path.node.tag, state) + ) { const { tag: callee, quasi: { quasis, expressions }, diff --git a/test/fixtures/aliased-import-allowlist/.babelrc b/test/fixtures/aliased-import-allowlist/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/aliased-import-allowlist/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/aliased-import-allowlist/code.js b/test/fixtures/aliased-import-allowlist/code.js new file mode 100644 index 00000000..826251f2 --- /dev/null +++ b/test/fixtures/aliased-import-allowlist/code.js @@ -0,0 +1,13 @@ +import sc from 'styled-components' +import { styled as s } from 'styled-components' + +const FromDefault = sc.div`color: red;` +const FromNamed = s.View`color: blue;` +const helper = sc.isStyledComponent(FromDefault) +const unknown = s.somethingNew`color: green;` +const owned = sc.hasOwnProperty('div') +const Box = sc.box`color: white;` +const Filter = s.feNotAFilter`color: black;` +const helperCss = sc.css`color: pink;` +const helperDefault = sc.default.css`color: teal;` +const FromDefaultProxy = sc.default.marquee`color: gray;` diff --git a/test/fixtures/aliased-import-allowlist/output.js b/test/fixtures/aliased-import-allowlist/output.js new file mode 100644 index 00000000..85d1648a --- /dev/null +++ b/test/fixtures/aliased-import-allowlist/output.js @@ -0,0 +1,30 @@ +import sc from 'styled-components'; +import { styled as s } from 'styled-components'; +const FromDefault = sc.div.withConfig({ + displayName: "code__FromDefault", + componentId: "sc-ijl2tq-0" +})(["color: red;"]); +const FromNamed = s.View.withConfig({ + displayName: "code__FromNamed", + componentId: "sc-ijl2tq-1" +})(["color: blue;"]); +const helper = sc.isStyledComponent(FromDefault); +const unknown = s.somethingNew.withConfig({ + displayName: "code__unknown", + componentId: "sc-ijl2tq-2" +})(["color: green;"]); +const owned = sc.hasOwnProperty('div'); +const Box = sc.box.withConfig({ + displayName: "code__Box", + componentId: "sc-ijl2tq-3" +})(["color: white;"]); +const Filter = s.feNotAFilter.withConfig({ + displayName: "code__Filter", + componentId: "sc-ijl2tq-4" +})(["color: black;"]); +const helperCss = sc.css`color: pink;`; +const helperDefault = sc.default.css`color: teal;`; +const FromDefaultProxy = sc.default.marquee.withConfig({ + displayName: "code__FromDefaultProxy", + componentId: "sc-ijl2tq-5" +})(["color: gray;"]); diff --git a/test/fixtures/css-prop-script-mode-require-default-after-css-prop/.babelrc b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/.babelrc new file mode 100644 index 00000000..e7cd3222 --- /dev/null +++ b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "cssProp": true + } + ] + ] +} diff --git a/test/fixtures/css-prop-script-mode-require-default-after-css-prop/code.js b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/code.js new file mode 100644 index 00000000..5d891f13 --- /dev/null +++ b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/code.js @@ -0,0 +1,7 @@ +const styled = require('styled-components').default + +const Early = styled.h1`color: blue;` +const A = () =>
+ +const late = require('styled-components')['default'] +const Late = late.span`color: green;` diff --git a/test/fixtures/css-prop-script-mode-require-default-after-css-prop/output.js b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/output.js new file mode 100644 index 00000000..0d0c0581 --- /dev/null +++ b/test/fixtures/css-prop-script-mode-require-default-after-css-prop/output.js @@ -0,0 +1,17 @@ +var _styled = _interopRequireDefault(require("styled-components")).default; +function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } +const styled = require('styled-components').default; +const Early = styled.h1.withConfig({ + displayName: "code__Early", + componentId: "sc-lpytol-0" +})(["color:blue;"]); +const A = () => <_StyledDiv />; +const late = require('styled-components')['default']; +const Late = late.span.withConfig({ + displayName: "code__Late", + componentId: "sc-lpytol-2" +})(["color:green;"]); +var _StyledDiv = _styled("div").withConfig({ + displayName: "code___StyledDiv", + componentId: "sc-lpytol-1" +})(["color:red;"]); diff --git a/test/fixtures/css-prop-with-named-styled-import/.babelrc b/test/fixtures/css-prop-with-named-styled-import/.babelrc new file mode 100644 index 00000000..c8dd2643 --- /dev/null +++ b/test/fixtures/css-prop-with-named-styled-import/.babelrc @@ -0,0 +1,15 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false, + "cssProp": true + } + ] + ] +} diff --git a/test/fixtures/css-prop-with-named-styled-import/code.js b/test/fixtures/css-prop-with-named-styled-import/code.js new file mode 100644 index 00000000..67ac77ae --- /dev/null +++ b/test/fixtures/css-prop-with-named-styled-import/code.js @@ -0,0 +1,4 @@ +import { styled } from 'styled-components' + +const Tag = styled.div`color: blue;` +const App = () =>
diff --git a/test/fixtures/css-prop-with-named-styled-import/output.js b/test/fixtures/css-prop-with-named-styled-import/output.js new file mode 100644 index 00000000..e5dc8a7e --- /dev/null +++ b/test/fixtures/css-prop-with-named-styled-import/output.js @@ -0,0 +1,10 @@ +import { styled } from 'styled-components'; +const Tag = styled.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-sqpadk-0" +})(["color: blue;"]); +const App = () => <_StyledDiv />; +var _StyledDiv = styled("div").withConfig({ + displayName: "code___StyledDiv", + componentId: "sc-sqpadk-1" +})(["color: red;"]); diff --git a/test/fixtures/default-import-unknown-member/.babelrc b/test/fixtures/default-import-unknown-member/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/default-import-unknown-member/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/default-import-unknown-member/code.js b/test/fixtures/default-import-unknown-member/code.js new file mode 100644 index 00000000..e5d42b1b --- /dev/null +++ b/test/fixtures/default-import-unknown-member/code.js @@ -0,0 +1,6 @@ +import styled from 'styled-components' + +const Tag = styled.div`color: red;` +const Extra = styled.notAnExport`color: blue;` +const helper = styled.isStyledComponent(Tag) +const owned = styled.hasOwnProperty('div') diff --git a/test/fixtures/default-import-unknown-member/output.js b/test/fixtures/default-import-unknown-member/output.js new file mode 100644 index 00000000..bf7d1fe5 --- /dev/null +++ b/test/fixtures/default-import-unknown-member/output.js @@ -0,0 +1,11 @@ +import styled from 'styled-components'; +const Tag = styled.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-1awywoz-0" +})(["color: red;"]); +const Extra = styled.notAnExport.withConfig({ + displayName: "code__Extra", + componentId: "sc-1awywoz-1" +})(["color: blue;"]); +const helper = styled.isStyledComponent(Tag); +const owned = styled.hasOwnProperty('div'); diff --git a/test/fixtures/import-alias-unresolved-key/.babelrc b/test/fixtures/import-alias-unresolved-key/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/import-alias-unresolved-key/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/import-alias-unresolved-key/code.js b/test/fixtures/import-alias-unresolved-key/code.js new file mode 100644 index 00000000..234ebbb9 --- /dev/null +++ b/test/fixtures/import-alias-unresolved-key/code.js @@ -0,0 +1,8 @@ +import { styled as s } from 'styled-components' +import { ThemeProvider as themeKey } from 'styled-components' + +let dynamic = 'div' + +const Comp = s.box`color: red;` +const Skipped = s[dynamic]`color: blue;` +const SkippedImport = s[themeKey]`color: green;` diff --git a/test/fixtures/import-alias-unresolved-key/output.js b/test/fixtures/import-alias-unresolved-key/output.js new file mode 100644 index 00000000..4397d32a --- /dev/null +++ b/test/fixtures/import-alias-unresolved-key/output.js @@ -0,0 +1,9 @@ +import { styled as s } from 'styled-components'; +import { ThemeProvider as themeKey } from 'styled-components'; +let dynamic = 'div'; +const Comp = s.box.withConfig({ + displayName: "code__Comp", + componentId: "sc-raskk5-0" +})(["color: red;"]); +const Skipped = s[dynamic]`color: blue;`; +const SkippedImport = s[themeKey]`color: green;`; diff --git a/test/fixtures/import-default-as-unresolved-key/.babelrc b/test/fixtures/import-default-as-unresolved-key/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/import-default-as-unresolved-key/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/import-default-as-unresolved-key/code.js b/test/fixtures/import-default-as-unresolved-key/code.js new file mode 100644 index 00000000..7e055735 --- /dev/null +++ b/test/fixtures/import-default-as-unresolved-key/code.js @@ -0,0 +1,5 @@ +import { default as s } from 'styled-components' + +let dynamic = 'div' + +const Comp = s[dynamic]`color: red;` diff --git a/test/fixtures/import-default-as-unresolved-key/output.js b/test/fixtures/import-default-as-unresolved-key/output.js new file mode 100644 index 00000000..385fc827 --- /dev/null +++ b/test/fixtures/import-default-as-unresolved-key/output.js @@ -0,0 +1,6 @@ +import { default as s } from 'styled-components'; +let dynamic = 'div'; +const Comp = s[dynamic].withConfig({ + displayName: "code__Comp", + componentId: "sc-gturdc-0" +})(["color: red;"]); diff --git a/test/fixtures/import-styled-alias-unresolved-key/.babelrc b/test/fixtures/import-styled-alias-unresolved-key/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/import-styled-alias-unresolved-key/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/import-styled-alias-unresolved-key/code.js b/test/fixtures/import-styled-alias-unresolved-key/code.js new file mode 100644 index 00000000..283d3762 --- /dev/null +++ b/test/fixtures/import-styled-alias-unresolved-key/code.js @@ -0,0 +1,7 @@ +import styled, { css } from 'styled-components' + +const s2 = styled +let dynamic = 'div' + +const Comp = s2[dynamic]`color: red;` +const Helper = styled[css]`color: blue;` diff --git a/test/fixtures/import-styled-alias-unresolved-key/output.js b/test/fixtures/import-styled-alias-unresolved-key/output.js new file mode 100644 index 00000000..312c7f4c --- /dev/null +++ b/test/fixtures/import-styled-alias-unresolved-key/output.js @@ -0,0 +1,8 @@ +import styled, { css } from 'styled-components'; +const s2 = styled; +let dynamic = 'div'; +const Comp = s2[dynamic].withConfig({ + displayName: "code__Comp", + componentId: "sc-aqb1s8-0" +})(["color: red;"]); +const Helper = styled[css]`color: blue;`; diff --git a/test/fixtures/import-styled-unresolved-key/.babelrc b/test/fixtures/import-styled-unresolved-key/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/import-styled-unresolved-key/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/import-styled-unresolved-key/code.js b/test/fixtures/import-styled-unresolved-key/code.js new file mode 100644 index 00000000..28ba4e17 --- /dev/null +++ b/test/fixtures/import-styled-unresolved-key/code.js @@ -0,0 +1,5 @@ +import styled from 'styled-components' + +let dynamic = 'div' + +const Comp = styled[dynamic]`color: red;` diff --git a/test/fixtures/import-styled-unresolved-key/output.js b/test/fixtures/import-styled-unresolved-key/output.js new file mode 100644 index 00000000..0c3a0dc2 --- /dev/null +++ b/test/fixtures/import-styled-unresolved-key/output.js @@ -0,0 +1,6 @@ +import styled from 'styled-components'; +let dynamic = 'div'; +const Comp = styled[dynamic].withConfig({ + displayName: "code__Comp", + componentId: "sc-7lj0lh-0" +})(["color: red;"]); diff --git a/test/fixtures/namespace-default-non-tag-exports/.babelrc b/test/fixtures/namespace-default-non-tag-exports/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/namespace-default-non-tag-exports/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/namespace-default-non-tag-exports/code.js b/test/fixtures/namespace-default-non-tag-exports/code.js new file mode 100644 index 00000000..50b0d1b5 --- /dev/null +++ b/test/fixtures/namespace-default-non-tag-exports/code.js @@ -0,0 +1,11 @@ +import * as ns from 'styled-components' + +const Tagged = ns.default.div`color: red;` +const ok = ns.default.isStyledComponent(Tagged) +const theme = ns.default['createTheme']({}) +const viaComputed = ns['isStyledComponent'](Tagged) +const Also = ns.default['div']`color: blue;` +const helperCss = ns.default.css`color: green;` +const helperComputed = ns['css']`color: purple;` +const Proxy = ns.box`color: white;` +const ProxyDefault = ns.default.foo`color: black;` diff --git a/test/fixtures/namespace-default-non-tag-exports/output.js b/test/fixtures/namespace-default-non-tag-exports/output.js new file mode 100644 index 00000000..e3783e80 --- /dev/null +++ b/test/fixtures/namespace-default-non-tag-exports/output.js @@ -0,0 +1,22 @@ +import * as ns from 'styled-components'; +const Tagged = ns.default.div.withConfig({ + displayName: "code__Tagged", + componentId: "sc-18vnb1u-0" +})(["color: red;"]); +const ok = ns.default.isStyledComponent(Tagged); +const theme = ns.default['createTheme']({}); +const viaComputed = ns['isStyledComponent'](Tagged); +const Also = ns.default['div'].withConfig({ + displayName: "code__Also", + componentId: "sc-18vnb1u-1" +})(["color: blue;"]); +const helperCss = ns.default.css`color: green;`; +const helperComputed = ns['css']`color: purple;`; +const Proxy = ns.box.withConfig({ + displayName: "code__Proxy", + componentId: "sc-18vnb1u-2" +})(["color: white;"]); +const ProxyDefault = ns.default.foo.withConfig({ + displayName: "code__ProxyDefault", + componentId: "sc-18vnb1u-3" +})(["color: black;"]); diff --git a/test/fixtures/namespace-direct-call/.babelrc b/test/fixtures/namespace-direct-call/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/namespace-direct-call/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/namespace-direct-call/code.js b/test/fixtures/namespace-direct-call/code.js new file mode 100644 index 00000000..2ae863d1 --- /dev/null +++ b/test/fixtures/namespace-direct-call/code.js @@ -0,0 +1,15 @@ +import * as ns from 'styled-components' + +function Base(props) { + return props.children || null +} + +const Called = ns(Base)`color: red;` +const ViaComputed = ns['default'](Base)`color: blue;` +const Tag = ns.div`color: green;` +const helper = ns.isStyledComponent(Tag) +const renamed = ns.default +const Aliased = renamed.span`color: purple;` +const unknown = renamed.somethingNew`color: black;` +const Proxy = ns.box`color: white;` +const skipped = ns.css`color: yellow;` diff --git a/test/fixtures/namespace-direct-call/output.js b/test/fixtures/namespace-direct-call/output.js new file mode 100644 index 00000000..57dbac4a --- /dev/null +++ b/test/fixtures/namespace-direct-call/output.js @@ -0,0 +1,31 @@ +import * as ns from 'styled-components'; +function Base(props) { + return props.children || null; +} +const Called = ns(Base).withConfig({ + displayName: "code__Called", + componentId: "sc-p2vwnu-0" +})(["color: red;"]); +const ViaComputed = ns['default'](Base).withConfig({ + displayName: "code__ViaComputed", + componentId: "sc-p2vwnu-1" +})(["color: blue;"]); +const Tag = ns.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-p2vwnu-2" +})(["color: green;"]); +const helper = ns.isStyledComponent(Tag); +const renamed = ns.default; +const Aliased = renamed.span.withConfig({ + displayName: "code__Aliased", + componentId: "sc-p2vwnu-3" +})(["color: purple;"]); +const unknown = renamed.somethingNew.withConfig({ + displayName: "code__unknown", + componentId: "sc-p2vwnu-4" +})(["color: black;"]); +const Proxy = ns.box.withConfig({ + displayName: "code__Proxy", + componentId: "sc-p2vwnu-5" +})(["color: white;"]); +const skipped = ns.css`color: yellow;`; diff --git a/test/fixtures/namespace-unresolved-key/.babelrc b/test/fixtures/namespace-unresolved-key/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/namespace-unresolved-key/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/namespace-unresolved-key/code.js b/test/fixtures/namespace-unresolved-key/code.js new file mode 100644 index 00000000..b90c9899 --- /dev/null +++ b/test/fixtures/namespace-unresolved-key/code.js @@ -0,0 +1,5 @@ +import * as ns from 'styled-components' + +let dynamic = 'div' + +const Comp = ns[dynamic]`color: red;` diff --git a/test/fixtures/namespace-unresolved-key/output.js b/test/fixtures/namespace-unresolved-key/output.js new file mode 100644 index 00000000..10226c19 --- /dev/null +++ b/test/fixtures/namespace-unresolved-key/output.js @@ -0,0 +1,6 @@ +import * as ns from 'styled-components'; +let dynamic = 'div'; +const Comp = ns[dynamic].withConfig({ + displayName: "code__Comp", + componentId: "sc-172imzu-0" +})(["color: red;"]); diff --git a/test/fixtures/pre-transpiled-tsc-importdefault/code.js b/test/fixtures/pre-transpiled-tsc-importdefault/code.js index 211d54ca..ddfbde17 100644 --- a/test/fixtures/pre-transpiled-tsc-importdefault/code.js +++ b/test/fixtures/pre-transpiled-tsc-importdefault/code.js @@ -14,3 +14,9 @@ var Foo = styled_components_1.default.div` var Bar = (0, styled_components_1.default)('span')` color: blue; `; +var Box = styled_components_1.default.box` + color: green; +`; +var helper = styled_components_1.default.css` + color: pink; +`; diff --git a/test/fixtures/pre-transpiled-tsc-importdefault/output.js b/test/fixtures/pre-transpiled-tsc-importdefault/output.js index 088d3e95..af1b2eaa 100644 --- a/test/fixtures/pre-transpiled-tsc-importdefault/output.js +++ b/test/fixtures/pre-transpiled-tsc-importdefault/output.js @@ -21,3 +21,10 @@ var Bar = (0, styled_components_1.default)('span').withConfig({ displayName: "Bar", componentId: "sc-1km53of-1" })`color:blue;`; +var Box = styled_components_1.default.box.withConfig({ + displayName: "Box", + componentId: "sc-1km53of-2" +})`color:green;`; +var helper = styled_components_1.default.css` + color: pink; +`; diff --git a/test/fixtures/require-computed-default/.babelrc b/test/fixtures/require-computed-default/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-computed-default/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-computed-default/code.js b/test/fixtures/require-computed-default/code.js new file mode 100644 index 00000000..1d5d7d36 --- /dev/null +++ b/test/fixtures/require-computed-default/code.js @@ -0,0 +1,9 @@ +const sc = require('styled-components') + +function Base(props) { + return props.children || null +} + +const Tagged = sc['default'].div`color: red;` +const Called = sc['default'](Base)`color: blue;` +const ObjectCalled = sc['default'](Base)({ color: 'green' }) diff --git a/test/fixtures/require-computed-default/output.js b/test/fixtures/require-computed-default/output.js new file mode 100644 index 00000000..b0f8f81b --- /dev/null +++ b/test/fixtures/require-computed-default/output.js @@ -0,0 +1,18 @@ +const sc = require('styled-components'); +function Base(props) { + return props.children || null; +} +const Tagged = sc['default'].div.withConfig({ + displayName: "code__Tagged", + componentId: "sc-nrtf10-0" +})(["color: red;"]); +const Called = sc['default'](Base).withConfig({ + displayName: "code__Called", + componentId: "sc-nrtf10-1" +})(["color: blue;"]); +const ObjectCalled = sc['default'](Base).withConfig({ + displayName: "code__ObjectCalled", + componentId: "sc-nrtf10-2" +})({ + color: 'green' +}); diff --git a/test/fixtures/require-computed-static-key/.babelrc b/test/fixtures/require-computed-static-key/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-computed-static-key/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-computed-static-key/code.js b/test/fixtures/require-computed-static-key/code.js new file mode 100644 index 00000000..ebec05ab --- /dev/null +++ b/test/fixtures/require-computed-static-key/code.js @@ -0,0 +1,21 @@ +const sc = require('styled-components') + +const name = 'css' +const alias = name +const tag = 'box' +const css = 'div' +let dynamic = 'css' +const bag = { key: 'css' } +const { key: fromBag } = bag + +const Skipped = sc[name]`color: red;` +const SkippedAlias = sc[alias]`color: blue;` +const SkippedTpl = sc[`css`]`color: green;` +const SkippedDefault = sc.default[name]`color: orange;` +const ViaDiv = sc[css]`color: purple;` +const Tag = sc[tag]`color: pink;` +// A key that does not fold to a string stays plain on a require binding. +const Dynamic = sc[dynamic]`color: black;` +const FromBag = sc[fromBag]`color: white;` +const FoldedHelper = sc[`${'css'}`]`color: teal;` +const FoldedTag = sc[`${'span'}`]`color: gray;` diff --git a/test/fixtures/require-computed-static-key/output.js b/test/fixtures/require-computed-static-key/output.js new file mode 100644 index 00000000..ba637591 --- /dev/null +++ b/test/fixtures/require-computed-static-key/output.js @@ -0,0 +1,32 @@ +const sc = require('styled-components'); +const name = 'css'; +const alias = name; +const tag = 'box'; +const css = 'div'; +let dynamic = 'css'; +const bag = { + key: 'css' +}; +const { + key: fromBag +} = bag; +const Skipped = sc[name]`color: red;`; +const SkippedAlias = sc[alias]`color: blue;`; +const SkippedTpl = sc[`css`]`color: green;`; +const SkippedDefault = sc.default[name]`color: orange;`; +const ViaDiv = sc[css].withConfig({ + displayName: "code__ViaDiv", + componentId: "sc-1v8m2kx-0" +})(["color: purple;"]); +const Tag = sc[tag].withConfig({ + displayName: "code__Tag", + componentId: "sc-1v8m2kx-1" +})(["color: pink;"]); +// A key that does not fold to a string stays plain on a require binding. +const Dynamic = sc[dynamic]`color: black;`; +const FromBag = sc[fromBag]`color: white;`; +const FoldedHelper = sc[`${'css'}`]`color: teal;`; +const FoldedTag = sc[`${'span'}`].withConfig({ + displayName: "code__FoldedTag", + componentId: "sc-1v8m2kx-2" +})(["color: gray;"]); diff --git a/test/fixtures/require-computed-variable-key/.babelrc b/test/fixtures/require-computed-variable-key/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-computed-variable-key/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-computed-variable-key/code.js b/test/fixtures/require-computed-variable-key/code.js new file mode 100644 index 00000000..07af4df2 --- /dev/null +++ b/test/fixtures/require-computed-variable-key/code.js @@ -0,0 +1,4 @@ +const sc = require('styled-components') +const css = 'div' + +const Comp = sc.default[css]`color: red;` diff --git a/test/fixtures/require-computed-variable-key/output.js b/test/fixtures/require-computed-variable-key/output.js new file mode 100644 index 00000000..6742fdab --- /dev/null +++ b/test/fixtures/require-computed-variable-key/output.js @@ -0,0 +1,6 @@ +const sc = require('styled-components'); +const css = 'div'; +const Comp = sc.default[css].withConfig({ + displayName: "code__Comp", + componentId: "sc-wgr5zl-0" +})(["color: red;"]); diff --git a/test/fixtures/require-default-alias/.babelrc b/test/fixtures/require-default-alias/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-default-alias/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-default-alias/code.js b/test/fixtures/require-default-alias/code.js new file mode 100644 index 00000000..7c224c5a --- /dev/null +++ b/test/fixtures/require-default-alias/code.js @@ -0,0 +1,6 @@ +const sc = require('styled-components') +const styled = sc.default + +const Tag = styled.div`color: red;` +const helper = styled.isStyledComponent(Tag) +const unknown = styled.somethingNew`color: blue;` diff --git a/test/fixtures/require-default-alias/output.js b/test/fixtures/require-default-alias/output.js new file mode 100644 index 00000000..08b0cc41 --- /dev/null +++ b/test/fixtures/require-default-alias/output.js @@ -0,0 +1,11 @@ +const sc = require('styled-components'); +const styled = sc.default; +const Tag = styled.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-oqdthy-0" +})(["color: red;"]); +const helper = styled.isStyledComponent(Tag); +const unknown = styled.somethingNew.withConfig({ + displayName: "code__unknown", + componentId: "sc-oqdthy-1" +})(["color: blue;"]); diff --git a/test/fixtures/require-default-binding/.babelrc b/test/fixtures/require-default-binding/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-default-binding/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-default-binding/code.js b/test/fixtures/require-default-binding/code.js new file mode 100644 index 00000000..c0778400 --- /dev/null +++ b/test/fixtures/require-default-binding/code.js @@ -0,0 +1,11 @@ +const styled = require('styled-components').default +const viaComputed = require('styled-components')['default'] + +function Base(props) { + return props.children || null +} + +const Tag = styled.div`color: red;` +const Called = viaComputed(Base)`color: blue;` +const helper = styled.isStyledComponent(Tag) +const unknown = styled.somethingNew`color: green;` diff --git a/test/fixtures/require-default-binding/output.js b/test/fixtures/require-default-binding/output.js new file mode 100644 index 00000000..a3eea8cb --- /dev/null +++ b/test/fixtures/require-default-binding/output.js @@ -0,0 +1,18 @@ +const styled = require('styled-components').default; +const viaComputed = require('styled-components')['default']; +function Base(props) { + return props.children || null; +} +const Tag = styled.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-1odpxj7-0" +})(["color: red;"]); +const Called = viaComputed(Base).withConfig({ + displayName: "code__Called", + componentId: "sc-1odpxj7-1" +})(["color: blue;"]); +const helper = styled.isStyledComponent(Tag); +const unknown = styled.somethingNew.withConfig({ + displayName: "code__unknown", + componentId: "sc-1odpxj7-2" +})(["color: green;"]); diff --git a/test/fixtures/require-non-tag-exports-styled-name/.babelrc b/test/fixtures/require-non-tag-exports-styled-name/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-non-tag-exports-styled-name/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-non-tag-exports-styled-name/code.js b/test/fixtures/require-non-tag-exports-styled-name/code.js new file mode 100644 index 00000000..1a6bd5e2 --- /dev/null +++ b/test/fixtures/require-non-tag-exports-styled-name/code.js @@ -0,0 +1,18 @@ +const styled = require('styled-components') + +const x = () => null +const ok = styled.isStyledComponent(x) +const theme = styled.createTheme({}) +const extracted = styled.extractCSS() +const Tag = styled.div`color: red;` +const Box = styled.box`color: blue;` +const Foo = styled.foo`color: green;` +const Filter = styled.feNotAFilter`color: orange;` +const Marquee = styled.marquee`color: purple;` +const helperCss = styled.css`color: pink;` +const helperKeyframes = styled.keyframes`from { opacity: 0; } to { opacity: 1; }` +const helperGlobal = styled.createGlobalStyle`body { margin: 0; }` +const helperComputed = styled['createGlobalStyle']`body { margin: 0; }` +const owned = styled.hasOwnProperty('div') +const text = String(styled.toString()) +const helperDefaultCss = styled.default.css`color: black;` diff --git a/test/fixtures/require-non-tag-exports-styled-name/output.js b/test/fixtures/require-non-tag-exports-styled-name/output.js new file mode 100644 index 00000000..1db64ea6 --- /dev/null +++ b/test/fixtures/require-non-tag-exports-styled-name/output.js @@ -0,0 +1,32 @@ +const styled = require('styled-components'); +const x = () => null; +const ok = styled.isStyledComponent(x); +const theme = styled.createTheme({}); +const extracted = styled.extractCSS(); +const Tag = styled.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-w432we-0" +})(["color: red;"]); +const Box = styled.box.withConfig({ + displayName: "code__Box", + componentId: "sc-w432we-1" +})(["color: blue;"]); +const Foo = styled.foo.withConfig({ + displayName: "code__Foo", + componentId: "sc-w432we-2" +})(["color: green;"]); +const Filter = styled.feNotAFilter.withConfig({ + displayName: "code__Filter", + componentId: "sc-w432we-3" +})(["color: orange;"]); +const Marquee = styled.marquee.withConfig({ + displayName: "code__Marquee", + componentId: "sc-w432we-4" +})(["color: purple;"]); +const helperCss = styled.css`color: pink;`; +const helperKeyframes = styled.keyframes`from { opacity: 0; } to { opacity: 1; }`; +const helperGlobal = styled.createGlobalStyle`body { margin: 0; }`; +const helperComputed = styled['createGlobalStyle']`body { margin: 0; }`; +const owned = styled.hasOwnProperty('div'); +const text = String(styled.toString()); +const helperDefaultCss = styled.default.css`color: black;`; diff --git a/test/fixtures/require-non-tag-exports/.babelrc b/test/fixtures/require-non-tag-exports/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-non-tag-exports/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-non-tag-exports/code.js b/test/fixtures/require-non-tag-exports/code.js new file mode 100644 index 00000000..faa0ecfb --- /dev/null +++ b/test/fixtures/require-non-tag-exports/code.js @@ -0,0 +1,21 @@ +const sc = require('styled-components') + +const A = sc.div`color: red;` +const B = sc.default.div`color: blue;` +const ok = sc.isStyledComponent(A) +const theme = sc.createTheme({}) +const extracted = sc.extractCSS() +const sheet = sc.toStyleSheet() +const factory = sc.styled(A) +const viaDefault = sc.default.isStyledComponent(A) +const viaComputed = sc['isStyledComponent'](A) +const themeComputed = sc.default['createTheme']({}) +const C = sc['div']`color: green;` +const D = sc.default['div']`color: orange;` +const helperCss = sc['css']`color: purple;` +const helperDefaultCss = sc.default.css`color: pink;` +const helperKeyframes = sc['keyframes']`from { opacity: 0; } to { opacity: 1; }` +const Box = sc.box`color: white;` +const Foo = sc.default.foo`color: black;` +const Filter = sc['feNotAFilter']`color: teal;` +const Marquee = sc.default['marquee']`color: gray;` diff --git a/test/fixtures/require-non-tag-exports/output.js b/test/fixtures/require-non-tag-exports/output.js new file mode 100644 index 00000000..3f4da1a8 --- /dev/null +++ b/test/fixtures/require-non-tag-exports/output.js @@ -0,0 +1,44 @@ +const sc = require('styled-components'); +const A = sc.div.withConfig({ + displayName: "code__A", + componentId: "sc-c84c7x-0" +})(["color: red;"]); +const B = sc.default.div.withConfig({ + displayName: "code__B", + componentId: "sc-c84c7x-1" +})(["color: blue;"]); +const ok = sc.isStyledComponent(A); +const theme = sc.createTheme({}); +const extracted = sc.extractCSS(); +const sheet = sc.toStyleSheet(); +const factory = sc.styled(A); +const viaDefault = sc.default.isStyledComponent(A); +const viaComputed = sc['isStyledComponent'](A); +const themeComputed = sc.default['createTheme']({}); +const C = sc['div'].withConfig({ + displayName: "code__C", + componentId: "sc-c84c7x-2" +})(["color: green;"]); +const D = sc.default['div'].withConfig({ + displayName: "code__D", + componentId: "sc-c84c7x-3" +})(["color: orange;"]); +const helperCss = sc['css']`color: purple;`; +const helperDefaultCss = sc.default.css`color: pink;`; +const helperKeyframes = sc['keyframes']`from { opacity: 0; } to { opacity: 1; }`; +const Box = sc.box.withConfig({ + displayName: "code__Box", + componentId: "sc-c84c7x-4" +})(["color: white;"]); +const Foo = sc.default.foo.withConfig({ + displayName: "code__Foo", + componentId: "sc-c84c7x-5" +})(["color: black;"]); +const Filter = sc['feNotAFilter'].withConfig({ + displayName: "code__Filter", + componentId: "sc-c84c7x-6" +})(["color: teal;"]); +const Marquee = sc.default['marquee'].withConfig({ + displayName: "code__Marquee", + componentId: "sc-c84c7x-7" +})(["color: gray;"]); diff --git a/test/fixtures/require-object-methods/.babelrc b/test/fixtures/require-object-methods/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-object-methods/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-object-methods/code.js b/test/fixtures/require-object-methods/code.js new file mode 100644 index 00000000..64794742 --- /dev/null +++ b/test/fixtures/require-object-methods/code.js @@ -0,0 +1,15 @@ +const sc = require('styled-components') + +const Comp = sc.default.div`color: red;` +const has = sc.hasOwnProperty('css') +const text = String(sc.toString()) +const Lookup = sc.__lookupGetter__`x` +const LookupSet = sc.__lookupSetter__`x` +const Proto = sc.__proto__`x` +const Bound = sc.bind`x` +const Applied = sc.apply`x` +const Called = sc.call`x` +const Len = sc.length`x` +const Named = sc.name`x` +const Args = sc.arguments`x` +const Caller = sc.caller`x` diff --git a/test/fixtures/require-object-methods/output.js b/test/fixtures/require-object-methods/output.js new file mode 100644 index 00000000..c238a37b --- /dev/null +++ b/test/fixtures/require-object-methods/output.js @@ -0,0 +1,17 @@ +const sc = require('styled-components'); +const Comp = sc.default.div.withConfig({ + displayName: "code__Comp", + componentId: "sc-n5x1im-0" +})(["color: red;"]); +const has = sc.hasOwnProperty('css'); +const text = String(sc.toString()); +const Lookup = sc.__lookupGetter__`x`; +const LookupSet = sc.__lookupSetter__`x`; +const Proto = sc.__proto__`x`; +const Bound = sc.bind`x`; +const Applied = sc.apply`x`; +const Called = sc.call`x`; +const Len = sc.length`x`; +const Named = sc.name`x`; +const Args = sc.arguments`x`; +const Caller = sc.caller`x`; diff --git a/test/fixtures/require-reassign-before/.babelrc b/test/fixtures/require-reassign-before/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-reassign-before/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-reassign-before/code.js b/test/fixtures/require-reassign-before/code.js new file mode 100644 index 00000000..4bd8e05f --- /dev/null +++ b/test/fixtures/require-reassign-before/code.js @@ -0,0 +1,13 @@ +let sc = require('styled-components') + +const Before = sc.div`color: red;` + +// The function can run after `sc` is replaced, so this stays plain. +function writtenAbove() { + return sc.span`color: blue;` +} + +sc = { div: s => () => null, span: s => () => null } + +const After = sc.div`color: green;` +const Late = writtenAbove() diff --git a/test/fixtures/require-reassign-before/output.js b/test/fixtures/require-reassign-before/output.js new file mode 100644 index 00000000..293815d3 --- /dev/null +++ b/test/fixtures/require-reassign-before/output.js @@ -0,0 +1,16 @@ +let sc = require('styled-components'); +const Before = sc.div.withConfig({ + displayName: "code__Before", + componentId: "sc-m6xnle-0" +})(["color: red;"]); + +// The function can run after `sc` is replaced, so this stays plain. +function writtenAbove() { + return sc.span`color: blue;`; +} +sc = { + div: s => () => null, + span: s => () => null +}; +const After = sc.div`color: green;`; +const Late = writtenAbove(); diff --git a/test/fixtures/require-reassign-loop/.babelrc b/test/fixtures/require-reassign-loop/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-reassign-loop/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-reassign-loop/code.js b/test/fixtures/require-reassign-loop/code.js new file mode 100644 index 00000000..8bd96778 --- /dev/null +++ b/test/fixtures/require-reassign-loop/code.js @@ -0,0 +1,32 @@ +let sc = require('styled-components') + +const First = sc.div`color: red;` + +for (let i = 0; i < 2; i++) { + const Comp = sc.div`color: blue;` + sc = { div: s => () => null } +} + +while (false) { + const InWhile = sc.div`color: blue;` + sc = { div: s => () => null } +} + +do { + const InDo = sc.div`color: blue;` + sc = { div: s => () => null } +} while (false) + +const list = [1] +for (const item of list) { + const InOf = sc.div`color: blue;` + sc = { div: s => () => null } +} + +const bag = { a: 1 } +for (const key in bag) { + const InIn = sc.div`color: blue;` + sc = { div: s => () => null } +} + +const After = sc.div`color: green;` diff --git a/test/fixtures/require-reassign-loop/output.js b/test/fixtures/require-reassign-loop/output.js new file mode 100644 index 00000000..05bc8670 --- /dev/null +++ b/test/fixtures/require-reassign-loop/output.js @@ -0,0 +1,40 @@ +let sc = require('styled-components'); +const First = sc.div.withConfig({ + displayName: "code__First", + componentId: "sc-yjtl8g-0" +})(["color: red;"]); +for (let i = 0; i < 2; i++) { + const Comp = sc.div`color: blue;`; + sc = { + div: s => () => null + }; +} +while (false) { + const InWhile = sc.div`color: blue;`; + sc = { + div: s => () => null + }; +} +do { + const InDo = sc.div`color: blue;`; + sc = { + div: s => () => null + }; +} while (false); +const list = [1]; +for (const item of list) { + const InOf = sc.div`color: blue;`; + sc = { + div: s => () => null + }; +} +const bag = { + a: 1 +}; +for (const key in bag) { + const InIn = sc.div`color: blue;`; + sc = { + div: s => () => null + }; +} +const After = sc.div`color: green;`; diff --git a/test/fixtures/require-reassign/.babelrc b/test/fixtures/require-reassign/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-reassign/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-reassign/code.js b/test/fixtures/require-reassign/code.js new file mode 100644 index 00000000..2208d172 --- /dev/null +++ b/test/fixtures/require-reassign/code.js @@ -0,0 +1,7 @@ +const real = require('styled-components') +let sc = require('styled-components') + +sc = { div: s => () => null } + +const Comp = sc.div`color: red;` +const Real = real.default.div`color: blue;` diff --git a/test/fixtures/require-reassign/output.js b/test/fixtures/require-reassign/output.js new file mode 100644 index 00000000..f35a4b4c --- /dev/null +++ b/test/fixtures/require-reassign/output.js @@ -0,0 +1,10 @@ +const real = require('styled-components'); +let sc = require('styled-components'); +sc = { + div: s => () => null +}; +const Comp = sc.div`color: red;`; +const Real = real.default.div.withConfig({ + displayName: "code__Real", + componentId: "sc-1awls2t-0" +})(["color: blue;"]); diff --git a/test/fixtures/require-shadow-local/.babelrc b/test/fixtures/require-shadow-local/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-shadow-local/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-shadow-local/code.js b/test/fixtures/require-shadow-local/code.js new file mode 100644 index 00000000..11b7ab0a --- /dev/null +++ b/test/fixtures/require-shadow-local/code.js @@ -0,0 +1,15 @@ +const sc = require('styled-components') + +function make() { + const sc = { div: s => () => null } + return sc.div`color: red;` +} + +function other() { + const sc = { box: s => () => null } + return sc.box`color: blue;` +} + +const Comp = make() +const Other = other() +const Real = sc.default.div`color: green;` diff --git a/test/fixtures/require-shadow-local/output.js b/test/fixtures/require-shadow-local/output.js new file mode 100644 index 00000000..750e8184 --- /dev/null +++ b/test/fixtures/require-shadow-local/output.js @@ -0,0 +1,19 @@ +const sc = require('styled-components'); +function make() { + const sc = { + div: s => () => null + }; + return sc.div`color: red;`; +} +function other() { + const sc = { + box: s => () => null + }; + return sc.box`color: blue;`; +} +const Comp = make(); +const Other = other(); +const Real = sc.default.div.withConfig({ + displayName: "code__Real", + componentId: "sc-1tv6n6c-0" +})(["color: green;"]); diff --git a/test/fixtures/require-shadow-param/.babelrc b/test/fixtures/require-shadow-param/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-shadow-param/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-shadow-param/code.js b/test/fixtures/require-shadow-param/code.js new file mode 100644 index 00000000..d1139971 --- /dev/null +++ b/test/fixtures/require-shadow-param/code.js @@ -0,0 +1,8 @@ +const sc = require('styled-components') + +function make(sc) { + return sc.div`color: red;` +} + +const Comp = make({ div: s => () => null }) +const Real = sc.default.div`color: blue;` diff --git a/test/fixtures/require-shadow-param/output.js b/test/fixtures/require-shadow-param/output.js new file mode 100644 index 00000000..44e2e34c --- /dev/null +++ b/test/fixtures/require-shadow-param/output.js @@ -0,0 +1,11 @@ +const sc = require('styled-components'); +function make(sc) { + return sc.div`color: red;`; +} +const Comp = make({ + div: s => () => null +}); +const Real = sc.default.div.withConfig({ + displayName: "code__Real", + componentId: "sc-1xotko2-0" +})(["color: blue;"]); diff --git a/test/fixtures/require-styled-name-unresolved-key/.babelrc b/test/fixtures/require-styled-name-unresolved-key/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-styled-name-unresolved-key/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-styled-name-unresolved-key/code.js b/test/fixtures/require-styled-name-unresolved-key/code.js new file mode 100644 index 00000000..4c16ed76 --- /dev/null +++ b/test/fixtures/require-styled-name-unresolved-key/code.js @@ -0,0 +1,5 @@ +const styled = require('styled-components') + +let dynamic = 'div' + +const Comp = styled[dynamic]`color: red;` diff --git a/test/fixtures/require-styled-name-unresolved-key/output.js b/test/fixtures/require-styled-name-unresolved-key/output.js new file mode 100644 index 00000000..16c365c1 --- /dev/null +++ b/test/fixtures/require-styled-name-unresolved-key/output.js @@ -0,0 +1,6 @@ +const styled = require('styled-components'); +let dynamic = 'div'; +const Comp = styled[dynamic].withConfig({ + displayName: "code__Comp", + componentId: "sc-r7gp37-0" +})(["color: red;"]); diff --git a/test/fixtures/require-unknown-export/.babelrc b/test/fixtures/require-unknown-export/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-unknown-export/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-unknown-export/code.js b/test/fixtures/require-unknown-export/code.js new file mode 100644 index 00000000..29d0778b --- /dev/null +++ b/test/fixtures/require-unknown-export/code.js @@ -0,0 +1,9 @@ +const sc = require('styled-components') + +function Base(props) { + return props.children || null +} + +const Comp = sc.default.div`color: red;` +const n = typeof sc.somethingNew === 'function' ? sc.somethingNew(Base) : null +const Ghost = sc.somethingNew`color: blue;` diff --git a/test/fixtures/require-unknown-export/output.js b/test/fixtures/require-unknown-export/output.js new file mode 100644 index 00000000..6903c39d --- /dev/null +++ b/test/fixtures/require-unknown-export/output.js @@ -0,0 +1,16 @@ +const sc = require('styled-components'); +function Base(props) { + return props.children || null; +} +const Comp = sc.default.div.withConfig({ + displayName: "code__Comp", + componentId: "sc-9r4q5l-0" +})(["color: red;"]); +const n = typeof sc.somethingNew === 'function' ? sc.somethingNew.withConfig({ + displayName: "code__n", + componentId: "sc-9r4q5l-1" +})(Base) : null; +const Ghost = sc.somethingNew.withConfig({ + displayName: "code__Ghost", + componentId: "sc-9r4q5l-2" +})(["color: blue;"]); diff --git a/test/fixtures/require-unrelated-styled-binding/.babelrc b/test/fixtures/require-unrelated-styled-binding/.babelrc new file mode 100644 index 00000000..080e6ee1 --- /dev/null +++ b/test/fixtures/require-unrelated-styled-binding/.babelrc @@ -0,0 +1,15 @@ +{ + "sourceType": "script", + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/require-unrelated-styled-binding/code.js b/test/fixtures/require-unrelated-styled-binding/code.js new file mode 100644 index 00000000..9419bc3e --- /dev/null +++ b/test/fixtures/require-unrelated-styled-binding/code.js @@ -0,0 +1,5 @@ +const sc = require('styled-components') +const styled = { box: s => () => null } + +const Comp = sc.default.div`color: red;` +const Other = styled.box`color: blue;` diff --git a/test/fixtures/require-unrelated-styled-binding/output.js b/test/fixtures/require-unrelated-styled-binding/output.js new file mode 100644 index 00000000..e56ede6f --- /dev/null +++ b/test/fixtures/require-unrelated-styled-binding/output.js @@ -0,0 +1,9 @@ +const sc = require('styled-components'); +const styled = { + box: s => () => null +}; +const Comp = sc.default.div.withConfig({ + displayName: "code__Comp", + componentId: "sc-1rj7i4r-0" +})(["color: red;"]); +const Other = styled.box`color: blue;`; diff --git a/test/fixtures/string-named-styled-import/.babelrc b/test/fixtures/string-named-styled-import/.babelrc new file mode 100644 index 00000000..4f8903d2 --- /dev/null +++ b/test/fixtures/string-named-styled-import/.babelrc @@ -0,0 +1,14 @@ +{ + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/string-named-styled-import/code.js b/test/fixtures/string-named-styled-import/code.js new file mode 100644 index 00000000..0ce44cb2 --- /dev/null +++ b/test/fixtures/string-named-styled-import/code.js @@ -0,0 +1,5 @@ +import { 'styled' as s } from 'styled-components' + +const Tag = s.div`color: red;` +const helper = s.isStyledComponent(Tag) +const unknown = s.somethingNew`color: blue;` diff --git a/test/fixtures/string-named-styled-import/output.js b/test/fixtures/string-named-styled-import/output.js new file mode 100644 index 00000000..c15ea720 --- /dev/null +++ b/test/fixtures/string-named-styled-import/output.js @@ -0,0 +1,10 @@ +import { 'styled' as s } from 'styled-components'; +const Tag = s.div.withConfig({ + displayName: "code__Tag", + componentId: "sc-2s7ou2-0" +})(["color: red;"]); +const helper = s.isStyledComponent(Tag); +const unknown = s.somethingNew.withConfig({ + displayName: "code__unknown", + componentId: "sc-2s7ou2-1" +})(["color: blue;"]); diff --git a/test/fixtures/styled-alias-ts-assertion/.babelrc b/test/fixtures/styled-alias-ts-assertion/.babelrc new file mode 100644 index 00000000..2349af8b --- /dev/null +++ b/test/fixtures/styled-alias-ts-assertion/.babelrc @@ -0,0 +1,17 @@ +{ + "parserOpts": { + "plugins": ["typescript"] + }, + "plugins": [ + [ + "../../../src", + { + "displayName": true, + "fileName": true, + "ssr": true, + "transpileTemplateLiterals": true, + "minify": false + } + ] + ] +} diff --git a/test/fixtures/styled-alias-ts-assertion/code.js b/test/fixtures/styled-alias-ts-assertion/code.js new file mode 100644 index 00000000..00964925 --- /dev/null +++ b/test/fixtures/styled-alias-ts-assertion/code.js @@ -0,0 +1,8 @@ +import baseStyled from 'styled-components' + +const styled = baseStyled as any +const satisfied = baseStyled satisfies any + +const Foo = styled.div`color: red;` +const Bar = satisfied.span`color: blue;` +const helper = styled.isStyledComponent(Foo) diff --git a/test/fixtures/styled-alias-ts-assertion/output.js b/test/fixtures/styled-alias-ts-assertion/output.js new file mode 100644 index 00000000..eb5dfa06 --- /dev/null +++ b/test/fixtures/styled-alias-ts-assertion/output.js @@ -0,0 +1,12 @@ +import baseStyled from 'styled-components'; +const styled = baseStyled as any; +const satisfied = baseStyled satisfies any; +const Foo = styled.div.withConfig({ + displayName: "code__Foo", + componentId: "sc-o52rri-0" +})(["color: red;"]); +const Bar = satisfied.span.withConfig({ + displayName: "code__Bar", + componentId: "sc-o52rri-1" +})(["color: blue;"]); +const helper = styled.isStyledComponent(Foo);