@spectrabrain/eds 0.3.1 → 0.6.0
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/AI.md +4 -4
- package/AI_CONTEXT.json +4 -4
- package/CHANGELOG.md +28 -0
- package/CONSUMER_AGENT.md +27 -26
- package/FRONTEND_CONVENTIONS.md +2 -2
- package/QUICKSTART.md +5 -5
- package/README.md +9 -10
- package/TAILWIND_V4.md +4 -6
- package/TOKEN_CONTEXT.md +2 -18
- package/consumer/align-plan.mjs +4 -10
- package/consumer/align.mjs +5 -3
- package/consumer/cli.mjs +6 -6
- package/consumer/css-root.mjs +1 -1
- package/consumer/generate-tailwind.mjs +0 -30
- package/consumer/init.mjs +10 -11
- package/consumer/managed-block.mjs +9 -3
- package/consumer/register-scripts.mjs +39 -37
- package/consumer/scan-classes.mjs +9 -19
- package/consumer/sync.mjs +24 -11
- package/consumer/update-agent-context.mjs +2 -2
- package/consumer-postinstall.mjs +73 -15
- package/dist/components/Button/README.md +2 -4
- package/dist/components/Button/spec.json +6208 -5
- package/dist/index.d.mts +6289 -5
- package/dist/index.d.ts +6289 -5
- package/dist/index.js +6284 -381
- package/dist/index.mjs +6285 -382
- package/dist/tokens.css +0 -30
- package/dist/tokens.d.mts +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +0 -300
- package/dist/tokens.json +0 -360
- package/dist/tokens.mjs +0 -300
- package/llms.txt +2 -2
- package/package.json +3 -2
package/consumer/css-root.mjs
CHANGED
|
@@ -149,7 +149,7 @@ export function findTailwindRoot(
|
|
|
149
149
|
"Could not identify one Tailwind root CSS file (found " + found.length + ": " + found.join(", ") + "). " +
|
|
150
150
|
"Ask the user which of these is the app's Tailwind root, then run " + tailwindRootEnvironmentVariable + "=<that file> " +
|
|
151
151
|
runnerCommand(packageManager, "sync:eds") + " once. sync records the choice in package.json as \"" + consumerConfigField +
|
|
152
|
-
"\": { \"" + tailwindRootConfigKey + "\": \"<that file>\" } so doctor and
|
|
152
|
+
"\": { \"" + tailwindRootConfigKey + "\": \"<that file>\" } so doctor and sync reuse it.",
|
|
153
153
|
2,
|
|
154
154
|
);
|
|
155
155
|
}
|
|
@@ -11,10 +11,6 @@ const propertyNames = [
|
|
|
11
11
|
|
|
12
12
|
export const fontSansStyleName = "body1";
|
|
13
13
|
|
|
14
|
-
function safeTailwindAlias(name) {
|
|
15
|
-
return "eds-" + name.replace(/\./g, "-");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
function cssVariableForToken(manifest, name) {
|
|
19
15
|
const cssVariable = manifest.tokens?.[name]?.$extensions?.eds?.cssVariable;
|
|
20
16
|
if (typeof cssVariable === "string") {
|
|
@@ -24,27 +20,9 @@ function cssVariableForToken(manifest, name) {
|
|
|
24
20
|
return "--eds-" + name.replace(/\./g, "\\.");
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
function collectDimensionAliases(manifest, prefix) {
|
|
28
|
-
const aliases = new Map();
|
|
29
|
-
|
|
30
|
-
for (const name of Object.keys(manifest.tokens ?? {}).filter((entry) => entry.startsWith(prefix))) {
|
|
31
|
-
const alias = safeTailwindAlias(name);
|
|
32
|
-
const existing = aliases.get(alias);
|
|
33
|
-
if (existing !== undefined && existing !== name) {
|
|
34
|
-
throw new Error(`Tailwind alias collision: ${existing} and ${name} both map to ${alias}.`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
aliases.set(alias, name);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return [...aliases.entries()].sort(([left], [right]) => left.localeCompare(right));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
23
|
export function renderTailwindCss(manifest, version) {
|
|
44
24
|
const tokens = Object.keys(manifest.tokens ?? {});
|
|
45
25
|
const colorNames = tokens.filter((name) => name.startsWith("color-"));
|
|
46
|
-
const spacingAliases = collectDimensionAliases(manifest, "spacing-");
|
|
47
|
-
const radiusAliases = collectDimensionAliases(manifest, "radius-");
|
|
48
26
|
const typography = new Map();
|
|
49
27
|
|
|
50
28
|
for (const name of tokens.filter((entry) => entry.startsWith("typography-"))) {
|
|
@@ -74,12 +52,6 @@ export function renderTailwindCss(manifest, version) {
|
|
|
74
52
|
const colorAliases = colorNames
|
|
75
53
|
.map((name) => ` --color-eds-${name.slice("color-".length)}: var(${cssVariableForToken(manifest, name)});`)
|
|
76
54
|
.join("\n");
|
|
77
|
-
const spacingThemeAliases = spacingAliases
|
|
78
|
-
.map(([alias, name]) => ` --spacing-${alias}: var(${cssVariableForToken(manifest, name)});`)
|
|
79
|
-
.join("\n");
|
|
80
|
-
const radiusThemeAliases = radiusAliases
|
|
81
|
-
.map(([alias, name]) => ` --radius-${alias}: var(${cssVariableForToken(manifest, name)});`)
|
|
82
|
-
.join("\n");
|
|
83
55
|
// Last line of the @theme block: Tailwind's default font family becomes the EDS body font.
|
|
84
56
|
const fontSans = ` --font-sans: var(--eds-typography-${fontSansStyleName}-fontFamily);`;
|
|
85
57
|
const typographyUtilities = [...typography.keys()].sort().map((styleName) => [
|
|
@@ -96,8 +68,6 @@ export function renderTailwindCss(manifest, version) {
|
|
|
96
68
|
`/* Generated from @spectrabrain/eds@${version}. Do not edit manually. */`,
|
|
97
69
|
"@theme inline {",
|
|
98
70
|
colorAliases,
|
|
99
|
-
spacingThemeAliases,
|
|
100
|
-
radiusThemeAliases,
|
|
101
71
|
fontSans,
|
|
102
72
|
"}",
|
|
103
73
|
"",
|
package/consumer/init.mjs
CHANGED
|
@@ -77,7 +77,7 @@ export function renderCiSnippet(target, packageManager) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// npm 12+ blocks dependency install scripts unless package.json allows them. A name-only entry keeps
|
|
80
|
-
// the EDS postinstall
|
|
80
|
+
// the EDS postinstall available for script maintenance and first-sync bootstrap.
|
|
81
81
|
function writeAllowScripts(consumerRoot, { log, dryRun }) {
|
|
82
82
|
const packagePath = path.join(consumerRoot, "package.json");
|
|
83
83
|
const source = fs.readFileSync(packagePath, "utf8");
|
|
@@ -98,7 +98,7 @@ function writeAllowScripts(consumerRoot, { log, dryRun }) {
|
|
|
98
98
|
}
|
|
99
99
|
consumerPackage[allowScriptsField] = { ...(existing ?? {}), [packageName]: true };
|
|
100
100
|
writeConsumerPackageJson(packagePath, source, consumerPackage);
|
|
101
|
-
log("[EDS] wrote " + allowScriptsField + "[\"" + packageName + "\"] = true to package.json so npm " + minimumAllowScriptsNpmMajor + "+
|
|
101
|
+
log("[EDS] wrote " + allowScriptsField + "[\"" + packageName + "\"] = true to package.json so npm " + minimumAllowScriptsNpmMajor + "+ can run the EDS postinstall on later installs.");
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -113,24 +113,23 @@ function printInstallScriptGuidance(consumerRoot, packageManager, { log, dryRun,
|
|
|
113
113
|
}
|
|
114
114
|
} else if (major === null) {
|
|
115
115
|
log("[EDS] The npm version is unknown because init did not run through npm. If this project uses npm " + minimumAllowScriptsNpmMajor +
|
|
116
|
-
"+, run " + approveCommand + ", or rerun " + cliCommand(packageManager, "init") + " so init can
|
|
116
|
+
"+, run " + approveCommand + ", or rerun " + cliCommand(packageManager, "init") + " so init can enable the EDS postinstall.");
|
|
117
117
|
}
|
|
118
118
|
return;
|
|
119
119
|
}
|
|
120
|
-
//
|
|
121
|
-
// except npm 12+, where init records the approval itself.
|
|
120
|
+
// Install hooks are optional in package managers that require dependency build approval.
|
|
122
121
|
if (name === "pnpm") {
|
|
123
|
-
log("[EDS] pnpm:
|
|
124
|
-
|
|
122
|
+
log("[EDS] pnpm: postinstall can sync EDS files but does not persist package.json script edits. The coding agent registers them with " +
|
|
123
|
+
cliCommand(packageManager, "sync") + " at the start of its next UI task.");
|
|
125
124
|
return;
|
|
126
125
|
}
|
|
127
126
|
if (name === "bun") {
|
|
128
|
-
log("[EDS] Optional (bun): to let the EDS postinstall
|
|
127
|
+
log("[EDS] Optional (bun): to let the EDS postinstall bootstrap files and maintain scripts, add \"trustedDependencies\": [\"" +
|
|
129
128
|
packageName + "\"] to package.json. It replaces bun's default trusted list, so this is the project owner's decision.");
|
|
130
129
|
return;
|
|
131
130
|
}
|
|
132
131
|
if (name === "yarn" && berry) {
|
|
133
|
-
log("[EDS] Optional (Yarn 2+): to let the EDS postinstall
|
|
132
|
+
log("[EDS] Optional (Yarn 2+): to let the EDS postinstall bootstrap files and maintain scripts, keep enableScripts: true in .yarnrc.yml " +
|
|
134
133
|
"or add \"dependenciesMeta\": { \"" + packageName + "\": { \"built\": true } } to package.json.");
|
|
135
134
|
}
|
|
136
135
|
}
|
|
@@ -159,7 +158,7 @@ export function runInit({
|
|
|
159
158
|
1,
|
|
160
159
|
);
|
|
161
160
|
}
|
|
162
|
-
// A workspace root or a parent folder can resolve a hoisted EDS; the scripts
|
|
161
|
+
// A workspace root or a parent folder can resolve a hoisted EDS; the scripts and AGENTS.md belong to the
|
|
163
162
|
// app package that declares it.
|
|
164
163
|
const consumerPackage = readPackageJson(consumerRoot);
|
|
165
164
|
const declared = ["dependencies", "devDependencies", "optionalDependencies"].some((field) => consumerPackage?.[field]?.[packageName] !== undefined);
|
|
@@ -182,5 +181,5 @@ export function runInit({
|
|
|
182
181
|
log("[EDS] Run " + registered.syncRunCommand + " to finish setup.");
|
|
183
182
|
return 0;
|
|
184
183
|
}
|
|
185
|
-
return runSync({ consumerRoot, env, dryRun, log });
|
|
184
|
+
return runSync({ consumerRoot, env, dryRun, ensureScripts: false, log });
|
|
186
185
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { renderRunnerCommands } from "./paths.mjs";
|
|
3
|
+
import { renderRunnerCommands, runnerCommand } from "./paths.mjs";
|
|
4
4
|
|
|
5
5
|
export const beginMarker = "<!-- EDS:BEGIN -->";
|
|
6
6
|
export const endMarker = "<!-- EDS:END -->";
|
|
@@ -21,13 +21,19 @@ export function readTemplateBlock(packageRoot) {
|
|
|
21
21
|
|
|
22
22
|
// The template is written for npm; the rendered block uses the consumer's package-manager runner
|
|
23
23
|
// (pnpm, yarn, bun run) so the commands an agent reads work in that repository.
|
|
24
|
-
export function renderBlock(template, version, packageManager = null) {
|
|
24
|
+
export function renderBlock(template, version, packageManager = null, commands = null) {
|
|
25
25
|
const versionLine = "<!-- EDS:VERSION " + version + " -->";
|
|
26
26
|
const withoutVersion = template.replace(/^<!-- EDS:VERSION \S+ -->\r?\n/m, "");
|
|
27
27
|
if (!withoutVersion.startsWith(beginMarker)) {
|
|
28
28
|
throw new Error("CONSUMER_AGENT.md managed block must start with " + beginMarker + ".");
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
let body = renderRunnerCommands(withoutVersion.slice(beginMarker.length), packageManager);
|
|
31
|
+
if (commands?.sync) {
|
|
32
|
+
body = body.replaceAll(runnerCommand(packageManager, "sync:eds"), commands.sync);
|
|
33
|
+
}
|
|
34
|
+
if (commands?.doctor) {
|
|
35
|
+
body = body.replaceAll(runnerCommand(packageManager, "eds:doctor"), commands.doctor);
|
|
36
|
+
}
|
|
31
37
|
return beginMarker + "\n" + versionLine + body;
|
|
32
38
|
}
|
|
33
39
|
|
|
@@ -20,10 +20,9 @@ const legacyScripts = {
|
|
|
20
20
|
"update:eds-agent-context": [legacyRunnerCommand],
|
|
21
21
|
"generate:eds-tailwind": [legacyRunnerCommand],
|
|
22
22
|
};
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
];
|
|
23
|
+
const obsoleteDoctorHooks = new Set(["pretypecheck", "prebuild"]);
|
|
24
|
+
const doctorPrefix = doctorCommand + " && ";
|
|
25
|
+
const doctorLifecycleScripts = ["pretypecheck", "prebuild", "typecheck", "build"];
|
|
27
26
|
|
|
28
27
|
export function writeConsumerPackageJson(packagePath, source, consumerPackage) {
|
|
29
28
|
const trailingNewline = source.endsWith("\n") ? "\n" : "";
|
|
@@ -40,15 +39,22 @@ export function isCompatibleCommand(value, command) {
|
|
|
40
39
|
return new RegExp("(^|[\\s&|;(])" + escaped + "($|[\\s&|;)])").test(value);
|
|
41
40
|
}
|
|
42
41
|
|
|
42
|
+
export function canUseConsumerScript(scripts, name, command, { willRegister = false } = {}) {
|
|
43
|
+
if (isCompatibleCommand(scripts[name], command)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return willRegister && (scripts[name] === undefined || legacyScripts[name]?.includes(scripts[name]) === true);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export function registerConsumerScripts(
|
|
44
50
|
consumerRoot,
|
|
45
|
-
{ log = console.log, dryRun = false, packageManager = detectPackageManager(consumerRoot) } = {},
|
|
51
|
+
{ log = console.log, dryRun = false, logNoop = true, packageManager = detectPackageManager(consumerRoot) } = {},
|
|
46
52
|
) {
|
|
47
53
|
const packagePath = path.join(consumerRoot, "package.json");
|
|
48
54
|
const source = fs.readFileSync(packagePath, "utf8");
|
|
49
55
|
const consumerPackage = JSON.parse(source);
|
|
50
56
|
const scripts = { ...(consumerPackage.scripts ?? {}) };
|
|
51
|
-
const result = { changed: false, added: [], migrated: [], removed: [],
|
|
57
|
+
const result = { changed: false, added: [], migrated: [], removed: [], cleanedDoctorScripts: [], conflicts: [], syncRunCommand: null };
|
|
52
58
|
|
|
53
59
|
for (const [name, command] of Object.entries(requiredScripts)) {
|
|
54
60
|
if (isCompatibleCommand(scripts[name], command)) {
|
|
@@ -79,51 +85,47 @@ export function registerConsumerScripts(
|
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (isCompatibleCommand(scripts[hook.name], doctorCommand)) {
|
|
88
|
+
// Previous EDS versions ran Doctor as an npm lifecycle hook, or told Yarn Berry users to prefix these
|
|
89
|
+
// commands. Remove that automatic check while preserving any command that followed the Doctor prefix.
|
|
90
|
+
for (const name of doctorLifecycleScripts) {
|
|
91
|
+
if (obsoleteDoctorHooks.has(name) && scripts[name] === doctorCommand) {
|
|
92
|
+
delete scripts[name];
|
|
93
|
+
result.cleanedDoctorScripts.push(name);
|
|
89
94
|
continue;
|
|
90
95
|
}
|
|
91
|
-
if (scripts[
|
|
92
|
-
scripts[
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
if (typeof scripts[name] === "string" && scripts[name].startsWith(doctorPrefix)) {
|
|
97
|
+
const remainingCommand = scripts[name].slice(doctorPrefix.length).trimStart();
|
|
98
|
+
if (remainingCommand.length > 0) {
|
|
99
|
+
scripts[name] = remainingCommand;
|
|
100
|
+
result.cleanedDoctorScripts.push(name);
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
|
-
result.skippedHooks.push(hook.name);
|
|
97
|
-
log(
|
|
98
|
-
`[EDS] ${hook.name} already runs ${JSON.stringify(scripts[hook.name])}; left unchanged. Change it to ` +
|
|
99
|
-
`${JSON.stringify(doctorCommand + " && " + scripts[hook.name])} so ${doctorCommand} gates ${hook.target}.`,
|
|
100
|
-
);
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
result.changed = result.added.length > 0 || result.migrated.length > 0 || result.removed.length > 0;
|
|
104
|
-
// When sync:eds belongs to the consumer, point at the CLI instead of their script. The caller
|
|
105
|
-
//
|
|
105
|
+
result.changed = result.added.length > 0 || result.migrated.length > 0 || result.removed.length > 0 || result.cleanedDoctorScripts.length > 0;
|
|
106
|
+
// When sync:eds belongs to the consumer, point at the CLI instead of their script. The caller uses this when it
|
|
107
|
+
// cannot safely ensure that the package script has been registered.
|
|
106
108
|
result.syncRunCommand = result.conflicts.includes(syncScriptName) ? cliCommand(packageManager, "sync") : runnerCommand(packageManager, syncScriptName);
|
|
109
|
+
const changedScripts = [
|
|
110
|
+
...result.added,
|
|
111
|
+
...result.migrated,
|
|
112
|
+
...result.removed.map((name) => "removed " + name),
|
|
113
|
+
...result.cleanedDoctorScripts.map((name) => "removed Doctor from " + name),
|
|
114
|
+
];
|
|
107
115
|
if (result.changed && dryRun) {
|
|
108
|
-
log("[EDS] would update package.json scripts: " +
|
|
116
|
+
log("[EDS] would update package.json scripts: " + changedScripts.join(", "));
|
|
109
117
|
} else if (result.changed) {
|
|
110
118
|
consumerPackage.scripts = scripts;
|
|
111
119
|
writeConsumerPackageJson(packagePath, source, consumerPackage);
|
|
112
|
-
log("[EDS]
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
log("[EDS] Updated package.json scripts: " + changedScripts.join(", ") + ".");
|
|
121
|
+
if (result.cleanedDoctorScripts.length > 0) {
|
|
122
|
+
log("[EDS] Doctor runs before committing; it is no longer attached to typecheck or build.");
|
|
123
|
+
}
|
|
124
|
+
} else if (result.conflicts.length === 0 && logNoop) {
|
|
115
125
|
log("[EDS] The EDS scripts are already registered.");
|
|
116
126
|
} else {
|
|
117
127
|
log("[EDS] The EDS scripts were left unchanged.");
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
// Yarn berry (2+) does not run pre/post lifecycle hooks, so the doctor gate must be inlined by the consumer.
|
|
121
|
-
if (packageManager?.name === "yarn" && packageManager.berry && hookTargets.length > 0) {
|
|
122
|
-
log(
|
|
123
|
-
"[EDS] Yarn 2+ does not run pretypecheck/prebuild hooks. Prepend \"" + doctorCommand +
|
|
124
|
-
" && \" to your " + hookTargets.join(" and ") + " script" + (hookTargets.length > 1 ? "s" : "") +
|
|
125
|
-
" to keep the doctor gate (this tool does not edit them).",
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
130
|
return result;
|
|
129
131
|
}
|
|
@@ -10,9 +10,10 @@ const opacity = "(?:/[0-9]{1,3})?";
|
|
|
10
10
|
|
|
11
11
|
const edsColorClass = new RegExp(boundaryBefore + variantPrefix + colorUtility + "-eds-([A-Za-z0-9-]+)" + opacity + boundaryAfter, "g");
|
|
12
12
|
const edsTypographyClass = new RegExp(boundaryBefore + variantPrefix + "eds-typography-([A-Za-z0-9-]+)" + boundaryAfter, "g");
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const retiredDimensionClass = new RegExp(
|
|
14
|
+
boundaryBefore + variantPrefix + "-?[A-Za-z0-9-]+-eds-(?:spacing|radius)-[A-Za-z0-9-]+" + boundaryAfter,
|
|
15
|
+
"g",
|
|
16
|
+
);
|
|
16
17
|
const rawColorClass = new RegExp(
|
|
17
18
|
boundaryBefore + variantPrefix + colorUtility +
|
|
18
19
|
"-(?:(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}|white|black|\\[(?:#|rgba?\\(|hsla?\\(|oklch\\(|oklab\\(|color\\()[^\\]]*\\])" +
|
|
@@ -20,11 +21,10 @@ const rawColorClass = new RegExp(
|
|
|
20
21
|
"g",
|
|
21
22
|
);
|
|
22
23
|
|
|
23
|
-
//
|
|
24
|
+
// EDS publishes color and typography utilities. Spacing and radius use Tailwind CSS v4 defaults.
|
|
24
25
|
export function publicClassNames(manifest) {
|
|
25
26
|
const colors = new Set();
|
|
26
27
|
const typography = new Set();
|
|
27
|
-
const dimensions = new Set();
|
|
28
28
|
for (const name of Object.keys(manifest.tokens ?? {})) {
|
|
29
29
|
if (name.startsWith("color-")) {
|
|
30
30
|
colors.add("eds-" + name.slice("color-".length));
|
|
@@ -35,18 +35,15 @@ export function publicClassNames(manifest) {
|
|
|
35
35
|
typography.add(match[1]);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
if (name.startsWith("spacing-") || name.startsWith("radius-")) {
|
|
39
|
-
dimensions.add("eds-" + name.replace(/\./g, "-"));
|
|
40
|
-
}
|
|
41
38
|
}
|
|
42
|
-
return { colors, typography
|
|
39
|
+
return { colors, typography };
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
// files: [{ path, contents }] as readSources() lists them (the generated eds-tokens.css is not among them).
|
|
46
43
|
// unknown: eds-* classes that no public token backs (always a mistake, Tailwind silently drops them).
|
|
47
44
|
// raw: Tailwind palette, white/black and arbitrary color classes that bypass the EDS tokens (informational).
|
|
48
45
|
export function scanClassNames({ consumerRoot, manifest, files }) {
|
|
49
|
-
const { colors, typography
|
|
46
|
+
const { colors, typography } = publicClassNames(manifest);
|
|
50
47
|
const unknown = [];
|
|
51
48
|
const raw = [];
|
|
52
49
|
for (const file of files) {
|
|
@@ -70,15 +67,8 @@ export function scanClassNames({ consumerRoot, manifest, files }) {
|
|
|
70
67
|
unknown.push({ location, className: match[0] });
|
|
71
68
|
}
|
|
72
69
|
}
|
|
73
|
-
for (const match of line.matchAll(
|
|
74
|
-
|
|
75
|
-
unknown.push({ location, className: match[0] });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
for (const match of line.matchAll(edsRadiusClass)) {
|
|
79
|
-
if (match[1] !== undefined && !dimensions.has("eds-" + match[1])) {
|
|
80
|
-
unknown.push({ location, className: match[0] });
|
|
81
|
-
}
|
|
70
|
+
for (const match of line.matchAll(retiredDimensionClass)) {
|
|
71
|
+
unknown.push({ location, className: match[0] });
|
|
82
72
|
}
|
|
83
73
|
for (const match of line.matchAll(rawColorClass)) {
|
|
84
74
|
raw.push({ location, className: match[0] });
|
package/consumer/sync.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./css-root.mjs";
|
|
14
14
|
import { planTailwind } from "./generate-tailwind.mjs";
|
|
15
15
|
import { cliCommand, createExitError, detectPackageManager, generatedCssRelativePath, normalizeEol, readPackageJson, resolvePackage, runnerCommand, toPosix } from "./paths.mjs";
|
|
16
|
-
import { doctorCommand, doctorScriptName, isCompatibleCommand, writeConsumerPackageJson } from "./register-scripts.mjs";
|
|
16
|
+
import { canUseConsumerScript, doctorCommand, doctorScriptName, isCompatibleCommand, registerConsumerScripts, syncCommand, syncScriptName, writeConsumerPackageJson } from "./register-scripts.mjs";
|
|
17
17
|
import { planAgentContext, planClaudeImport, planDeliveryNotes, planGeminiImport } from "./update-agent-context.mjs";
|
|
18
18
|
|
|
19
19
|
export const syncTargets = new Set(["tailwind", "agents"]);
|
|
@@ -31,9 +31,18 @@ export function describeMissingTailwindRoot(plans) {
|
|
|
31
31
|
: "Tailwind CSS v3 detected (@tailwind directives in " + plans.tailwindV3File + "); " + skippedWiring + " " + tailwindV3Advice;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export function collectPlans({ consumerRoot, env = process.env }) {
|
|
34
|
+
export function collectPlans({ consumerRoot, env = process.env, ensureScripts = false }) {
|
|
35
35
|
const pkg = resolvePackage(consumerRoot);
|
|
36
36
|
const packageManager = detectPackageManager(consumerRoot, env);
|
|
37
|
+
const scripts = readPackageJson(consumerRoot)?.scripts ?? {};
|
|
38
|
+
const commands = {
|
|
39
|
+
sync: canUseConsumerScript(scripts, syncScriptName, syncCommand, { willRegister: ensureScripts })
|
|
40
|
+
? runnerCommand(packageManager, syncScriptName)
|
|
41
|
+
: cliCommand(packageManager, "sync"),
|
|
42
|
+
doctor: canUseConsumerScript(scripts, doctorScriptName, doctorCommand, { willRegister: ensureScripts })
|
|
43
|
+
? runnerCommand(packageManager, doctorScriptName)
|
|
44
|
+
: cliCommand(packageManager, "doctor"),
|
|
45
|
+
};
|
|
37
46
|
const sources = readSources(consumerRoot);
|
|
38
47
|
const root = findTailwindRoot(consumerRoot, env, sources, packageManager);
|
|
39
48
|
// No Tailwind root: the Tailwind wiring is skipped entirely (no generated CSS, no root edits).
|
|
@@ -44,7 +53,7 @@ export function collectPlans({ consumerRoot, env = process.env }) {
|
|
|
44
53
|
const errors = [];
|
|
45
54
|
let agents = null;
|
|
46
55
|
try {
|
|
47
|
-
agents = planAgentContext({ consumerRoot, pkg, packageManager });
|
|
56
|
+
agents = planAgentContext({ consumerRoot, pkg, packageManager, commands });
|
|
48
57
|
} catch (error) {
|
|
49
58
|
errors.push(error);
|
|
50
59
|
}
|
|
@@ -54,11 +63,11 @@ export function collectPlans({ consumerRoot, env = process.env }) {
|
|
|
54
63
|
return { consumerRoot, pkg, packageManager, sources, root, tailwindV3File, tailwind, cssRoot, legacyImports, agents, claude, gemini, deliveryNotes, errors };
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
export function planSync({ consumerRoot, env = process.env, only = null }) {
|
|
66
|
+
export function planSync({ consumerRoot, env = process.env, only = null, ensureScripts = true }) {
|
|
58
67
|
if (only !== null && !syncTargets.has(only)) {
|
|
59
68
|
throw createExitError("Unknown --only target: " + only + " (use tailwind or agents).", 2);
|
|
60
69
|
}
|
|
61
|
-
const plans = collectPlans({ consumerRoot, env });
|
|
70
|
+
const plans = collectPlans({ consumerRoot, env, ensureScripts: ensureScripts && only === null });
|
|
62
71
|
if (plans.errors.length > 0) {
|
|
63
72
|
// Same wording as the doctor line, so the CONSUMER_AGENT.md table applies to both.
|
|
64
73
|
plans.errors[0].message = "invalid: " + plans.errors[0].message;
|
|
@@ -83,7 +92,7 @@ export function planSync({ consumerRoot, env = process.env, only = null }) {
|
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
// EDS_TAILWIND_ROOT is a one-time choice: sync records it as package.json "eds": { "tailwindRoot" } so
|
|
86
|
-
//
|
|
95
|
+
// later sync and doctor runs resolve the same root without the environment variable.
|
|
87
96
|
function persistTailwindRoot(plan, { dryRun, log }) {
|
|
88
97
|
if (plan.root?.source !== "env" || plan.only === "agents") {
|
|
89
98
|
return false;
|
|
@@ -94,7 +103,7 @@ function persistTailwindRoot(plan, { dryRun, log }) {
|
|
|
94
103
|
const existing = consumerPackage[consumerConfigField];
|
|
95
104
|
if (existing !== undefined && (typeof existing !== "object" || existing === null || Array.isArray(existing))) {
|
|
96
105
|
log("[EDS] package.json \"" + consumerConfigField + "\" is not an object, so " + tailwindRootEnvironmentVariable + " was not recorded. " +
|
|
97
|
-
"With several Tailwind CSS candidates, doctor and
|
|
106
|
+
"With several Tailwind CSS candidates, doctor and sync exit 2 again. Check what uses the existing \"" +
|
|
98
107
|
consumerConfigField + "\" value and move it so \"" + consumerConfigField + "\" can be an object, or set " + tailwindRootEnvironmentVariable + " on every run.");
|
|
99
108
|
return false;
|
|
100
109
|
}
|
|
@@ -109,7 +118,7 @@ function persistTailwindRoot(plan, { dryRun, log }) {
|
|
|
109
118
|
}
|
|
110
119
|
consumerPackage[consumerConfigField] = { ...(existing ?? {}), [tailwindRootConfigKey]: value };
|
|
111
120
|
writeConsumerPackageJson(packagePath, source, consumerPackage);
|
|
112
|
-
log("[EDS] wrote " + label + " to package.json; doctor
|
|
121
|
+
log("[EDS] wrote " + label + " to package.json; doctor and sync now use this Tailwind root without " + tailwindRootEnvironmentVariable + ".");
|
|
113
122
|
return true;
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -143,13 +152,17 @@ export function applySync(plan, { dryRun = false, log = console.log } = {}) {
|
|
|
143
152
|
const doctorRun = isCompatibleCommand(scripts[doctorScriptName], doctorCommand)
|
|
144
153
|
? runnerCommand(plan.packageManager, doctorScriptName)
|
|
145
154
|
: cliCommand(plan.packageManager, "doctor");
|
|
146
|
-
log("[EDS] Synced with " + version + ". Include the files written above in your change,
|
|
155
|
+
log("[EDS] Synced with " + version + ". Include the files written above in your change; before committing, run " + doctorRun + ".");
|
|
147
156
|
}
|
|
148
157
|
return plan.writes.map((write) => write.relativePath);
|
|
149
158
|
}
|
|
150
159
|
|
|
151
|
-
export function runSync({ consumerRoot, env = process.env, only = null, dryRun = false, log = console.log }) {
|
|
152
|
-
const plan = planSync({ consumerRoot, env, only });
|
|
160
|
+
export function runSync({ consumerRoot, env = process.env, only = null, dryRun = false, ensureScripts = true, log = console.log }) {
|
|
161
|
+
const plan = planSync({ consumerRoot, env, only, ensureScripts });
|
|
162
|
+
// Keep script setup and cleanup in the same one-time task-start command, including package managers that skip postinstall.
|
|
163
|
+
if (ensureScripts && only === null) {
|
|
164
|
+
registerConsumerScripts(consumerRoot, { log, dryRun, logNoop: false, packageManager: plan.packageManager });
|
|
165
|
+
}
|
|
153
166
|
applySync(plan, { dryRun, log });
|
|
154
167
|
return 0;
|
|
155
168
|
}
|
|
@@ -17,12 +17,12 @@ export const geminiImportLine = "@./AGENTS.md";
|
|
|
17
17
|
const importPattern = /^[ \t]*@(?:\.\.?\/)?AGENTS\.md[ \t]*$/m;
|
|
18
18
|
const endMarkerText = "<!-- EDS:END -->";
|
|
19
19
|
|
|
20
|
-
export function planAgentContext({ consumerRoot, pkg, packageManager = null }) {
|
|
20
|
+
export function planAgentContext({ consumerRoot, pkg, packageManager = null, commands = null }) {
|
|
21
21
|
const filePath = path.join(consumerRoot, "AGENTS.md");
|
|
22
22
|
const before = readTextIfExists(filePath);
|
|
23
23
|
// The block is rendered with the target file's dominant line ending; a new file uses LF.
|
|
24
24
|
const eol = before === null ? "\n" : detectEol(before);
|
|
25
|
-
const block = withEol(renderBlock(readTemplateBlock(pkg.packageRoot), pkg.version, packageManager), eol);
|
|
25
|
+
const block = withEol(renderBlock(readTemplateBlock(pkg.packageRoot), pkg.version, packageManager, commands), eol);
|
|
26
26
|
const located = before === null ? null : locateBlock(before);
|
|
27
27
|
return {
|
|
28
28
|
label: "agents",
|
package/consumer-postinstall.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { locateBlock, readBlockVersion } from "./consumer/managed-block.mjs";
|
|
5
|
+
import { cliCommand, detectPackageManager, resolvePackage } from "./consumer/paths.mjs";
|
|
4
6
|
import { registerConsumerScripts } from "./consumer/register-scripts.mjs";
|
|
7
|
+
import { runSync } from "./consumer/sync.mjs";
|
|
5
8
|
|
|
6
9
|
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
7
10
|
const consumerRoot = process.env.INIT_CWD;
|
|
@@ -12,7 +15,7 @@ if (!consumerRoot || path.resolve(consumerRoot) === packageRoot) {
|
|
|
12
15
|
|
|
13
16
|
const consumerPackagePath = path.join(consumerRoot, "package.json");
|
|
14
17
|
if (!fs.existsSync(consumerPackagePath)) {
|
|
15
|
-
console.log("[EDS] Consumer package.json was not found; run
|
|
18
|
+
console.log("[EDS] Consumer package.json was not found; the coding agent should run EDS setup in the app package that depends on EDS.");
|
|
16
19
|
process.exit(0);
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -26,35 +29,90 @@ function realpathOrNull(filePath) {
|
|
|
26
29
|
|
|
27
30
|
const installedPath = realpathOrNull(path.join(consumerRoot, "node_modules", "@spectrabrain", "eds"));
|
|
28
31
|
if (installedPath === null || installedPath !== realpathOrNull(packageRoot)) {
|
|
29
|
-
console.log("[EDS] Skipping consumer setup: this
|
|
32
|
+
console.log("[EDS] Skipping consumer setup: this is not the app package's direct EDS install. The coding agent should set up EDS in the app package that depends on it.");
|
|
30
33
|
process.exit(0);
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
let consumerPackage;
|
|
37
|
+
try {
|
|
38
|
+
consumerPackage = JSON.parse(fs.readFileSync(consumerPackagePath, "utf8"));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log("[EDS] Consumer package.json could not be parsed (" + error.message + "); the coding agent should run EDS setup after fixing it.");
|
|
36
41
|
process.exit(0);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
let
|
|
44
|
+
let packageManager;
|
|
45
|
+
let packageJson;
|
|
40
46
|
try {
|
|
41
|
-
|
|
47
|
+
packageManager = detectPackageManager(consumerRoot);
|
|
48
|
+
packageJson = resolvePackage(consumerRoot);
|
|
42
49
|
} catch (error) {
|
|
43
|
-
console.log("[EDS] Consumer
|
|
50
|
+
console.log("[EDS] Consumer setup was skipped (" + (error instanceof Error ? error.message : String(error)) + ").");
|
|
44
51
|
process.exit(0);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
// there would make the root build look for src/styles/eds-tokens.css and AGENTS.md in the wrong place.
|
|
54
|
+
const initCommand = cliCommand(packageManager, "init");
|
|
49
55
|
if (consumerPackage.workspaces !== undefined) {
|
|
50
|
-
console.log("[EDS] Skipping consumer setup
|
|
56
|
+
console.log("[EDS] Skipping consumer setup at the workspaces root. The coding agent should run " + initCommand + " inside the app package that depends on EDS.");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const agentsPath = path.join(consumerRoot, "AGENTS.md");
|
|
61
|
+
let existingManagedBlock = null;
|
|
62
|
+
let markerVersion = null;
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(agentsPath)) {
|
|
65
|
+
const block = locateBlock(fs.readFileSync(agentsPath, "utf8"));
|
|
66
|
+
existingManagedBlock = block?.block ?? null;
|
|
67
|
+
markerVersion = existingManagedBlock === null ? null : readBlockVersion(existingManagedBlock);
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.log(
|
|
71
|
+
"[EDS] Initial sync was skipped (" + (error instanceof Error ? error.message : String(error)) + "); " +
|
|
72
|
+
"the coding agent should repair the EDS markers before syncing.",
|
|
73
|
+
);
|
|
51
74
|
process.exit(0);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
|
|
77
|
+
const pnpmInstall = String(process.env.npm_config_user_agent ?? "").startsWith("pnpm/");
|
|
78
|
+
if (!pnpmInstall) {
|
|
79
|
+
try {
|
|
80
|
+
registerConsumerScripts(consumerRoot, { packageManager });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.log("[EDS] Script maintenance was skipped (" + (error instanceof Error ? error.message : String(error)) + ").");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (existingManagedBlock !== null && markerVersion === null) {
|
|
87
|
+
console.log(
|
|
88
|
+
"[EDS] The existing AGENTS.md EDS block has no version marker; scripts were maintained without changing files, and the coding agent will read the installed version's CHANGELOG.md actions and sync at the start of the next UI task.",
|
|
89
|
+
);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (markerVersion !== null) {
|
|
94
|
+
if (markerVersion !== packageJson.version) {
|
|
95
|
+
console.log(
|
|
96
|
+
"[EDS] Installed EDS version " + packageJson.version + " differs from the AGENTS.md marker " + markerVersion + "; " +
|
|
97
|
+
"the coding agent will read the version's CHANGELOG.md actions and sync at the start of the next UI task.",
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
console.log("[EDS] The AGENTS.md version marker matches; the coding agent will check generated files with its next task-start sync.");
|
|
101
|
+
}
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// pnpm restores package.json after dependency lifecycle scripts, so sync its initial managed files now but let the agent
|
|
106
|
+
// register scripts with its first task-start sync. Other package managers keep both scripts and files.
|
|
55
107
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
108
|
+
runSync({ consumerRoot, ensureScripts: false });
|
|
109
|
+
if (pnpmInstall) {
|
|
110
|
+
console.log("[EDS] pnpm leaves package.json scripts to the coding agent's first task-start sync.");
|
|
111
|
+
}
|
|
112
|
+
console.log("[EDS] Initial EDS sync is ready; the managed instructions tell the coding agent when to sync on later tasks.");
|
|
58
113
|
} catch (error) {
|
|
59
|
-
console.log(
|
|
114
|
+
console.log(
|
|
115
|
+
"[EDS] Initial sync was skipped (" + (error instanceof Error ? error.message : String(error)) + "); " +
|
|
116
|
+
"the coding agent should resolve the finding and run " + cliCommand(packageManager, "sync") + ".",
|
|
117
|
+
);
|
|
60
118
|
}
|
|
@@ -65,10 +65,8 @@ Layout은 prop으로 지정하지 않고 슬롯 조합에서 추론합니다.
|
|
|
65
65
|
| `iconBoth` | `children`과 앞·뒤 아이콘을 모두 지정합니다. | 양쪽 아이콘이 모두 의미를 더하는 명확한 경우에만 사용합니다. |
|
|
66
66
|
| `iconOnly` | `children` 없이 `icon` 또는 `iconName`과 `aria-label`을 지정합니다. | |
|
|
67
67
|
|
|
68
|
-
- 이름 슬롯(`iconName`·`startIconName`·`endIconName`)은
|
|
69
|
-
-
|
|
70
|
-
- 목록 밖 아이콘은 `icon`, `startIcon`, `endIcon`에 `ReactElement`로 전달하며, 같은 슬롯에 이름과 custom element를 함께 지정하지 않습니다. `lucide-react`에서 직접 import할 때는 EDS package root `package.json`의 `dependencies["lucide-react"]`와 같은 exact 버전을 소비처 `package.json`에 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. custom lucide 아이콘은 lucide 기본 stroke를 그대로 써서 이름으로 지정한 아이콘과 맞춥니다.
|
|
71
|
-
- 목록 밖 아이콘을 `ReactElement`로 썼다면 완료 보고에 아이콘 이름과 사용 파일을 allowlist 추가 후보로 적습니다.
|
|
68
|
+
- 이름 슬롯(`iconName`·`startIconName`·`endIconName`)은 설치된 `lucide-react`의 실제 아이콘 component 이름과 alias를 모두 받습니다. `ButtonIconName`이 이 목록을 자동완성하며, 전체 이름은 `spec.json`의 `api.iconNames`에 있습니다. 잘못된 이름은 TypeScript 오류이고 런타임에도 render 오류입니다.
|
|
69
|
+
- custom 아이콘은 `icon`·`startIcon`·`endIcon`에 `ReactElement`로 전달합니다. 같은 슬롯에 이름과 custom element를 함께 지정하지 않습니다. `lucide-react`에서 직접 import할 때는 EDS package root `package.json`의 `dependencies["lucide-react"]`와 같은 exact 버전을 소비처 `package.json`에 선언합니다. pnpm처럼 엄격한 node_modules에서는 선언 없이 resolve되지 않고, 버전이 다르면 아이콘 코드가 두 벌 번들됩니다. 이름 슬롯은 설치된 아이콘 크기와 stroke를 사용합니다.
|
|
72
70
|
|
|
73
71
|
### State
|
|
74
72
|
|