@astryxdesign/cli 0.1.2-canary.ff01f6d → 0.1.3-canary.6d57c8d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -1
- package/README.md +49 -55
- package/docs/layout.doc.dense.mjs +90 -0
- package/docs/layout.doc.mjs +160 -0
- package/docs/principles.doc.dense.mjs +2 -2
- package/docs/principles.doc.mjs +4 -0
- package/docs/principles.doc.zh.mjs +2 -2
- package/package.json +7 -7
- package/src/api/blog.mjs +192 -0
- package/src/api/blog.test.mjs +144 -0
- package/src/api/doctor.mjs +3 -3
- package/src/api/index.mjs +1 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
- package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
- package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +23 -0
- package/src/codemods/transforms/v0.1.0/index.mjs +28 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
- package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
- package/src/commands/agent-docs.mjs +4 -1
- package/src/commands/blog.mjs +60 -0
- package/src/commands/build-theme.import-path.test.mjs +2 -12
- package/src/commands/build-theme.mjs +117 -0
- package/src/commands/build-theme.prose.test.mjs +5 -15
- package/src/commands/build-theme.watch.test.mjs +149 -0
- package/src/commands/ensure-core-built.mjs +120 -0
- package/src/index.mjs +1 -0
- package/src/lib/error-codes.mjs +8 -0
- package/src/lib/site.mjs +20 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
- package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
- package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
- package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
- package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
- package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
- package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
- package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
- package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
- package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
- package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
- package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
- package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
- package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
- package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
- package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
- package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
- package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
- package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
- package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
- package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
- package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
- package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
- package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
- package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
- package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
- package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
- package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
- package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
- package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
- package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
- package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
- package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
- package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
- package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
- package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
- package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
- package/templates/pages/kanban-board/page.tsx +729 -0
- package/templates/pages/kanban-board/template.doc.mjs +12 -0
- package/templates/pages/shell-side-nav/page.tsx +0 -1
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
- package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
package/src/api/blog.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Programmatic API for the blog command.
|
|
5
|
+
*
|
|
6
|
+
* The blog is read the same way any feed reader reads it: over the published
|
|
7
|
+
* RSS feed. Listing parses the feed; reading a post fetches the plaintext
|
|
8
|
+
* (.txt) alternate the feed advertises for each item. Nothing here touches the
|
|
9
|
+
* blog's source files — the CLI is just a consumer of the public feed, so the
|
|
10
|
+
* blog's structure can change freely without touching the CLI.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {AstryxError} from './error.mjs';
|
|
14
|
+
import {ERROR_CODES} from '../lib/error-codes.mjs';
|
|
15
|
+
import {SITE_URL, SITE_ORIGIN} from '../lib/site.mjs';
|
|
16
|
+
|
|
17
|
+
/** Abort a feed/post fetch that hangs, and cap how much we'll read. */
|
|
18
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
19
|
+
const MAX_BYTES = 5 * 1024 * 1024; // 5 MB — a blog feed is never larger.
|
|
20
|
+
|
|
21
|
+
const FEED_URL = new URL('/rss.xml', SITE_URL).toString();
|
|
22
|
+
|
|
23
|
+
async function fetchText(url) {
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
26
|
+
let res;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetch(url, {
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
redirect: 'follow',
|
|
31
|
+
});
|
|
32
|
+
} catch (e) {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
const reason = e.name === 'AbortError' ? 'timed out' : e.message;
|
|
35
|
+
throw new AstryxError(
|
|
36
|
+
`Could not reach ${url}: ${reason}`,
|
|
37
|
+
[],
|
|
38
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new AstryxError(
|
|
44
|
+
`Request to ${url} failed with ${res.status}`,
|
|
45
|
+
[],
|
|
46
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const body = await res.text();
|
|
50
|
+
if (body.length > MAX_BYTES) {
|
|
51
|
+
throw new AstryxError(
|
|
52
|
+
`Response from ${url} exceeded ${MAX_BYTES} bytes`,
|
|
53
|
+
[],
|
|
54
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return body;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Defense in depth: a post's plaintext URL comes from feed content. Require it
|
|
62
|
+
* to live on the canonical origin so even a tampered feed can't redirect the
|
|
63
|
+
* CLI to another host.
|
|
64
|
+
*/
|
|
65
|
+
function assertCanonicalOrigin(target) {
|
|
66
|
+
let targetOrigin;
|
|
67
|
+
try {
|
|
68
|
+
targetOrigin = new URL(target).origin;
|
|
69
|
+
} catch {
|
|
70
|
+
throw new AstryxError(
|
|
71
|
+
`Post has an invalid plaintext URL: "${target}"`,
|
|
72
|
+
[],
|
|
73
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (targetOrigin !== SITE_ORIGIN) {
|
|
77
|
+
throw new AstryxError(
|
|
78
|
+
`Refusing to fetch post text from a non-canonical origin (${targetOrigin})`,
|
|
79
|
+
[],
|
|
80
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function unescapeXml(value) {
|
|
86
|
+
return value
|
|
87
|
+
.replace(/</g, '<')
|
|
88
|
+
.replace(/>/g, '>')
|
|
89
|
+
.replace(/"/g, '"')
|
|
90
|
+
.replace(/'/g, "'")
|
|
91
|
+
.replace(/&/g, '&');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function tag(item, name) {
|
|
95
|
+
const m = item.match(new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`));
|
|
96
|
+
return m ? unescapeXml(m[1].trim()) : '';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tagAll(item, name) {
|
|
100
|
+
const out = [];
|
|
101
|
+
const re = new RegExp(`<${name}[^>]*>([\\s\\S]*?)</${name}>`, 'g');
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = re.exec(item))) out.push(unescapeXml(m[1].trim()));
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Extract the plaintext alternate href from an <item>. */
|
|
108
|
+
function textHref(item) {
|
|
109
|
+
// Match the atom:link alternate regardless of attribute order/quoting; then
|
|
110
|
+
// confirm it's the text/plain alternate before trusting the href.
|
|
111
|
+
const re = /<atom:link\b[^>]*?\/?>/g;
|
|
112
|
+
let m;
|
|
113
|
+
while ((m = re.exec(item))) {
|
|
114
|
+
const el = m[0];
|
|
115
|
+
if (/rel\s*=\s*["']alternate["']/.test(el) &&
|
|
116
|
+
/type\s*=\s*["']text\/plain["']/.test(el)) {
|
|
117
|
+
const href = el.match(/href\s*=\s*["']([^"']+)["']/);
|
|
118
|
+
if (href) return unescapeXml(href[1]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Derive a slug from a post link (last path segment). */
|
|
125
|
+
function slugFromLink(link) {
|
|
126
|
+
try {
|
|
127
|
+
const path = new URL(link).pathname.replace(/\/$/, '');
|
|
128
|
+
return path.slice(path.lastIndexOf('/') + 1);
|
|
129
|
+
} catch {
|
|
130
|
+
return link;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFeed(xml) {
|
|
135
|
+
const items = [];
|
|
136
|
+
const re = /<item>([\s\S]*?)<\/item>/g;
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(xml))) {
|
|
139
|
+
const raw = m[1];
|
|
140
|
+
const link = tag(raw, 'link');
|
|
141
|
+
items.push({
|
|
142
|
+
slug: slugFromLink(link),
|
|
143
|
+
title: tag(raw, 'title'),
|
|
144
|
+
description: tag(raw, 'description'),
|
|
145
|
+
date: tag(raw, 'pubDate'),
|
|
146
|
+
type: tag(raw, 'category'),
|
|
147
|
+
authors: tagAll(raw, 'author'),
|
|
148
|
+
link,
|
|
149
|
+
textUrl: textHref(raw),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* List posts (from the feed), or read one post (via its .txt alternate).
|
|
157
|
+
* Both envelopes carry `feedUrl` so a caller can hit the RSS feed directly.
|
|
158
|
+
* The feed is always the canonical site — there is no user-supplied URL.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} [slug]
|
|
161
|
+
* @returns {Promise<{type: string, data: unknown}>}
|
|
162
|
+
*/
|
|
163
|
+
export async function blog(slug) {
|
|
164
|
+
const xml = await fetchText(FEED_URL);
|
|
165
|
+
const posts = parseFeed(xml);
|
|
166
|
+
|
|
167
|
+
if (!slug) {
|
|
168
|
+
return {type: 'blog.list', data: {feedUrl: FEED_URL, posts}};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const normalized = slug.toLowerCase();
|
|
172
|
+
const post = posts.find(p => p.slug.toLowerCase() === normalized);
|
|
173
|
+
if (!post) {
|
|
174
|
+
throw new AstryxError(
|
|
175
|
+
`No blog post with slug "${slug}"`,
|
|
176
|
+
posts.map(p => ({name: p.slug, reason: 'available post'})),
|
|
177
|
+
ERROR_CODES.ERR_UNKNOWN_POST,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!post.textUrl) {
|
|
182
|
+
throw new AstryxError(
|
|
183
|
+
`Post "${slug}" has no plaintext alternate in the feed`,
|
|
184
|
+
[],
|
|
185
|
+
ERROR_CODES.ERR_FETCH_FAILED,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
assertCanonicalOrigin(post.textUrl);
|
|
190
|
+
const text = await fetchText(post.textUrl);
|
|
191
|
+
return {type: 'blog.detail', data: {...post, feedUrl: FEED_URL, text}};
|
|
192
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Tests for the blog API — reads the blog over the canonical RSS feed
|
|
5
|
+
* and fetches each post's plaintext (.txt) alternate. `fetch` is stubbed so
|
|
6
|
+
* the tests never hit the network. The feed origin is fixed (not
|
|
7
|
+
* user-configurable), so the tests assert the canonical URLs directly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {describe, it, expect, beforeEach, afterEach, vi} from 'vitest';
|
|
11
|
+
import {blog} from './blog.mjs';
|
|
12
|
+
import {AstryxError} from './error.mjs';
|
|
13
|
+
import {SITE_URL} from '../lib/site.mjs';
|
|
14
|
+
|
|
15
|
+
const FEED = `<?xml version="1.0" encoding="UTF-8"?>
|
|
16
|
+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
17
|
+
<channel>
|
|
18
|
+
<title>Astryx Blog</title>
|
|
19
|
+
<link>${SITE_URL}/blog</link>
|
|
20
|
+
<item>
|
|
21
|
+
<title>How Astryx works</title>
|
|
22
|
+
<link>${SITE_URL}/blog/how-astryx-works</link>
|
|
23
|
+
<guid isPermaLink="true">${SITE_URL}/blog/how-astryx-works</guid>
|
|
24
|
+
<description>Under the hood & more</description>
|
|
25
|
+
<category>engineering</category>
|
|
26
|
+
<author>cvkxx</author>
|
|
27
|
+
<author>cixzhang</author>
|
|
28
|
+
<pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate>
|
|
29
|
+
<atom:link rel="alternate" type="text/plain" href="${SITE_URL}/blog/how-astryx-works.txt" />
|
|
30
|
+
</item>
|
|
31
|
+
<item>
|
|
32
|
+
<title>Introducing Astryx</title>
|
|
33
|
+
<link>${SITE_URL}/blog/introducing-astryx</link>
|
|
34
|
+
<guid isPermaLink="true">${SITE_URL}/blog/introducing-astryx</guid>
|
|
35
|
+
<description>The launch</description>
|
|
36
|
+
<category>update</category>
|
|
37
|
+
<author>cvkxx</author>
|
|
38
|
+
<pubDate>Thu, 18 Jun 2026 00:00:00 GMT</pubDate>
|
|
39
|
+
<atom:link rel="alternate" type="text/plain" href="${SITE_URL}/blog/introducing-astryx.txt" />
|
|
40
|
+
</item>
|
|
41
|
+
</channel>
|
|
42
|
+
</rss>`;
|
|
43
|
+
|
|
44
|
+
const POST_TEXT = '# How Astryx works\n\nThe body of the post.';
|
|
45
|
+
const FEED_URL = `${SITE_URL}/rss.xml`;
|
|
46
|
+
const TXT_URL = `${SITE_URL}/blog/how-astryx-works.txt`;
|
|
47
|
+
|
|
48
|
+
/** Build a fetch stub from a url→{status,body} map. */
|
|
49
|
+
function stubFetch(routes) {
|
|
50
|
+
return vi.fn(async (url, opts) => {
|
|
51
|
+
const u = String(url);
|
|
52
|
+
const r = routes[u];
|
|
53
|
+
if (!r) return {ok: false, status: 404, text: async () => 'not found'};
|
|
54
|
+
if (r.throw) throw r.throw;
|
|
55
|
+
return {ok: r.status < 400, status: r.status, text: async () => r.body};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.stubGlobal('fetch', stubFetch({
|
|
61
|
+
[FEED_URL]: {status: 200, body: FEED},
|
|
62
|
+
[TXT_URL]: {status: 200, body: POST_TEXT},
|
|
63
|
+
[`${SITE_URL}/blog/introducing-astryx.txt`]: {status: 200, body: 'launch body'},
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
vi.unstubAllGlobals();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('blog API', () => {
|
|
72
|
+
it('lists posts parsed from the canonical feed', async () => {
|
|
73
|
+
const res = await blog();
|
|
74
|
+
expect(res.type).toBe('blog.list');
|
|
75
|
+
expect(res.data.feedUrl).toBe(FEED_URL);
|
|
76
|
+
expect(res.data.posts.map(p => p.slug)).toEqual([
|
|
77
|
+
'how-astryx-works',
|
|
78
|
+
'introducing-astryx',
|
|
79
|
+
]);
|
|
80
|
+
expect(res.data.posts[0].authors).toEqual(['cvkxx', 'cixzhang']);
|
|
81
|
+
expect(res.data.posts[0].textUrl).toBe(TXT_URL);
|
|
82
|
+
// Entity decoding works (& -> &).
|
|
83
|
+
expect(res.data.posts[0].description).toBe('Under the hood & more');
|
|
84
|
+
// The list never fetches post bodies.
|
|
85
|
+
expect(res.data.posts[0].text).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reads a post via its plaintext alternate', async () => {
|
|
89
|
+
const res = await blog('how-astryx-works');
|
|
90
|
+
expect(res.type).toBe('blog.detail');
|
|
91
|
+
expect(res.data.text).toBe(POST_TEXT);
|
|
92
|
+
expect(res.data.feedUrl).toBe(FEED_URL);
|
|
93
|
+
expect(fetch).toHaveBeenCalledWith(TXT_URL, expect.any(Object));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('is case-insensitive on the slug', async () => {
|
|
97
|
+
const res = await blog('How-Astryx-Works');
|
|
98
|
+
expect(res.data.slug).toBe('how-astryx-works');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('throws ERR_UNKNOWN_POST with suggestions for a bad slug', async () => {
|
|
102
|
+
await expect(blog('does-not-exist')).rejects.toMatchObject({
|
|
103
|
+
code: 'ERR_UNKNOWN_POST',
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
await blog('does-not-exist');
|
|
107
|
+
} catch (e) {
|
|
108
|
+
expect(e).toBeInstanceOf(AstryxError);
|
|
109
|
+
expect(Array.isArray(e.suggestions)).toBe(true);
|
|
110
|
+
expect(e.suggestions.length).toBeGreaterThan(0);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('throws ERR_FETCH_FAILED when the feed request fails', async () => {
|
|
115
|
+
vi.stubGlobal('fetch', stubFetch({
|
|
116
|
+
[FEED_URL]: {status: 500, body: 'boom'},
|
|
117
|
+
}));
|
|
118
|
+
await expect(blog()).rejects.toMatchObject({code: 'ERR_FETCH_FAILED'});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('refuses a post whose plaintext URL is on a different origin (SSRF guard)', async () => {
|
|
122
|
+
const evilFeed = FEED.replace(
|
|
123
|
+
`${SITE_URL}/blog/how-astryx-works.txt`,
|
|
124
|
+
'http://169.254.169.254/latest/meta-data',
|
|
125
|
+
);
|
|
126
|
+
vi.stubGlobal('fetch', stubFetch({
|
|
127
|
+
[FEED_URL]: {status: 200, body: evilFeed},
|
|
128
|
+
'http://169.254.169.254/latest/meta-data': {status: 200, body: 'SECRETS'},
|
|
129
|
+
}));
|
|
130
|
+
await expect(blog('how-astryx-works')).rejects.toMatchObject({
|
|
131
|
+
code: 'ERR_FETCH_FAILED',
|
|
132
|
+
});
|
|
133
|
+
// The internal host must never be fetched.
|
|
134
|
+
expect(fetch).not.toHaveBeenCalledWith(
|
|
135
|
+
'http://169.254.169.254/latest/meta-data',
|
|
136
|
+
expect.anything(),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('always reads from the canonical feed URL', async () => {
|
|
141
|
+
await blog();
|
|
142
|
+
expect(fetch).toHaveBeenCalledWith(FEED_URL, expect.any(Object));
|
|
143
|
+
});
|
|
144
|
+
});
|
package/src/api/doctor.mjs
CHANGED
|
@@ -240,7 +240,7 @@ export function checkThemes(ctx) {
|
|
|
240
240
|
label: 'Theme packages',
|
|
241
241
|
status: 'warn',
|
|
242
242
|
message: 'No @astryxdesign/theme-* packages are installed.',
|
|
243
|
-
fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set
|
|
243
|
+
fix: 'Install a theme, e.g. `npm install @astryxdesign/theme-neutral`, then import its CSS or set astryx.theme.',
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -354,7 +354,7 @@ export function checkAgentDocs(ctx) {
|
|
|
354
354
|
label: 'AI agent docs',
|
|
355
355
|
status: 'warn',
|
|
356
356
|
message: `Agent docs present (${present.join(', ')}) but no Astryx section markers found.`,
|
|
357
|
-
fix: 'Add the
|
|
357
|
+
fix: 'Add the Astryx section to your agent docs with `astryx init --features agents`.',
|
|
358
358
|
};
|
|
359
359
|
}
|
|
360
360
|
|
|
@@ -362,7 +362,7 @@ export function checkAgentDocs(ctx) {
|
|
|
362
362
|
id: 'agent-docs',
|
|
363
363
|
label: 'AI agent docs',
|
|
364
364
|
status: 'pass',
|
|
365
|
-
message: `
|
|
365
|
+
message: `Astryx agent docs section present in ${withMarkers.join(', ')}.`,
|
|
366
366
|
};
|
|
367
367
|
}
|
|
368
368
|
|
package/src/api/index.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
export {component} from './component.mjs';
|
|
23
23
|
export {docs} from './docs.mjs';
|
|
24
|
+
export {blog} from './blog.mjs';
|
|
24
25
|
export {discover} from './discover.mjs';
|
|
25
26
|
export {template} from './template.mjs';
|
|
26
27
|
export {themeAdd, listThemes} from './theme-add.mjs';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source, filePath = 'test.css') {
|
|
6
|
+
const {default: transform} = await import('../migrate-xds-css-surfaces.mjs');
|
|
7
|
+
// api.jscodeshift is unused by this CSS codemod, but the runner still
|
|
8
|
+
// passes it — mirror that shape.
|
|
9
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
10
|
+
const api = {jscodeshift, stats: () => {}, report: () => {}};
|
|
11
|
+
const file = {source, path: filePath};
|
|
12
|
+
const result = transform(file, api);
|
|
13
|
+
return result ?? source;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('migrate-xds-css-surfaces', () => {
|
|
17
|
+
it('rewrites the .xds- class-selector prefix', async () => {
|
|
18
|
+
const input = '.xds-heading { color: red; }';
|
|
19
|
+
const output = await applyTransform(input);
|
|
20
|
+
expect(output).toBe('.astryx-heading { color: red; }');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rewrites the [data-xds-theme] attribute selector', async () => {
|
|
24
|
+
const input = '[data-xds-theme="dark"] .xds-card { background: black; }';
|
|
25
|
+
const output = await applyTransform(input);
|
|
26
|
+
expect(output).toContain('[data-astryx-theme="dark"]');
|
|
27
|
+
expect(output).toContain('.astryx-card');
|
|
28
|
+
expect(output).not.toContain('xds-');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rewrites data-xds-theme-prose and data-xds-media attribute selectors', async () => {
|
|
32
|
+
const input =
|
|
33
|
+
'[data-xds-theme-prose] {} [data-xds-media="print"] {}';
|
|
34
|
+
const output = await applyTransform(input);
|
|
35
|
+
expect(output).toContain('[data-astryx-theme-prose]');
|
|
36
|
+
expect(output).toContain('[data-astryx-media="print"]');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rewrites @layer xds-theme and @layer xds-base', async () => {
|
|
40
|
+
const input = '@layer xds-theme, xds-base;\n@layer xds-theme { a { color: red; } }';
|
|
41
|
+
const output = await applyTransform(input);
|
|
42
|
+
expect(output).toContain('astryx-theme');
|
|
43
|
+
expect(output).toContain('astryx-base');
|
|
44
|
+
expect(output).not.toContain('xds-theme');
|
|
45
|
+
expect(output).not.toContain('xds-base');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('does NOT rewrite a bare "xds" in a comment or value', async () => {
|
|
49
|
+
const input = '/* xds tokens live here */\n.card { content: "xds"; }';
|
|
50
|
+
const output = await applyTransform(input);
|
|
51
|
+
expect(output).toBe(input);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('does NOT rewrite a class-like word missing the leading dot', async () => {
|
|
55
|
+
// e.g. inside a JS-ish string or a comment fragment — not a class selector.
|
|
56
|
+
const input = '/* use xds-heading in markup */';
|
|
57
|
+
const output = await applyTransform(input);
|
|
58
|
+
expect(output).toBe(input);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('leaves already-migrated CSS unchanged', async () => {
|
|
62
|
+
const input =
|
|
63
|
+
'@layer astryx-theme { [data-astryx-theme="dark"] .astryx-card {} }';
|
|
64
|
+
const output = await applyTransform(input);
|
|
65
|
+
expect(output).toBe(input);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {describe, it, expect} from 'vitest';
|
|
4
|
+
|
|
5
|
+
async function applyTransform(source, filePath = 'test.d.ts') {
|
|
6
|
+
const {default: transform} = await import('../migrate-xds-declare-module.mjs');
|
|
7
|
+
const jscodeshift = (await import('jscodeshift')).default;
|
|
8
|
+
const j = jscodeshift.withParser('tsx');
|
|
9
|
+
const api = {jscodeshift: j, stats: () => {}, report: () => {}};
|
|
10
|
+
const file = {source, path: filePath};
|
|
11
|
+
const result = transform(file, api);
|
|
12
|
+
return result ?? source;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('migrate-xds-declare-module', () => {
|
|
16
|
+
it('renames a subpath declare module', async () => {
|
|
17
|
+
const input = [
|
|
18
|
+
"declare module '@xds/core/Heading' {",
|
|
19
|
+
' export const HeadingLevelMap: Record<string, number>;',
|
|
20
|
+
'}',
|
|
21
|
+
].join('\n');
|
|
22
|
+
|
|
23
|
+
const output = await applyTransform(input);
|
|
24
|
+
expect(output).toMatch(/declare module ['"]@astryxdesign\/core\/Heading['"]/);
|
|
25
|
+
expect(output).not.toContain('@xds/core');
|
|
26
|
+
// The augmentation body identifier is untouched.
|
|
27
|
+
expect(output).toContain('HeadingLevelMap');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renames a bare @xds/core declare module', async () => {
|
|
31
|
+
const input = "declare module '@xds/core' {\n const x: number;\n}";
|
|
32
|
+
const output = await applyTransform(input);
|
|
33
|
+
expect(output).toMatch(/declare module ['"]@astryxdesign\/core['"]/);
|
|
34
|
+
expect(output).not.toContain('@xds/core');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renames @xds/lab declare module using the shared map', async () => {
|
|
38
|
+
const input = "declare module '@xds/lab/Thing' {\n const y: number;\n}";
|
|
39
|
+
const output = await applyTransform(input);
|
|
40
|
+
expect(output).toMatch(/declare module ['"]@astryxdesign\/lab\/Thing['"]/);
|
|
41
|
+
expect(output).not.toContain('@xds/lab');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('leaves non-xds declare modules alone', async () => {
|
|
45
|
+
const input = "declare module 'react' {\n const z: number;\n}";
|
|
46
|
+
const output = await applyTransform(input);
|
|
47
|
+
expect(output).toBe(input);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('leaves an already-migrated declare module unchanged', async () => {
|
|
51
|
+
const input = "declare module '@astryxdesign/core/Heading' {\n const a: number;\n}";
|
|
52
|
+
const output = await applyTransform(input);
|
|
53
|
+
expect(output).toBe(input);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not touch namespace declarations (Identifier id)', async () => {
|
|
57
|
+
const input = 'declare namespace XDSCore {\n const b: number;\n}';
|
|
58
|
+
const output = await applyTransform(input, 'test.ts');
|
|
59
|
+
expect(output).toBe(input);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -41,6 +41,29 @@ describe('v0.1.0 codemod manifest ordering', () => {
|
|
|
41
41
|
expect(entry.optional).toBeFalsy();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
it('orders declare-module and CSS codemods after migrate-xds-module-specifiers', async () => {
|
|
45
|
+
const {default: manifest} = await import('../index.mjs');
|
|
46
|
+
const names = manifest.map(entry => entry.name);
|
|
47
|
+
expect(names).toEqual([
|
|
48
|
+
'drop-xds-prefix-imports',
|
|
49
|
+
'migrate-xds-module-specifiers',
|
|
50
|
+
'migrate-xds-declare-module',
|
|
51
|
+
'migrate-xds-css-surfaces',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('marks migrate-xds-declare-module and migrate-xds-css-surfaces mandatory', async () => {
|
|
56
|
+
const {default: manifest} = await import('../index.mjs');
|
|
57
|
+
for (const name of [
|
|
58
|
+
'migrate-xds-declare-module',
|
|
59
|
+
'migrate-xds-css-surfaces',
|
|
60
|
+
]) {
|
|
61
|
+
const entry = manifest.find(e => e.name === name);
|
|
62
|
+
expect(entry, name).toBeDefined();
|
|
63
|
+
expect(entry.optional).toBeFalsy();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
44
67
|
it('un-prefixes useXDSTheme from @xds/core/theme AND renames the scope to @astryxdesign', async () => {
|
|
45
68
|
const input = [
|
|
46
69
|
"import {useXDSTheme} from '@xds/core/theme';",
|
|
@@ -14,6 +14,14 @@ import migrateXdsModuleSpecifiers, {
|
|
|
14
14
|
meta as migrateXdsModuleSpecifiersMeta,
|
|
15
15
|
} from './migrate-xds-module-specifiers.mjs';
|
|
16
16
|
|
|
17
|
+
import migrateXdsDeclareModule, {
|
|
18
|
+
meta as migrateXdsDeclareModuleMeta,
|
|
19
|
+
} from './migrate-xds-declare-module.mjs';
|
|
20
|
+
|
|
21
|
+
import migrateXdsCssSurfaces, {
|
|
22
|
+
meta as migrateXdsCssSurfacesMeta,
|
|
23
|
+
} from './migrate-xds-css-surfaces.mjs';
|
|
24
|
+
|
|
17
25
|
export default [
|
|
18
26
|
{
|
|
19
27
|
// XDS-prefix migration (P2380608025). Mandatory in v0.1.0: the release
|
|
@@ -34,4 +42,24 @@ export default [
|
|
|
34
42
|
transform: migrateXdsModuleSpecifiers,
|
|
35
43
|
meta: migrateXdsModuleSpecifiersMeta,
|
|
36
44
|
},
|
|
45
|
+
{
|
|
46
|
+
// Cleans up TypeScript `declare module "@xds/core/..."` augmentations,
|
|
47
|
+
// which reference the package by string specifier and so are dead after
|
|
48
|
+
// the scope rename. Mandatory in v0.1.0. Ordered AFTER the specifier
|
|
49
|
+
// codemod: it uses the same @xds -> @astryxdesign mapping and repairs an
|
|
50
|
+
// adjacent surface the specifier codemod does not visit (module-decl ids
|
|
51
|
+
// are not import/export/require sources).
|
|
52
|
+
name: 'migrate-xds-declare-module',
|
|
53
|
+
transform: migrateXdsDeclareModule,
|
|
54
|
+
meta: migrateXdsDeclareModuleMeta,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
// Rewrites consumer CSS surfaces broken by the v0.1.0 DOM-namespace rename
|
|
58
|
+
// (core no longer dual-emits `xds-*`): the `.xds-*` class-selector prefix,
|
|
59
|
+
// `[data-xds-*]` attribute selectors, and `@layer xds-*` layer names all
|
|
60
|
+
// become their `astryx-*` forms. Mandatory in v0.1.0.
|
|
61
|
+
name: 'migrate-xds-css-surfaces',
|
|
62
|
+
transform: migrateXdsCssSurfaces,
|
|
63
|
+
meta: migrateXdsCssSurfacesMeta,
|
|
64
|
+
},
|
|
37
65
|
];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Codemod: migrate XDS CSS surfaces to their Astryx equivalents
|
|
5
|
+
*
|
|
6
|
+
* The v0.1.0 release renamed the DOM namespace emitted by @astryxdesign/core
|
|
7
|
+
* from `xds-*` to `astryx-*` and dropped the dual-emit compatibility (core no
|
|
8
|
+
* longer emits the legacy `xds-*` classes or `data-xds-*` attributes). Any
|
|
9
|
+
* consumer CSS that targeted those surfaces is now broken.
|
|
10
|
+
*
|
|
11
|
+
* This transform performs precise, targeted string replacements on CSS/SCSS
|
|
12
|
+
* files for the documented surfaces only:
|
|
13
|
+
*
|
|
14
|
+
* .xds-* (class-name prefix in selectors — matched only
|
|
15
|
+
* after a `.` so it is a class selector)
|
|
16
|
+
* [data-xds-theme (theme attribute selector)
|
|
17
|
+
* [data-xds-theme-prose (prose-theme attribute selector)
|
|
18
|
+
* [data-xds-media (media attribute selector)
|
|
19
|
+
* @layer xds-theme (cascade layer name — incl. comma-separated
|
|
20
|
+
* @layer xds-base `@layer a, b;` statement lists and nested blocks)
|
|
21
|
+
*
|
|
22
|
+
* It deliberately does NOT blindly replace every `xds` substring: a bare
|
|
23
|
+
* `xds` in a comment, a custom-property value, or an unrelated identifier is
|
|
24
|
+
* left untouched. CSS has no meaningful AST for this class of edit, so this is
|
|
25
|
+
* a plain string transform — `api.jscodeshift` is available but unused.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const meta = {
|
|
29
|
+
title: 'Migrate .xds-* / [data-xds-*] / @layer xds-* CSS surfaces to astryx',
|
|
30
|
+
description:
|
|
31
|
+
'Rewrites the documented XDS CSS surfaces to their Astryx equivalents: ' +
|
|
32
|
+
'the `.xds-*` class-selector prefix, `[data-xds-theme]` / ' +
|
|
33
|
+
'`[data-xds-theme-prose]` / `[data-xds-media]` attribute selectors, and ' +
|
|
34
|
+
'`@layer xds-theme` / `@layer xds-base` cascade-layer names all become ' +
|
|
35
|
+
'their `astryx-*` / `data-astryx-*` forms.',
|
|
36
|
+
pr: '#3092',
|
|
37
|
+
fileExtensions: ['.css', '.scss'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// The XDS cascade-layer names we own. A `@layer` prelude can name several
|
|
41
|
+
// layers in a comma-separated list (`@layer a, b;`), so we can't anchor every
|
|
42
|
+
// occurrence to the `@layer` keyword. We instead rewrite these exact layer
|
|
43
|
+
// tokens within a `@layer ...` prelude (up to the next `{` or `;`).
|
|
44
|
+
const XDS_LAYER_NAMES = new Map([
|
|
45
|
+
['xds-theme', 'astryx-theme'],
|
|
46
|
+
['xds-base', 'astryx-base'],
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function rewriteLayerPrelude(prelude) {
|
|
50
|
+
return prelude.replace(/\bxds-(?:theme|base)\b/g, name =>
|
|
51
|
+
XDS_LAYER_NAMES.get(name) ?? name,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Ordered, non-overlapping replacements. Each pattern is intentionally narrow
|
|
56
|
+
// so we never touch a bare `xds` substring outside these surfaces.
|
|
57
|
+
const REPLACEMENTS = [
|
|
58
|
+
// Class-name prefix: only when preceded by a `.` (a class selector).
|
|
59
|
+
{re: /\.xds-/g, to: '.astryx-'},
|
|
60
|
+
// Attribute selectors: any `[data-xds-` opener (theme, theme-prose, media).
|
|
61
|
+
{re: /\[data-xds-/g, to: '[data-astryx-'},
|
|
62
|
+
// Cascade-layer preludes: `@layer <names>` up to the block `{` or `;`.
|
|
63
|
+
// Rewrite only the recognized xds-* layer tokens inside the prelude.
|
|
64
|
+
{re: /@layer\b[^{;]*/g, to: match => rewriteLayerPrelude(match)},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
export default function transformer(file /*, api */) {
|
|
68
|
+
const source = file.source;
|
|
69
|
+
if (typeof source !== 'string') return undefined;
|
|
70
|
+
|
|
71
|
+
let next = source;
|
|
72
|
+
for (const {re, to} of REPLACEMENTS) {
|
|
73
|
+
next = next.replace(re, to);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return next === source ? undefined : next;
|
|
77
|
+
}
|