Skip to content

Commit 3615940

Browse files
authored
Improve comments handling + and more (#10)
1 parent 5427b9d commit 3615940

17 files changed

Lines changed: 1108 additions & 174 deletions

src/internal/collect-styled-decls.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,34 @@ export function collectStyledDecls(args: {
367367
if (parentPath.node.declarations?.length !== 1) {
368368
return;
369369
}
370-
const comments = parentPath.node.comments ?? parentPath.node.leadingComments;
370+
// Comments may be attached to the VariableDeclaration itself OR to an enclosing
371+
// ExportNamedDeclaration (e.g. `export const X = ...`) depending on the parser/printer.
372+
let comments = parentPath.node.comments ?? parentPath.node.leadingComments;
373+
if (!comments || !Array.isArray(comments) || comments.length === 0) {
374+
// In practice, jscodeshift paths sometimes insert intermediate "VariableDeclaration" paths
375+
// between a declarator and the `ExportNamedDeclaration`, so walk up to find the export wrapper.
376+
let cur = parentPath.parentPath;
377+
while (cur && cur.node) {
378+
if (cur.node.type === "ExportNamedDeclaration") {
379+
const decl = cur.node.declaration;
380+
if (decl?.type === "VariableDeclaration") {
381+
// Only preserve exported-declaration leading comments when they are fixture "Bug N:" narrative
382+
// blocks. Other exported-decl comments are often intentionally dropped in current fixtures.
383+
const exportComments = (cur.node.comments ?? cur.node.leadingComments) as any;
384+
const hasBugNarrative =
385+
Array.isArray(exportComments) &&
386+
exportComments.some((c: any) =>
387+
/^Bug\s+\d+[a-zA-Z]?\s*:/.test(
388+
(typeof c?.value === "string" ? String(c.value) : "").trim(),
389+
),
390+
);
391+
comments = hasBugNarrative ? exportComments : undefined;
392+
break;
393+
}
394+
}
395+
cur = cur.parentPath;
396+
}
397+
}
371398
if (!comments || !Array.isArray(comments) || comments.length === 0) {
372399
return;
373400
}
@@ -579,6 +606,40 @@ export function collectStyledDecls(args: {
579606
});
580607
}
581608

609+
// styled("tagName") - intrinsic element with string argument (without chaining)
610+
if (
611+
tag.type === "CallExpression" &&
612+
tag.callee.type === "Identifier" &&
613+
tag.callee.name === styledDefaultImport &&
614+
tag.arguments.length === 1 &&
615+
(tag.arguments[0]?.type === "StringLiteral" ||
616+
(tag.arguments[0]?.type === "Literal" &&
617+
typeof (tag.arguments[0] as any).value === "string"))
618+
) {
619+
const localName = id.name;
620+
const arg0 = tag.arguments[0] as any;
621+
const tagName = arg0.type === "StringLiteral" ? arg0.value : arg0.value;
622+
const template = init.quasi;
623+
const parsed = parseStyledTemplateLiteral(template);
624+
const rules = normalizeStylisAstToIR(parsed.stylisAst, parsed.slots, {
625+
rawCss: parsed.rawCss,
626+
});
627+
if (hasUniversalSelectorInRules(rules)) {
628+
noteUniversalSelector(template);
629+
}
630+
631+
styledDecls.push({
632+
...placementHints,
633+
localName,
634+
base: { kind: "intrinsic", tagName },
635+
styleKey: toStyleKey(localName),
636+
rules,
637+
templateExpressions: parsed.slots.map((s) => s.expression),
638+
rawCss: parsed.rawCss,
639+
...(leadingComments ? { leadingComments } : {}),
640+
});
641+
}
642+
582643
// styled(Base).withConfig(...)`...`
583644
if (
584645
tag.type === "CallExpression" &&

src/internal/emit-styles.ts

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ export function emitStylesAndImports(args: {
5454
addHeaderComments((n as any)?.comments);
5555
}
5656

57+
const isBugComment = (c: any): boolean => {
58+
const v = typeof c?.value === "string" ? String(c.value).trim() : "";
59+
// Treat "Bug N:" fixture narrative comments as file-level comments, not style-property docs.
60+
return /^Bug\s+\d+[a-zA-Z]?\s*:/.test(v);
61+
};
62+
63+
const splitBugNarrativeLeadingComments = (
64+
comments: unknown,
65+
): { narrative: any[]; property: any[] } => {
66+
if (!Array.isArray(comments) || comments.length === 0) {
67+
return { narrative: [], property: [] };
68+
}
69+
let bugIdx = -1;
70+
for (let i = 0; i < comments.length; i++) {
71+
if (isBugComment((comments as any[])[i])) {
72+
bugIdx = i;
73+
break;
74+
}
75+
}
76+
if (bugIdx < 0) {
77+
return { narrative: [], property: comments as any[] };
78+
}
79+
// Include the full contiguous comment array from the first "Bug ..." comment onward.
80+
// This captures follow-up lines like:
81+
// // Bug N: ...
82+
// // more context...
83+
return {
84+
narrative: (comments as any[]).slice(bugIdx),
85+
property: (comments as any[]).slice(0, bugIdx),
86+
};
87+
};
88+
5789
// Remove styled-components import(s), but preserve any named imports that are still referenced
5890
// (e.g. useTheme, withTheme, ThemeProvider if they're still used in the code)
5991
const preservedSpecifiers: string[] = [];
@@ -150,6 +182,65 @@ export function emitStylesAndImports(args: {
150182
}
151183
}
152184

185+
// Preserve leading comments that sit on the *styled declaration statement* itself.
186+
//
187+
// These often include fixture-level explanations (e.g. "Bug N: ...") that are attached to
188+
// `export const X = styled...` declarations. Since we remove those declarations later in the
189+
// transform, we need to migrate their leading comments onto a node that remains (the emitted
190+
// `const styles = stylex.create(...)` declaration is the best anchor).
191+
//
192+
// Important: we avoid duplicating comments that are already being preserved as style property
193+
// comments via `StyledDecl.leadingComments`.
194+
const propCommentKeys = new Set<string>();
195+
for (const decl of styledDecls) {
196+
const cs = (decl as any).leadingComments;
197+
if (!Array.isArray(cs)) {
198+
continue;
199+
}
200+
const { property } = splitBugNarrativeLeadingComments(cs);
201+
for (const c of property) {
202+
const key = `${(c as any)?.type ?? "Comment"}:${String((c as any)?.value ?? "").trim()}`;
203+
propCommentKeys.add(key);
204+
}
205+
}
206+
207+
const migratedStyledDeclLeadingComments: any[] = [];
208+
// Prefer sourcing these from `StyledDecl.leadingComments` (captured from the original styled
209+
// declaration VariableDeclaration). This is more reliable than reading statement comments
210+
// because some parsers/printers split multi-line comment runs across different comment arrays.
211+
const declsByLoc = [...styledDecls].sort((a, b) => {
212+
const al = ((a as any)?.loc?.start?.line ?? Number.POSITIVE_INFINITY) as number;
213+
const bl = ((b as any)?.loc?.start?.line ?? Number.POSITIVE_INFINITY) as number;
214+
return al - bl;
215+
});
216+
for (const d of declsByLoc) {
217+
const cs = (d as any).leadingComments;
218+
if (!Array.isArray(cs) || cs.length === 0) {
219+
continue;
220+
}
221+
const { narrative } = splitBugNarrativeLeadingComments(cs);
222+
if (narrative.length === 0) {
223+
continue;
224+
}
225+
for (const c of narrative) {
226+
const key = `${(c as any)?.type ?? "Comment"}:${String((c as any)?.value ?? "").trim()}`;
227+
if (propCommentKeys.has(key)) {
228+
continue;
229+
}
230+
if ((c as any)?.leading === false) {
231+
continue;
232+
}
233+
// Clone the comment node so we can safely reattach it even if the original
234+
// declaration node (that initially owned it) is later removed from the AST.
235+
migratedStyledDeclLeadingComments.push({
236+
...(c as any),
237+
leading: true,
238+
trailing: false,
239+
});
240+
}
241+
break;
242+
}
243+
153244
// Inject resolver-provided imports (from adapter.resolveValue calls).
154245
{
155246
const toModuleSpecifier = (from: ImportSource): string => {
@@ -265,7 +356,12 @@ export function emitStylesAndImports(args: {
265356
const styleKeyToComments = new Map<string, any[]>();
266357
for (const decl of styledDecls) {
267358
if (decl.leadingComments && decl.leadingComments.length > 0) {
268-
styleKeyToComments.set(decl.styleKey, decl.leadingComments);
359+
// Avoid attaching "Bug N:" narrative comments to a specific style property inside
360+
// `stylex.create({ ... })` — those belong above the `styles` declaration instead.
361+
const { property } = splitBugNarrativeLeadingComments(decl.leadingComments);
362+
if (property.length > 0) {
363+
styleKeyToComments.set(decl.styleKey, property);
364+
}
269365
}
270366
}
271367

@@ -298,6 +394,28 @@ export function emitStylesAndImports(args: {
298394
),
299395
]);
300396

397+
// Attach migrated leading comments (from the first styled declaration) to `styles`.
398+
if (migratedStyledDeclLeadingComments.length > 0) {
399+
const merged = [
400+
...migratedStyledDeclLeadingComments,
401+
...(Array.isArray((stylesDecl as any).leadingComments)
402+
? (stylesDecl as any).leadingComments
403+
: []),
404+
...(Array.isArray((stylesDecl as any).comments) ? (stylesDecl as any).comments : []),
405+
] as any[];
406+
const seen = new Set<string>();
407+
const deduped = merged.filter((c) => {
408+
const key = `${(c as any)?.type ?? "Comment"}:${String((c as any)?.value ?? "").trim()}`;
409+
if (seen.has(key)) {
410+
return false;
411+
}
412+
seen.add(key);
413+
return true;
414+
});
415+
(stylesDecl as any).leadingComments = deduped;
416+
(stylesDecl as any).comments = deduped;
417+
}
418+
301419
// If styles reference identifiers declared later in the file (e.g. string-interpolation fixture),
302420
// insert `styles` after the last such declaration to satisfy StyleX evaluation order.
303421
const referencedIdents = new Set<string>();

0 commit comments

Comments
 (0)