Skip to content

Commit 5427b9d

Browse files
authored
Emit types (#9)
* Improve type emits * Fix wrapper * Update test-case * Improve transform * Even better
1 parent b7b241b commit 5427b9d

22 files changed

Lines changed: 1298 additions & 355 deletions

src/__tests__/transform.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,61 @@ export const x = 1;
413413
});
414414
});
415415

416+
describe("JS/Flow transforms (no type emits)", () => {
417+
it("should not emit TypeScript types/annotations when parsing Flow (.jsx)", () => {
418+
const source = `
419+
// @flow
420+
import styled from "styled-components";
421+
422+
const Button = styled.button.withConfig({
423+
shouldForwardProp: (prop) => prop !== "foo",
424+
})\`
425+
color: red;
426+
\`;
427+
428+
export const App = () => <Button foo disabled>Click</Button>;
429+
`;
430+
const out = applyTransform(
431+
transform,
432+
{ adapter: fixtureAdapter },
433+
{ source, path: "plain-js-flow.jsx" },
434+
{ parser: "flow" },
435+
);
436+
437+
expect(out).toContain("function Button(props)");
438+
expect(out).not.toMatch(/\bimport\s+type\b/);
439+
expect(out).not.toMatch(/\btype\s+ButtonProps\b/);
440+
expect(out).not.toMatch(/props:\s*ButtonProps/);
441+
});
442+
443+
it("should not emit TypeScript types/annotations when transforming plain JS (.js)", () => {
444+
const source = `
445+
import styled from "styled-components";
446+
447+
const Card = styled.div.withConfig({
448+
shouldForwardProp: (prop) => prop !== "foo",
449+
})\`
450+
padding: 16px;
451+
\`;
452+
453+
export function App() {
454+
return <Card foo>Hi</Card>;
455+
}
456+
`;
457+
const out = applyTransform(
458+
transform,
459+
{ adapter: fixtureAdapter },
460+
{ source, path: "plain-js.js" },
461+
{ parser: "babel" },
462+
);
463+
464+
expect(out).toContain("function Card(props)");
465+
expect(out).not.toMatch(/\bimport\s+type\b/);
466+
expect(out).not.toMatch(/\btype\s+CardProps\b/);
467+
expect(out).not.toMatch(/props:\s*CardProps/);
468+
});
469+
});
470+
416471
describe("splitVariantsResolvedValue safety", () => {
417472
it("should not emit empty variant styles when adapter returns an unparseable expression for one branch", () => {
418473
const source = `

src/internal/collect-styled-decls.ts

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function collectStyledDecls(args: {
5656

5757
const fillFromObject = (obj: any) => {
5858
for (const prop of obj.properties ?? []) {
59-
if (!prop || prop.type !== "ObjectProperty") {
59+
if (!prop || (prop.type !== "ObjectProperty" && prop.type !== "Property")) {
6060
continue;
6161
}
6262
const key =
@@ -131,7 +131,7 @@ export function collectStyledDecls(args: {
131131
return undefined;
132132
}
133133
const prop = (arg0.properties ?? []).find((p: any) => {
134-
if (!p || p.type !== "ObjectProperty") {
134+
if (!p || (p.type !== "ObjectProperty" && p.type !== "Property")) {
135135
return false;
136136
}
137137
if (p.key?.type === "Identifier") {
@@ -251,10 +251,9 @@ export function collectStyledDecls(args: {
251251
if (!arg0 || arg0.type !== "ObjectExpression") {
252252
return undefined;
253253
}
254-
let displayName: string | undefined;
255254
let componentId: string | undefined;
256255
for (const p of arg0.properties ?? []) {
257-
if (!p || p.type !== "ObjectProperty") {
256+
if (!p || (p.type !== "ObjectProperty" && p.type !== "Property")) {
258257
continue;
259258
}
260259
const key =
@@ -276,20 +275,83 @@ export function collectStyledDecls(args: {
276275
if (!val) {
277276
continue;
278277
}
279-
if (key === "displayName") {
280-
displayName = val;
281-
}
282278
if (key === "componentId") {
283279
componentId = val;
284280
}
285281
}
286-
if (!displayName && !componentId) {
282+
if (!componentId) {
287283
return undefined;
288284
}
289-
return {
290-
...(displayName ? { displayName } : {}),
291-
...(componentId ? { componentId } : {}),
285+
return { componentId };
286+
};
287+
288+
/**
289+
* Unwrap TS generic instantiation wrappers and capture the first type argument, e.g.:
290+
* - styled.button<ButtonProps>`...`
291+
* - styled(Component)<CardProps>`...`
292+
*
293+
* Babel/Recast may represent this as:
294+
* - TSInstantiationExpression { expression, typeParameters }
295+
* - or an expression node with `.typeParameters` / `.typeArguments`
296+
*/
297+
const unwrapTypeInstantiation = (expr: any): { expr: any; propsType: any } => {
298+
let cur = expr;
299+
let propsType: any;
300+
301+
const readFirstTypeArg = (tp: any): any => {
302+
if (!tp) {
303+
return undefined;
304+
}
305+
const arr =
306+
(Array.isArray(tp.params) ? tp.params : null) ??
307+
(Array.isArray(tp.parameters) ? tp.parameters : null) ??
308+
(Array.isArray(tp.typeParameters) ? tp.typeParameters : null);
309+
return Array.isArray(arr) && arr.length > 0 ? arr[0] : undefined;
292310
};
311+
312+
// Prefer TSInstantiationExpression wrapper (Babel)
313+
while (cur && cur.type === "TSInstantiationExpression") {
314+
if (!propsType) {
315+
propsType = readFirstTypeArg(cur.typeParameters);
316+
}
317+
cur = cur.expression;
318+
}
319+
320+
// Some parsers attach type params directly to CallExpression/MemberExpression/etc.
321+
if (!propsType) {
322+
propsType =
323+
readFirstTypeArg(cur?.typeParameters) ?? readFirstTypeArg(cur?.typeArguments) ?? undefined;
324+
}
325+
326+
// If we consumed type params from a direct attachment, strip them from the expr so downstream
327+
// tag/callee matching is stable.
328+
if (cur && (cur.typeParameters || cur.typeArguments)) {
329+
try {
330+
delete cur.typeParameters;
331+
delete cur.typeArguments;
332+
} catch {
333+
// ignore (non-extensible nodes)
334+
}
335+
}
336+
337+
return { expr: cur, propsType };
338+
};
339+
340+
// Some parsers attach generic type args to the TaggedTemplateExpression itself.
341+
// (e.g. `styled.div.withConfig(...)<Props>\`...\`` where `<Props>` ends up on the tag wrapper.)
342+
const readFirstTypeArgFromNode = (node: any): any => {
343+
if (!node) {
344+
return undefined;
345+
}
346+
const tp = (node as any).typeParameters ?? (node as any).typeArguments ?? undefined;
347+
if (!tp) {
348+
return undefined;
349+
}
350+
const arr =
351+
(Array.isArray(tp.params) ? tp.params : null) ??
352+
(Array.isArray(tp.parameters) ? tp.parameters : null) ??
353+
(Array.isArray(tp.typeParameters) ? tp.typeParameters : null);
354+
return Array.isArray(arr) && arr.length > 0 ? arr[0] : undefined;
293355
};
294356

295357
/**
@@ -391,7 +453,18 @@ export function collectStyledDecls(args: {
391453
const leadingComments = getLeadingComments(p);
392454
const placementHints = getPlacementHints(p);
393455

394-
const tag = init.tag;
456+
let { expr: tag, propsType } = unwrapTypeInstantiation(init.tag);
457+
if (!propsType) {
458+
propsType = readFirstTypeArgFromNode(init);
459+
if (propsType) {
460+
try {
461+
delete (init as any).typeParameters;
462+
delete (init as any).typeArguments;
463+
} catch {
464+
// ignore
465+
}
466+
}
467+
}
395468
// styled.h1
396469
if (
397470
tag.type === "MemberExpression" &&
@@ -418,6 +491,7 @@ export function collectStyledDecls(args: {
418491
rules,
419492
templateExpressions: parsed.slots.map((s) => s.expression),
420493
rawCss: parsed.rawCss,
494+
...(propsType ? { propsType } : {}),
421495
...(leadingComments ? { leadingComments } : {}),
422496
});
423497
return;
@@ -466,6 +540,7 @@ export function collectStyledDecls(args: {
466540
...(attrsInfo ? { attrsInfo } : {}),
467541
...(shouldForwardProp ? { shouldForwardProp } : {}),
468542
...(withConfigMeta ? { withConfig: withConfigMeta } : {}),
543+
...(propsType ? { propsType } : {}),
469544
...(leadingComments ? { leadingComments } : {}),
470545
});
471546
return;
@@ -499,6 +574,7 @@ export function collectStyledDecls(args: {
499574
rules,
500575
templateExpressions: parsed.slots.map((s) => s.expression),
501576
rawCss: parsed.rawCss,
577+
...(propsType ? { propsType } : {}),
502578
...(leadingComments ? { leadingComments } : {}),
503579
});
504580
}
@@ -538,6 +614,7 @@ export function collectStyledDecls(args: {
538614
rawCss: parsed.rawCss,
539615
...(shouldForwardProp ? { shouldForwardProp } : {}),
540616
...(withConfigMeta ? { withConfig: withConfigMeta } : {}),
617+
...(propsType ? { propsType } : {}),
541618
...(leadingComments ? { leadingComments } : {}),
542619
});
543620
}
@@ -580,6 +657,7 @@ export function collectStyledDecls(args: {
580657
rawCss: parsed.rawCss,
581658
...(shouldForwardProp ? { shouldForwardProp } : {}),
582659
...(withConfigMeta ? { withConfig: withConfigMeta } : {}),
660+
...(propsType ? { propsType } : {}),
583661
...(leadingComments ? { leadingComments } : {}),
584662
});
585663
}

0 commit comments

Comments
 (0)