@spectrabrain/eds 0.1.0 → 0.2.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 +13 -63
- package/AI_CONTEXT.json +26 -95
- package/CHANGELOG.md +70 -4
- package/CONSUMER_AGENT.md +86 -164
- package/FRONTEND_CONVENTIONS.md +252 -0
- package/QUICKSTART.md +24 -29
- package/README.md +29 -73
- package/TAILWIND_V4.md +52 -184
- package/TOKEN_CONTEXT.md +79 -36
- package/consumer/align/templates/components.json +21 -0
- package/consumer/align/templates/env.mock +1 -0
- package/consumer/align/templates/eslint.config.js +35 -0
- package/consumer/align/templates/package.scripts.json +10 -0
- package/consumer/align/templates/prettierignore +2 -0
- package/consumer/align/templates/prettierrc.json +7 -0
- package/consumer/align/templates/src/app/App.tsx +16 -0
- package/consumer/align/templates/src/app/main.tsx +27 -0
- package/consumer/align/templates/src/app/providers/ErrorBoundary.tsx +58 -0
- package/consumer/align/templates/src/app/providers/ErrorHandler.tsx +29 -0
- package/consumer/align/templates/src/app/router/index.tsx +23 -0
- package/consumer/align/templates/src/app/styles/index.css +2 -0
- package/consumer/align/templates/src/mocks/browser.ts +5 -0
- package/consumer/align/templates/src/mocks/handlers.ts +4 -0
- package/consumer/align/templates/src/shared/api/index.ts +23 -0
- package/consumer/align/templates/src/shared/api/queryClient.ts +24 -0
- package/consumer/align/templates/src/shared/lib/eventBus.ts +44 -0
- package/consumer/align/templates/src/shared/lib/utils/index.ts +6 -0
- package/consumer/align/templates/src/vite-env.d.ts +2 -0
- package/consumer/align/templates/tsconfig.json +22 -0
- package/consumer/align/templates/vite.config.babel.ts +36 -0
- package/consumer/align/templates/vite.config.rolldown.ts +35 -0
- package/consumer/align-analysis.mjs +1159 -0
- package/consumer/align-plan.mjs +377 -0
- package/consumer/align-rules.mjs +473 -0
- package/consumer/align.mjs +401 -0
- package/consumer/cli.mjs +90 -40
- package/consumer/css-root.mjs +24 -4
- package/consumer/doctor.mjs +47 -21
- package/consumer/generate-tailwind.mjs +41 -1
- package/consumer/init.mjs +39 -25
- package/consumer/managed-block.mjs +11 -5
- package/consumer/paths.mjs +69 -14
- package/consumer/register-scripts.mjs +17 -12
- package/consumer/scan-classes.mjs +24 -5
- package/consumer/sync.mjs +35 -13
- package/consumer/update-agent-context.mjs +63 -7
- package/consumer-postinstall.mjs +8 -7
- package/dist/components/Button/README.md +66 -95
- package/dist/components/Button/spec.json +19 -138
- package/dist/components.css +102 -366
- package/dist/index.d.mts +20 -18
- package/dist/index.d.ts +20 -18
- package/dist/index.js +301 -1
- package/dist/index.mjs +301 -1
- package/dist/tokens.css +30 -0
- package/dist/tokens.d.mts +7 -3
- package/dist/tokens.d.ts +7 -3
- package/dist/tokens.js +301 -1
- package/dist/tokens.json +360 -0
- package/dist/tokens.mjs +301 -1
- package/llms.txt +20 -14
- package/package.json +3 -1
package/consumer/doctor.mjs
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { findPretendardReferences, planPackageImports, stylesImportLine, fontsImportLine } from "./css-root.mjs";
|
|
2
2
|
import { readGeneratedVersion } from "./generate-tailwind.mjs";
|
|
3
3
|
import { legacyVersionLabel } from "./managed-block.mjs";
|
|
4
|
-
import { generatedCssRelativePath, normalizeEol, readJson, readPackageJson, runnerCommand, toPosix } from "./paths.mjs";
|
|
4
|
+
import { cliCommand, generatedCssRelativePath, normalizeEol, readJson, readPackageJson, runnerCommand, toPosix } from "./paths.mjs";
|
|
5
5
|
import { doctorCommand, doctorScriptName, isCompatibleCommand, syncCommand, syncScriptName } from "./register-scripts.mjs";
|
|
6
6
|
import { scanClassNames } from "./scan-classes.mjs";
|
|
7
|
-
import { collectPlans,
|
|
7
|
+
import { collectPlans, describeMissingTailwindRoot } from "./sync.mjs";
|
|
8
8
|
|
|
9
9
|
export const doctorEnvironmentVariable = "EDS_DOCTOR";
|
|
10
10
|
|
|
11
|
+
// Without a Tailwind root, sync cannot add these imports, so each line says where they go.
|
|
11
12
|
const fontGuidance = {
|
|
12
|
-
[stylesImportLine]: "missing: @spectrabrain/eds/styles.css import
|
|
13
|
-
|
|
13
|
+
[stylesImportLine]: "missing: @spectrabrain/eds/styles.css import (or tokens.css + components.css + fonts.css); add it to the app entry, " +
|
|
14
|
+
"for example src/main.tsx, because sync cannot add it without a Tailwind root",
|
|
15
|
+
[fontsImportLine]: "missing: @spectrabrain/eds/fonts.css import next to tokens.css and components.css; add it where they are imported, " +
|
|
16
|
+
"because EDS text styles use the font family \"Pretendard\" it declares",
|
|
14
17
|
};
|
|
18
|
+
const syncFixablePattern = /^(?:missing|stale|legacy): /;
|
|
15
19
|
|
|
16
20
|
export function collectFindings({ consumerRoot, env = process.env }) {
|
|
17
21
|
const plans = collectPlans({ consumerRoot, env });
|
|
@@ -22,25 +26,35 @@ export function collectFindings({ consumerRoot, env = process.env }) {
|
|
|
22
26
|
let rawColors = null;
|
|
23
27
|
|
|
24
28
|
const scripts = readPackageJson(consumerRoot)?.scripts ?? {};
|
|
29
|
+
const unregistered = [];
|
|
25
30
|
for (const [name, command] of [[syncScriptName, syncCommand], [doctorScriptName, doctorCommand]]) {
|
|
26
31
|
if (isCompatibleCommand(scripts[name], command)) {
|
|
27
32
|
continue;
|
|
28
33
|
}
|
|
34
|
+
if (scripts[name] === undefined) {
|
|
35
|
+
unregistered.push(name);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
29
38
|
findings.push(
|
|
30
|
-
scripts[name]
|
|
31
|
-
|
|
32
|
-
: "scripts: " + name + " runs " + JSON.stringify(scripts[name]) + " instead of \"" + command +
|
|
33
|
-
"\"; rename that script and run npx @spectrabrain/eds init, or make it run \"" + command + "\"",
|
|
39
|
+
"scripts: " + name + " runs " + JSON.stringify(scripts[name]) + " instead of \"" + command +
|
|
40
|
+
"\"; rename that script and run " + cliCommand(plans.packageManager, "init") + ", or make it run \"" + command + "\"",
|
|
34
41
|
);
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
if (unregistered.length > 0) {
|
|
44
|
+
findings.unshift("scripts: " + unregistered.join(", ") + (unregistered.length === 1 ? " is" : " are") + " not registered; run " +
|
|
45
|
+
cliCommand(plans.packageManager, "init"));
|
|
46
|
+
}
|
|
47
|
+
// The fix lines may only point at sync:eds and eds:doctor when those scripts really run the EDS commands.
|
|
37
48
|
const syncRegistered = isCompatibleCommand(scripts[syncScriptName], syncCommand);
|
|
49
|
+
const doctorRegistered = isCompatibleCommand(scripts[doctorScriptName], doctorCommand);
|
|
50
|
+
const nonSyncFindings = new Set();
|
|
38
51
|
|
|
39
52
|
if (plans.root === null) {
|
|
40
|
-
notes.push(
|
|
53
|
+
notes.push(describeMissingTailwindRoot(plans));
|
|
41
54
|
// Without a Tailwind root, sync cannot insert the imports, so doctor states the requirement directly.
|
|
42
55
|
for (const line of planPackageImports(plans.sources)) {
|
|
43
56
|
findings.push(fontGuidance[line]);
|
|
57
|
+
nonSyncFindings.add(fontGuidance[line]);
|
|
44
58
|
}
|
|
45
59
|
} else {
|
|
46
60
|
if (plans.tailwind.before === null) {
|
|
@@ -72,13 +86,15 @@ export function collectFindings({ consumerRoot, env = process.env }) {
|
|
|
72
86
|
files: [...plans.sources.scripts, ...plans.sources.css, ...plans.sources.html],
|
|
73
87
|
});
|
|
74
88
|
for (const hit of classScan.unknown) {
|
|
75
|
-
findings.push("unknown: " + hit.location + " " + hit.className + " is not a public EDS class;
|
|
89
|
+
findings.push("unknown: " + hit.location + " " + hit.className + " is not a public EDS class; use a class from " + generatedRelativePath);
|
|
76
90
|
}
|
|
77
91
|
rawColors = classScan.raw;
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
notes.push(...plans.deliveryNotes);
|
|
80
95
|
for (const filePath of findPretendardReferences(consumerRoot, plans.sources)) {
|
|
81
|
-
notes.push(filePath + " references Pretendard outside @spectrabrain/eds
|
|
96
|
+
notes.push(filePath + " references Pretendard outside @spectrabrain/eds. EDS text styles use the font from @spectrabrain/eds/fonts.css, " +
|
|
97
|
+
"so this does not affect EDS; keep it unless the user asks to remove it, because other styles may use it.");
|
|
82
98
|
}
|
|
83
99
|
|
|
84
100
|
for (const error of plans.errors) {
|
|
@@ -98,13 +114,13 @@ export function collectFindings({ consumerRoot, env = process.env }) {
|
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
if (plans.claude && normalizeEol(plans.claude.before) !== normalizeEol(plans.claude.after)) {
|
|
101
|
-
findings.push("missing:
|
|
117
|
+
findings.push("missing: " + plans.claude.importLine + " import in " + plans.claude.relativePath);
|
|
102
118
|
}
|
|
103
119
|
if (plans.gemini && normalizeEol(plans.gemini.before) !== normalizeEol(plans.gemini.after)) {
|
|
104
|
-
findings.push("missing:
|
|
120
|
+
findings.push("missing: " + plans.gemini.importLine + " import in " + plans.gemini.relativePath);
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
return { plans, findings, notes, installed, generatedRelativePath, syncRegistered, rawColors };
|
|
123
|
+
return { plans, findings, notes, installed, generatedRelativePath, syncRegistered, doctorRegistered, nonSyncFindings, unregistered, rawColors };
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
const rawColorLineLimit = 40;
|
|
@@ -168,11 +184,21 @@ export function runDoctor({ consumerRoot, env = process.env, warn = false, scan
|
|
|
168
184
|
for (const finding of result.findings) {
|
|
169
185
|
log("[EDS] " + finding);
|
|
170
186
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
187
|
+
// One next command: init when scripts are missing (it also syncs), otherwise sync when it can fix a line.
|
|
188
|
+
// unknown:, invalid:, scripts: conflicts and the imports sync cannot add carry their own fix on the line.
|
|
189
|
+
const packageManager = result.plans.packageManager;
|
|
190
|
+
const syncFixable = result.findings.some((finding) => syncFixablePattern.test(finding) && !result.nonSyncFindings.has(finding));
|
|
191
|
+
if (result.unregistered.length > 0) {
|
|
192
|
+
log("[EDS] Run " + cliCommand(packageManager, "init") + " to register the scripts; it also runs sync.");
|
|
193
|
+
} else if (syncFixable) {
|
|
194
|
+
const syncRun = result.syncRegistered ? runnerCommand(packageManager, syncScriptName) : cliCommand(packageManager, "sync");
|
|
195
|
+
log("[EDS] Run " + syncRun + " to fix the missing:, stale: and legacy: lines.");
|
|
196
|
+
}
|
|
197
|
+
if (downgrade) {
|
|
198
|
+
log("[EDS] Warn mode: the lines above are not fixed, so this exit 0 does not mean the check passed.");
|
|
199
|
+
}
|
|
200
|
+
const doctorRun = result.doctorRegistered ? runnerCommand(packageManager, doctorScriptName) : cliCommand(packageManager, "doctor");
|
|
201
|
+
// The completion line itself starts with "[EDS] synced with"; this line must not contain that text.
|
|
202
|
+
log("[EDS] Then rerun " + doctorRun + " until it reports synced; the QUICKSTART.md completion criteria in the package root list the order.");
|
|
177
203
|
return downgrade ? 0 : 1;
|
|
178
204
|
}
|
|
@@ -10,9 +10,41 @@ const propertyNames = [
|
|
|
10
10
|
];
|
|
11
11
|
|
|
12
12
|
export const fontSansStyleName = "body1";
|
|
13
|
+
|
|
14
|
+
function safeTailwindAlias(name) {
|
|
15
|
+
return "eds-" + name.replace(/\./g, "-");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cssVariableForToken(manifest, name) {
|
|
19
|
+
const cssVariable = manifest.tokens?.[name]?.$extensions?.eds?.cssVariable;
|
|
20
|
+
if (typeof cssVariable === "string") {
|
|
21
|
+
return cssVariable;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return "--eds-" + name.replace(/\./g, "\\.");
|
|
25
|
+
}
|
|
26
|
+
|
|
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
|
+
|
|
13
43
|
export function renderTailwindCss(manifest, version) {
|
|
14
44
|
const tokens = Object.keys(manifest.tokens ?? {});
|
|
15
45
|
const colorNames = tokens.filter((name) => name.startsWith("color-"));
|
|
46
|
+
const spacingAliases = collectDimensionAliases(manifest, "spacing-");
|
|
47
|
+
const radiusAliases = collectDimensionAliases(manifest, "radius-");
|
|
16
48
|
const typography = new Map();
|
|
17
49
|
|
|
18
50
|
for (const name of tokens.filter((entry) => entry.startsWith("typography-"))) {
|
|
@@ -40,7 +72,13 @@ export function renderTailwindCss(manifest, version) {
|
|
|
40
72
|
}
|
|
41
73
|
|
|
42
74
|
const colorAliases = colorNames
|
|
43
|
-
.map((name) => ` --color-eds-${name.slice("color-".length)}: var(
|
|
75
|
+
.map((name) => ` --color-eds-${name.slice("color-".length)}: var(${cssVariableForToken(manifest, name)});`)
|
|
76
|
+
.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)});`)
|
|
44
82
|
.join("\n");
|
|
45
83
|
// Last line of the @theme block: Tailwind's default font family becomes the EDS body font.
|
|
46
84
|
const fontSans = ` --font-sans: var(--eds-typography-${fontSansStyleName}-fontFamily);`;
|
|
@@ -58,6 +96,8 @@ export function renderTailwindCss(manifest, version) {
|
|
|
58
96
|
`/* Generated from @spectrabrain/eds@${version}. Do not edit manually. */`,
|
|
59
97
|
"@theme inline {",
|
|
60
98
|
colorAliases,
|
|
99
|
+
spacingThemeAliases,
|
|
100
|
+
radiusThemeAliases,
|
|
61
101
|
fontSans,
|
|
62
102
|
"}",
|
|
63
103
|
"",
|
package/consumer/init.mjs
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { createExitError, detectPackageManager, packageName, resolvePackage, runnerCommand } from "./paths.mjs";
|
|
3
|
+
import { cliCommand, createExitError, detectPackageManager, installCommand, packageName, readPackageJson, resolvePackage, runnerCommand } from "./paths.mjs";
|
|
4
4
|
import { registerConsumerScripts, syncScriptName, writeConsumerPackageJson } from "./register-scripts.mjs";
|
|
5
5
|
import { runSync } from "./sync.mjs";
|
|
6
6
|
|
|
7
7
|
export const ciTargets = new Set(["github", "gitlab"]);
|
|
8
8
|
const allowScriptsField = "allowScripts";
|
|
9
9
|
const minimumAllowScriptsNpmMajor = 12;
|
|
10
|
+
// --no-allow-scripts-pin keeps the approval valid after EDS upgrades, like the name-only entry init writes.
|
|
11
|
+
const approveCommand = "npm install-scripts approve --no-allow-scripts-pin " + packageName;
|
|
10
12
|
|
|
11
|
-
const
|
|
13
|
+
const ciInstallCommands = {
|
|
12
14
|
npm: "npm ci",
|
|
13
15
|
pnpm: "pnpm install --frozen-lockfile",
|
|
14
16
|
yarn: "yarn install --frozen-lockfile",
|
|
15
17
|
bun: "bun install --frozen-lockfile",
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
function
|
|
20
|
+
function ciInstallCommand(packageManager) {
|
|
19
21
|
if (packageManager.name === "yarn" && packageManager.berry) {
|
|
20
22
|
return "yarn install --immutable";
|
|
21
23
|
}
|
|
22
|
-
return
|
|
24
|
+
return ciInstallCommands[packageManager.name];
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
// A CI job that fails when sync is out of date. It runs
|
|
26
|
-
// package.json scripts are not required.
|
|
27
|
+
// A CI job that fails when sync is out of date. It runs the EDS CLI directly with the repository's package
|
|
28
|
+
// manager, so the consumer's own package.json scripts are not required.
|
|
27
29
|
export function renderCiSnippet(target, packageManager) {
|
|
28
|
-
const install =
|
|
30
|
+
const install = ciInstallCommand(packageManager);
|
|
31
|
+
const doctor = cliCommand(packageManager, "doctor");
|
|
29
32
|
if (target === "github") {
|
|
30
33
|
const setup = packageManager.name === "pnpm"
|
|
31
34
|
? [" - uses: pnpm/action-setup@v4"]
|
|
@@ -48,7 +51,7 @@ export function renderCiSnippet(target, packageManager) {
|
|
|
48
51
|
" node-version: 22",
|
|
49
52
|
...cache,
|
|
50
53
|
" - run: " + install,
|
|
51
|
-
" - run:
|
|
54
|
+
" - run: " + doctor,
|
|
52
55
|
"",
|
|
53
56
|
].join("\n");
|
|
54
57
|
}
|
|
@@ -66,7 +69,7 @@ export function renderCiSnippet(target, packageManager) {
|
|
|
66
69
|
" script:",
|
|
67
70
|
...before,
|
|
68
71
|
" - " + install,
|
|
69
|
-
" -
|
|
72
|
+
" - " + doctor,
|
|
70
73
|
"",
|
|
71
74
|
].join("\n");
|
|
72
75
|
}
|
|
@@ -81,7 +84,7 @@ function writeAllowScripts(consumerRoot, { log, dryRun }) {
|
|
|
81
84
|
const consumerPackage = JSON.parse(source);
|
|
82
85
|
const existing = consumerPackage[allowScriptsField];
|
|
83
86
|
if (existing !== undefined && typeof existing !== "object") {
|
|
84
|
-
log("[EDS] package.json " + allowScriptsField + " is not an object;
|
|
87
|
+
log("[EDS] package.json " + allowScriptsField + " is not an object; left unchanged. To approve the EDS postinstall, run " + approveCommand + ".");
|
|
85
88
|
return false;
|
|
86
89
|
}
|
|
87
90
|
const current = existing?.[packageName];
|
|
@@ -106,29 +109,29 @@ function printInstallScriptGuidance(consumerRoot, packageManager, { log, dryRun,
|
|
|
106
109
|
if (allowScripts) {
|
|
107
110
|
writeAllowScripts(consumerRoot, { log, dryRun });
|
|
108
111
|
} else {
|
|
109
|
-
log("[EDS] --no-allow-scripts: not writing " + allowScriptsField + "
|
|
112
|
+
log("[EDS] --no-allow-scripts: not writing " + allowScriptsField + ". Later installs skip the EDS postinstall until someone runs " + approveCommand + ".");
|
|
110
113
|
}
|
|
111
114
|
} else if (major === null) {
|
|
112
|
-
log("[EDS] npm
|
|
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 write " + allowScriptsField + ".");
|
|
113
117
|
}
|
|
114
118
|
return;
|
|
115
119
|
}
|
|
120
|
+
// The postinstall only re-registers the scripts init has just written, so approving it is optional everywhere
|
|
121
|
+
// except npm 12+, where init records the approval itself.
|
|
116
122
|
if (name === "pnpm") {
|
|
117
|
-
log("[EDS] pnpm
|
|
118
|
-
|
|
119
|
-
log("[EDS] Without it, rerun " + runnerCommand(packageManager, syncScriptName) + " (or npx eds init) after each install.");
|
|
123
|
+
log("[EDS] pnpm: the scripts are registered, so the EDS postinstall needs no build approval. After upgrading EDS, run " +
|
|
124
|
+
runnerCommand(packageManager, syncScriptName) + ".");
|
|
120
125
|
return;
|
|
121
126
|
}
|
|
122
127
|
if (name === "bun") {
|
|
123
|
-
log("[EDS] bun
|
|
124
|
-
|
|
125
|
-
log("[EDS] Note: setting trustedDependencies replaces bun's default trusted list; include any other package whose install scripts you rely on.");
|
|
128
|
+
log("[EDS] Optional (bun): to let the EDS postinstall re-register the scripts on later installs, add \"trustedDependencies\": [\"" +
|
|
129
|
+
packageName + "\"] to package.json. It replaces bun's default trusted list, so this is the project owner's decision.");
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
if (name === "yarn" && berry) {
|
|
129
|
-
log("[EDS] Yarn 2+
|
|
130
|
-
|
|
131
|
-
log("[EDS] Otherwise rerun " + runnerCommand(packageManager, syncScriptName) + " (or npx eds init) after each install.");
|
|
133
|
+
log("[EDS] Optional (Yarn 2+): to let the EDS postinstall re-register the scripts on later installs, keep enableScripts: true in .yarnrc.yml " +
|
|
134
|
+
"or add \"dependenciesMeta\": { \"" + packageName + "\": { \"built\": true } } to package.json.");
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
|
|
@@ -152,20 +155,31 @@ export function runInit({
|
|
|
152
155
|
|
|
153
156
|
if (!fs.existsSync(path.join(consumerRoot, "package.json"))) {
|
|
154
157
|
throw createExitError(
|
|
155
|
-
"No package.json in " + consumerRoot + "; run
|
|
158
|
+
"No package.json in " + consumerRoot + "; run init in the package that depends on " + packageName + ".",
|
|
156
159
|
1,
|
|
157
160
|
);
|
|
158
161
|
}
|
|
162
|
+
// A workspace root or a parent folder can resolve a hoisted EDS; the scripts, hooks and AGENTS.md belong to the
|
|
163
|
+
// app package that declares it.
|
|
164
|
+
const consumerPackage = readPackageJson(consumerRoot);
|
|
165
|
+
const declared = ["dependencies", "devDependencies", "optionalDependencies"].some((field) => consumerPackage?.[field]?.[packageName] !== undefined);
|
|
166
|
+
if (!declared) {
|
|
167
|
+
throw createExitError(
|
|
168
|
+
"package.json in " + consumerRoot + " does not declare " + packageName + ". Run init in the app package that uses EDS, after " +
|
|
169
|
+
installCommand(packageManager) + " there.",
|
|
170
|
+
2,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
159
173
|
try {
|
|
160
174
|
resolvePackage(consumerRoot);
|
|
161
175
|
} catch {
|
|
162
|
-
throw createExitError(packageName + " is not installed
|
|
176
|
+
throw createExitError(packageName + " is declared but not installed; run " + installCommand(packageManager) + " first.", 1);
|
|
163
177
|
}
|
|
164
178
|
|
|
165
|
-
registerConsumerScripts(consumerRoot, { log, dryRun, packageManager });
|
|
179
|
+
const registered = registerConsumerScripts(consumerRoot, { log, dryRun, packageManager });
|
|
166
180
|
printInstallScriptGuidance(consumerRoot, packageManager, { log, dryRun, allowScripts });
|
|
167
181
|
if (noSync) {
|
|
168
|
-
log("[EDS]
|
|
182
|
+
log("[EDS] Run " + registered.syncRunCommand + " to finish setup.");
|
|
169
183
|
return 0;
|
|
170
184
|
}
|
|
171
185
|
return runSync({ consumerRoot, env, dryRun, log });
|
|
@@ -81,16 +81,22 @@ function lineNumberAt(text, index) {
|
|
|
81
81
|
return text.slice(0, index).split(/\r?\n/).length;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
const markerFix = "Keep one " + beginMarker + " line with the " + endMarker + " line after it, delete the other marker lines, " +
|
|
85
|
+
"then run sync again (sync rewrites what is between them).";
|
|
86
|
+
|
|
87
|
+
function describeLines(text, positions) {
|
|
88
|
+
const lines = positions.map((position) => lineNumberAt(text, position));
|
|
89
|
+
return lines.length === 0 ? "none" : (lines.length === 1 ? "line " : "lines ") + lines.join(", ");
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
export function locateBlock(existing) {
|
|
85
93
|
const masked = maskFencedBlocks(existing);
|
|
86
94
|
const begins = findAll(masked, beginMarker);
|
|
87
95
|
const ends = findAll(masked, endMarker);
|
|
88
96
|
if (begins.length !== ends.length || begins.length > 1) {
|
|
89
|
-
const first = begins[0] ?? ends[0];
|
|
90
97
|
throw new Error(
|
|
91
|
-
"AGENTS.md must contain zero or one complete EDS managed block.
|
|
92
|
-
|
|
93
|
-
" outside fenced code blocks" + (first === undefined ? "" : ", first at line " + lineNumberAt(existing, first)) + ".",
|
|
98
|
+
"AGENTS.md must contain zero or one complete EDS managed block. Outside fenced code blocks it has " + beginMarker + " at " +
|
|
99
|
+
describeLines(existing, begins) + " and " + endMarker + " at " + describeLines(existing, ends) + ". " + markerFix,
|
|
94
100
|
);
|
|
95
101
|
}
|
|
96
102
|
if (begins.length === 0) {
|
|
@@ -102,7 +108,7 @@ export function locateBlock(existing) {
|
|
|
102
108
|
if (endIndex < start) {
|
|
103
109
|
throw new Error(
|
|
104
110
|
"AGENTS.md has an invalid EDS managed block: " + endMarker + " at line " + lineNumberAt(existing, endIndex) +
|
|
105
|
-
" comes before " + beginMarker + " at line " + lineNumberAt(existing, start) + ".",
|
|
111
|
+
" comes before " + beginMarker + " at line " + lineNumberAt(existing, start) + ". " + markerFix,
|
|
106
112
|
);
|
|
107
113
|
}
|
|
108
114
|
const end = endIndex + endMarker.length;
|
package/consumer/paths.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function resolvePackage(consumerRoot) {
|
|
|
28
28
|
try {
|
|
29
29
|
packageJsonPath = require.resolve(packageName + "/package.json");
|
|
30
30
|
} catch {
|
|
31
|
-
throw createExitError(packageName + " is not installed here
|
|
31
|
+
throw createExitError(packageName + " is not installed here; install it in this package first.", 2);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const packageRoot = path.dirname(packageJsonPath);
|
|
@@ -175,25 +175,62 @@ function isYarnBerry(consumerRoot, major) {
|
|
|
175
175
|
return lockfile !== null && /^__metadata:/m.test(lockfile);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
// The directory that holds .git at or above startDirectory, or null outside git.
|
|
179
|
+
export function findGitRoot(startDirectory) {
|
|
180
|
+
for (let directory = path.resolve(startDirectory); ; ) {
|
|
181
|
+
if (fs.existsSync(path.join(directory, ".git"))) {
|
|
182
|
+
return directory;
|
|
183
|
+
}
|
|
184
|
+
const parent = path.dirname(directory);
|
|
185
|
+
if (parent === directory) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
directory = parent;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// The nearest lockfile names the package manager: this directory, then its parents up to the git root, so a
|
|
193
|
+
// workspace package resolves to the repository lockfile. Outside git only this directory counts, so a stray
|
|
194
|
+
// lockfile in a parent folder is never picked up.
|
|
195
|
+
export function findLockfile(startDirectory) {
|
|
196
|
+
const start = path.resolve(startDirectory);
|
|
197
|
+
const directories = [start];
|
|
198
|
+
for (let directory = start; !fs.existsSync(path.join(directory, ".git")); ) {
|
|
199
|
+
const parent = path.dirname(directory);
|
|
200
|
+
if (parent === directory) {
|
|
201
|
+
directories.length = 1;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
directories.push(parent);
|
|
205
|
+
directory = parent;
|
|
184
206
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
for (const directory of directories) {
|
|
208
|
+
const found = lockfileManagers.find(([fileName]) => fs.existsSync(path.join(directory, fileName)));
|
|
209
|
+
if (found) {
|
|
210
|
+
return { directory, fileName: found[0], name: found[1] };
|
|
211
|
+
}
|
|
188
212
|
}
|
|
189
|
-
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The lockfile decides; the runner in npm_config_user_agent (npm, npx, pnpm, yarn, bun) decides only without a
|
|
217
|
+
// lockfile. `npx @spectrabrain/eds init` in a pnpm repository therefore still renders pnpm commands. The runner's
|
|
218
|
+
// version is kept only when the runner is that same manager (npm 12+ allowScripts, Yarn 2+).
|
|
219
|
+
export function detectPackageManager(consumerRoot, env = process.env) {
|
|
220
|
+
const fromAgent = parseUserAgent(env.npm_config_user_agent);
|
|
221
|
+
const lockfile = findLockfile(consumerRoot);
|
|
222
|
+
const name = lockfile?.name ?? fromAgent?.name ?? "npm";
|
|
223
|
+
const agent = fromAgent?.name === name ? fromAgent : null;
|
|
224
|
+
const major = agent?.major ?? null;
|
|
190
225
|
return {
|
|
191
226
|
name,
|
|
192
|
-
version:
|
|
227
|
+
version: agent?.version ?? null,
|
|
193
228
|
major,
|
|
194
|
-
berry: name === "yarn" && isYarnBerry(consumerRoot, major),
|
|
229
|
+
berry: name === "yarn" && isYarnBerry(lockfile?.directory ?? consumerRoot, major),
|
|
230
|
+
// Yarn Plug'n'Play writes .pnp.cjs (.pnp.js before Yarn 3) next to yarn.lock and installs no node_modules.
|
|
231
|
+
pnp: name === "yarn" && [".pnp.cjs", ".pnp.js"].some((fileName) => fs.existsSync(path.join(lockfile?.directory ?? consumerRoot, fileName))),
|
|
195
232
|
runner: runnerPrefixes[name],
|
|
196
|
-
source,
|
|
233
|
+
source: lockfile ? "lockfile" : fromAgent ? "user-agent" : "default",
|
|
197
234
|
};
|
|
198
235
|
}
|
|
199
236
|
|
|
@@ -201,6 +238,24 @@ export function runnerCommand(packageManager, scriptName) {
|
|
|
201
238
|
return (packageManager?.runner ?? runnerPrefixes.npm) + scriptName;
|
|
202
239
|
}
|
|
203
240
|
|
|
241
|
+
// npx and bunx fall back to the registry when no local bin exists, and the unscoped registry name "eds" is an
|
|
242
|
+
// unrelated package, so they always name @spectrabrain/eds. pnpm exec and yarn run only the local bin.
|
|
243
|
+
const cliPrefixes = {
|
|
244
|
+
npm: "npx " + packageName,
|
|
245
|
+
pnpm: "pnpm exec eds",
|
|
246
|
+
yarn: "yarn eds",
|
|
247
|
+
bun: "bunx " + packageName,
|
|
248
|
+
};
|
|
249
|
+
const addPrefixes = { npm: "npm install", pnpm: "pnpm add", yarn: "yarn add", bun: "bun add" };
|
|
250
|
+
|
|
251
|
+
export function cliCommand(packageManager, args) {
|
|
252
|
+
return (cliPrefixes[packageManager?.name] ?? cliPrefixes.npm) + " " + args;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function installCommand(packageManager, names = [packageName]) {
|
|
256
|
+
return (addPrefixes[packageManager?.name] ?? addPrefixes.npm) + " " + names.join(" ");
|
|
257
|
+
}
|
|
258
|
+
|
|
204
259
|
export function renderRunnerCommands(text, packageManager) {
|
|
205
260
|
const runner = packageManager?.runner ?? runnerPrefixes.npm;
|
|
206
261
|
return runner === runnerPrefixes.npm ? text : text.replace(/\bnpm run /g, runner);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { detectIndent, detectPackageManager, runnerCommand } from "./paths.mjs";
|
|
3
|
+
import { cliCommand, detectIndent, detectPackageManager, runnerCommand } from "./paths.mjs";
|
|
4
4
|
|
|
5
5
|
export const syncCommand = "eds sync";
|
|
6
6
|
export const doctorCommand = "eds doctor";
|
|
@@ -48,7 +48,7 @@ export function registerConsumerScripts(
|
|
|
48
48
|
const source = fs.readFileSync(packagePath, "utf8");
|
|
49
49
|
const consumerPackage = JSON.parse(source);
|
|
50
50
|
const scripts = { ...(consumerPackage.scripts ?? {}) };
|
|
51
|
-
const result = { changed: false, added: [], migrated: [], removed: [], skippedHooks: [], conflicts: [] };
|
|
51
|
+
const result = { changed: false, added: [], migrated: [], removed: [], skippedHooks: [], conflicts: [], syncRunCommand: null };
|
|
52
52
|
|
|
53
53
|
for (const [name, command] of Object.entries(requiredScripts)) {
|
|
54
54
|
if (isCompatibleCommand(scripts[name], command)) {
|
|
@@ -67,8 +67,8 @@ export function registerConsumerScripts(
|
|
|
67
67
|
// Never fail the install over a consumer-owned script: leave it alone and let doctor report it.
|
|
68
68
|
result.conflicts.push(name);
|
|
69
69
|
log(
|
|
70
|
-
`[
|
|
71
|
-
`
|
|
70
|
+
`[EDS] package.json script ${name} runs ${JSON.stringify(scripts[name])}; left unchanged. ` +
|
|
71
|
+
`eds doctor reports it until the script runs "${command}".`,
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -94,28 +94,33 @@ export function registerConsumerScripts(
|
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
result.skippedHooks.push(hook.name);
|
|
97
|
-
log(
|
|
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
|
+
);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
result.changed = result.added.length > 0 || result.migrated.length > 0 || result.removed.length > 0;
|
|
101
|
-
// When sync:eds belongs to the consumer, point at the CLI instead of their script.
|
|
102
|
-
|
|
104
|
+
// When sync:eds belongs to the consumer, point at the CLI instead of their script. The caller prints the next
|
|
105
|
+
// step: init runs sync right away, postinstall tells the consumer to run it.
|
|
106
|
+
result.syncRunCommand = result.conflicts.includes(syncScriptName) ? cliCommand(packageManager, "sync") : runnerCommand(packageManager, syncScriptName);
|
|
103
107
|
if (result.changed && dryRun) {
|
|
104
|
-
log("[
|
|
108
|
+
log("[EDS] would update package.json scripts: " + [...result.added, ...result.migrated, ...result.removed].join(", "));
|
|
105
109
|
} else if (result.changed) {
|
|
106
110
|
consumerPackage.scripts = scripts;
|
|
107
111
|
writeConsumerPackageJson(packagePath, source, consumerPackage);
|
|
108
|
-
log("[
|
|
112
|
+
log("[EDS] Added the EDS scripts to package.json: " + [...result.added, ...result.migrated].join(", ") +
|
|
113
|
+
(result.removed.length > 0 ? " (removed " + result.removed.join(", ") + ")" : "") + ".");
|
|
109
114
|
} else if (result.conflicts.length === 0) {
|
|
110
|
-
log("[
|
|
115
|
+
log("[EDS] The EDS scripts are already registered.");
|
|
111
116
|
} else {
|
|
112
|
-
log("[
|
|
117
|
+
log("[EDS] The EDS scripts were left unchanged.");
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Yarn berry (2+) does not run pre/post lifecycle hooks, so the doctor gate must be inlined by the consumer.
|
|
116
121
|
if (packageManager?.name === "yarn" && packageManager.berry && hookTargets.length > 0) {
|
|
117
122
|
log(
|
|
118
|
-
"[
|
|
123
|
+
"[EDS] Yarn 2+ does not run pretypecheck/prebuild hooks. Prepend \"" + doctorCommand +
|
|
119
124
|
" && \" to your " + hookTargets.join(" and ") + " script" + (hookTargets.length > 1 ? "s" : "") +
|
|
120
125
|
" to keep the doctor gate (this tool does not edit them).",
|
|
121
126
|
);
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { relativeTo } from "./paths.mjs";
|
|
2
2
|
|
|
3
|
-
// Tailwind utilities that take a color. A class such as hover:bg-eds-bg-brand-primary/80 is one token here.
|
|
3
|
+
// Tailwind utilities that take a color. A class such as hover:bg-eds-bg-brand-primary/80 is one token here. Token names keep
|
|
4
|
+
// their case (stroke-focusRing), so a miscased class is reported instead of skipped.
|
|
4
5
|
const colorUtility = "(?:text|bg|border|ring|outline|fill|stroke|decoration|divide|from|to|via|accent|caret|shadow|placeholder|inset-ring)";
|
|
5
6
|
const variantPrefix = "(?:[a-z0-9-]+:)*";
|
|
6
7
|
const boundaryBefore = "(?<![\\w-])";
|
|
7
8
|
const boundaryAfter = "(?![\\w-])";
|
|
8
9
|
const opacity = "(?:/[0-9]{1,3})?";
|
|
9
10
|
|
|
10
|
-
const edsColorClass = new RegExp(boundaryBefore + variantPrefix + colorUtility + "-eds-([
|
|
11
|
-
const edsTypographyClass = new RegExp(boundaryBefore + variantPrefix + "eds-typography-([
|
|
11
|
+
const edsColorClass = new RegExp(boundaryBefore + variantPrefix + colorUtility + "-eds-([A-Za-z0-9-]+)" + opacity + boundaryAfter, "g");
|
|
12
|
+
const edsTypographyClass = new RegExp(boundaryBefore + variantPrefix + "eds-typography-([A-Za-z0-9-]+)" + boundaryAfter, "g");
|
|
13
|
+
const dimensionUtility = "(?:p|px|py|ps|pe|pt|pr|pb|pl|m|mx|my|ms|me|mt|mr|mb|ml|gap|gap-x|gap-y|space-x|space-y|inset|inset-x|inset-y|start|end|top|right|bottom|left|w|min-w|max-w|h|min-h|max-h|size|basis|scroll-m|scroll-p)";
|
|
14
|
+
const edsSpacingClass = new RegExp(boundaryBefore + variantPrefix + dimensionUtility + "-eds-(spacing-[A-Za-z0-9-]+)" + boundaryAfter, "g");
|
|
15
|
+
const edsRadiusClass = new RegExp(boundaryBefore + variantPrefix + "rounded(?:-(?:s|e|t|r|b|l|ss|se|ee|es|tl|tr|br|bl))?-eds-(radius-[A-Za-z0-9-]+)" + boundaryAfter, "g");
|
|
12
16
|
const rawColorClass = new RegExp(
|
|
13
17
|
boundaryBefore + variantPrefix + colorUtility +
|
|
14
18
|
"-(?:(?: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,6 +24,7 @@ const rawColorClass = new RegExp(
|
|
|
20
24
|
export function publicClassNames(manifest) {
|
|
21
25
|
const colors = new Set();
|
|
22
26
|
const typography = new Set();
|
|
27
|
+
const dimensions = new Set();
|
|
23
28
|
for (const name of Object.keys(manifest.tokens ?? {})) {
|
|
24
29
|
if (name.startsWith("color-")) {
|
|
25
30
|
colors.add("eds-" + name.slice("color-".length));
|
|
@@ -28,16 +33,20 @@ export function publicClassNames(manifest) {
|
|
|
28
33
|
const match = name.match(/^typography-(.+)-(?:fontFamily|fontWeight|fontSize|lineHeight|letterSpacing)$/);
|
|
29
34
|
if (match) {
|
|
30
35
|
typography.add(match[1]);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (name.startsWith("spacing-") || name.startsWith("radius-")) {
|
|
39
|
+
dimensions.add("eds-" + name.replace(/\./g, "-"));
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
|
-
return { colors, typography };
|
|
42
|
+
return { colors, typography, dimensions };
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
// files: [{ path, contents }] as readSources() lists them (the generated eds-tokens.css is not among them).
|
|
37
46
|
// unknown: eds-* classes that no public token backs (always a mistake, Tailwind silently drops them).
|
|
38
47
|
// raw: Tailwind palette, white/black and arbitrary color classes that bypass the EDS tokens (informational).
|
|
39
48
|
export function scanClassNames({ consumerRoot, manifest, files }) {
|
|
40
|
-
const { colors, typography } = publicClassNames(manifest);
|
|
49
|
+
const { colors, typography, dimensions } = publicClassNames(manifest);
|
|
41
50
|
const unknown = [];
|
|
42
51
|
const raw = [];
|
|
43
52
|
for (const file of files) {
|
|
@@ -61,6 +70,16 @@ export function scanClassNames({ consumerRoot, manifest, files }) {
|
|
|
61
70
|
unknown.push({ location, className: match[0] });
|
|
62
71
|
}
|
|
63
72
|
}
|
|
73
|
+
for (const match of line.matchAll(edsSpacingClass)) {
|
|
74
|
+
if (match[1] !== undefined && !dimensions.has("eds-" + match[1])) {
|
|
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
|
+
}
|
|
82
|
+
}
|
|
64
83
|
for (const match of line.matchAll(rawColorClass)) {
|
|
65
84
|
raw.push({ location, className: match[0] });
|
|
66
85
|
}
|