@astryxdesign/cli 0.7.2-canary.fbc03f7 → 0.7.2-canary.fdd384f
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/README.md +9 -8
- package/api/build/help/help.mjs +1 -1
- package/api/component/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/docs/_adapter.d.mts +11 -0
- package/api/docs/_adapter.mjs +244 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/index.d.mts +2 -0
- package/api/index.mjs +2 -0
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +8 -4
- package/api/integration/add-contribution.test.mjs +4 -2
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/check/check.d.mts +96 -0
- package/api/integration/check/check.mjs +357 -0
- package/api/integration/check/check.test.mjs +489 -0
- package/api/integration/integration-check.type.d.mts +118 -0
- package/api/integration/integration-check.type.mjs +84 -0
- package/api/integration/integrationCheck.doc.d.mts +10 -0
- package/api/integration/integrationCheck.doc.mjs +73 -0
- package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
- package/api/integration/pack-check.mjs +51 -97
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.mjs +8 -0
- package/api/integration/validate-integration.test.mjs +11 -0
- package/api/integration/validate-integration.type.d.mts +8 -0
- package/api/integration/validate-integration.type.mjs +12 -1
- package/api/integration/validateIntegration.doc.mjs +17 -3
- package/api/search/search.d.mts +9 -0
- package/api/search/search.integration-check.test.mjs +24 -0
- package/api/search/search.mjs +100 -13
- package/api/swizzle/copy/copy.mjs +28 -14
- package/api/theme/build/build.family.test.mjs +54 -6
- package/api/theme/build/build.mjs +114 -17
- package/api/theme/themeBuild.doc.mjs +4 -4
- package/assets/docs/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/block-template.doc.mjs +2 -2
- package/assets/docs/tree/check-the-props.doc.mjs +196 -0
- package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
- package/assets/docs/tree/checks.doc.mjs +21 -13
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/page-template.doc.mjs +1 -1
- package/assets/docs/tree/quick-start.doc.mjs +1 -1
- package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- package/assets/theme.template.ts +1 -1
- package/authoring/doctypes/component/component.doc.mjs +2 -2
- package/authoring/doctypes/component/type.ts +6 -3
- package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/template.doc.mjs +1 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
- package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
- package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
- package/clients/cli/commands/doctor-integration.test.mjs +837 -0
- package/clients/cli/commands/doctor.mjs +190 -0
- package/clients/cli/commands/integration-check.doc.mjs +85 -0
- package/clients/cli/commands/integration-check.test.mjs +267 -0
- package/clients/cli/commands/integration.doc.mjs +6 -2
- package/clients/cli/commands/integration.mjs +107 -1
- package/clients/cli/commands/text-json-parity.test.mjs +5 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/index.mjs +1 -0
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +178 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +659 -62
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -0
- package/foundation/discovery/integration-doc-files.d.mts +22 -0
- package/foundation/discovery/integration-doc-files.mjs +231 -0
- package/foundation/discovery/integration-doc-files.test.mjs +246 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -0
- package/foundation/discovery/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +278 -0
- package/foundation/discovery/prop-drift.mjs +2263 -0
- package/foundation/discovery/prop-drift.test.mjs +2219 -0
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +7 -2
- package/package.json +9 -9
package/api/search/search.mjs
CHANGED
|
@@ -81,12 +81,20 @@
|
|
|
81
81
|
|
|
82
82
|
import {withListingCache} from '../../foundation/fs/listing-cache.mjs';
|
|
83
83
|
import {readDocView} from '../../foundation/doc-compiler/read.mjs';
|
|
84
|
-
import {
|
|
84
|
+
import {
|
|
85
|
+
discoverExternalPackages,
|
|
86
|
+
findCoreDir,
|
|
87
|
+
} from '../../foundation/fs/paths.mjs';
|
|
85
88
|
import {
|
|
86
89
|
discoverComponents,
|
|
87
90
|
discoverValidIntegrationComponents,
|
|
91
|
+
docStem,
|
|
88
92
|
familyMember,
|
|
89
93
|
findComponentReadme,
|
|
94
|
+
forEachIntegrationDoc,
|
|
95
|
+
integrationMemberEntry,
|
|
96
|
+
integrationRecordHold,
|
|
97
|
+
isIntegrationMember,
|
|
90
98
|
resolveImportPath,
|
|
91
99
|
resolveIntegrationImportPath,
|
|
92
100
|
CORE_PACKAGE,
|
|
@@ -98,6 +106,7 @@ import {
|
|
|
98
106
|
import {
|
|
99
107
|
loadComponentReplacements,
|
|
100
108
|
loadIntegrationsSafely,
|
|
109
|
+
resolveLegacyExternalDoc,
|
|
101
110
|
} from '../component/_adapter.mjs';
|
|
102
111
|
import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
|
|
103
112
|
import {discoverTemplates, extractComponents} from '../template/template.mjs';
|
|
@@ -150,6 +159,9 @@ import {setResultCoverage} from './coverage.mjs';
|
|
|
150
159
|
* @property {'page'|'block'} [_kind]
|
|
151
160
|
* @property {string} [_resultName]
|
|
152
161
|
* @property {string} [_commandName]
|
|
162
|
+
* @property {'file'|'doc'} [_hold] - How an integration component holds its
|
|
163
|
+
* name, for the owner rule.
|
|
164
|
+
* @property {string} [_docPath] - An integration component's doc.
|
|
153
165
|
*/
|
|
154
166
|
|
|
155
167
|
/**
|
|
@@ -1110,20 +1122,57 @@ async function gatherCoreComponents(coreDir) {
|
|
|
1110
1122
|
* usage/description from each component's .doc.mjs, same as core. Without
|
|
1111
1123
|
* this, an integration component is invisible to `search`/`build` even
|
|
1112
1124
|
* though `component --list`/`component <Name>` already resolve it — the two
|
|
1113
|
-
* discovery paths silently disagreed.
|
|
1125
|
+
* discovery paths silently disagreed. Also returns, per name, the docs that
|
|
1126
|
+
* hold it by their file name and the loadable docs that only record it.
|
|
1114
1127
|
* @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]} loadedIntegrations
|
|
1115
|
-
* @returns {Promise<Candidate[]>}
|
|
1128
|
+
* @returns {Promise<{candidates: Candidate[], fileNamed: Map<string, Set<string>>, documented: Map<string, Set<string>>}>}
|
|
1116
1129
|
*/
|
|
1117
1130
|
async function gatherIntegrationComponents(loadedIntegrations) {
|
|
1118
1131
|
/** @type {Candidate[]} */
|
|
1119
1132
|
const candidates = [];
|
|
1133
|
+
// What the owner rule reads: the docs that hold a name by their file name,
|
|
1134
|
+
// loadable or not and hidden ones too, and the loadable docs that only
|
|
1135
|
+
// record it.
|
|
1136
|
+
/** @type {Map<string, Set<string>>} */
|
|
1137
|
+
const fileNamed = new Map();
|
|
1138
|
+
/** @type {Map<string, Set<string>>} */
|
|
1139
|
+
const documented = new Map();
|
|
1140
|
+
/** @param {Map<string, Set<string>>} map @param {string} name @param {string} docPath */
|
|
1141
|
+
const hold = (map, name, docPath) =>
|
|
1142
|
+
map.set(name, (map.get(name) ?? new Set()).add(docPath));
|
|
1120
1143
|
for (const integration of loadedIntegrations) {
|
|
1121
|
-
const {components} =
|
|
1144
|
+
const {components, discovered} =
|
|
1145
|
+
await discoverValidIntegrationComponents(integration);
|
|
1146
|
+
const renamed = new Set(
|
|
1147
|
+
discovered
|
|
1148
|
+
.filter(rec => integrationRecordHold(rec) === 'doc')
|
|
1149
|
+
.filter(rec => !isIntegrationMember(rec))
|
|
1150
|
+
.map(rec => rec.docPath),
|
|
1151
|
+
);
|
|
1152
|
+
forEachIntegrationDoc(integration.components, (docPath, stem) => {
|
|
1153
|
+
if (!renamed.has(docPath)) hold(fileNamed, stem, docPath);
|
|
1154
|
+
});
|
|
1122
1155
|
for (const rec of components) {
|
|
1123
|
-
const
|
|
1156
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1157
|
+
// A family member is indexed from its own entry: the family's keywords
|
|
1158
|
+
// and guidance describe the family.
|
|
1159
|
+
const member = isIntegrationMember(rec);
|
|
1160
|
+
const doc = member ? integrationMemberEntry(loaded, rec) : loaded;
|
|
1161
|
+
// A doc that names another export of its file still answers to the
|
|
1162
|
+
// file's name, as `component <file name>` does.
|
|
1163
|
+
const fileName = docStem(rec.docPath);
|
|
1164
|
+
if (integrationRecordHold(rec) === 'doc') {
|
|
1165
|
+
hold(documented, rec.name, rec.docPath);
|
|
1166
|
+
}
|
|
1167
|
+
if (renamed.has(rec.docPath) && !member) {
|
|
1168
|
+
hold(documented, fileName, rec.docPath);
|
|
1169
|
+
}
|
|
1124
1170
|
candidates.push({
|
|
1125
1171
|
domain: 'component',
|
|
1126
1172
|
name: rec.name,
|
|
1173
|
+
aliases: !member && fileName !== rec.name ? [fileName] : [],
|
|
1174
|
+
_hold: integrationRecordHold(rec),
|
|
1175
|
+
_docPath: rec.docPath,
|
|
1127
1176
|
keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
|
|
1128
1177
|
description: doc ? doc.usage?.description || doc.description || '' : '',
|
|
1129
1178
|
guidance: guidanceFrom(doc),
|
|
@@ -1143,12 +1192,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1143
1192
|
packageName: rec.package,
|
|
1144
1193
|
},
|
|
1145
1194
|
rec.name,
|
|
1146
|
-
|
|
1195
|
+
loaded?.import,
|
|
1147
1196
|
),
|
|
1148
1197
|
});
|
|
1149
1198
|
}
|
|
1150
1199
|
}
|
|
1151
|
-
return candidates;
|
|
1200
|
+
return {candidates, fileNamed, documented};
|
|
1152
1201
|
}
|
|
1153
1202
|
|
|
1154
1203
|
/**
|
|
@@ -1160,11 +1209,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
|
|
|
1160
1209
|
*/
|
|
1161
1210
|
async function gatherComponents(coreDir, cwd) {
|
|
1162
1211
|
const loadedIntegrations = await loadIntegrationsSafely(cwd);
|
|
1163
|
-
const [core,
|
|
1212
|
+
const [core, gathered, replacements] = await Promise.all([
|
|
1164
1213
|
gatherCoreComponents(coreDir),
|
|
1165
1214
|
gatherIntegrationComponents(loadedIntegrations),
|
|
1166
1215
|
loadComponentReplacements(coreDir, loadedIntegrations),
|
|
1167
1216
|
]);
|
|
1217
|
+
const {candidates: integrations, fileNamed, documented} = gathered;
|
|
1168
1218
|
// An active replacement answers to the Core name it replaces, so a search
|
|
1169
1219
|
// for that name finds the replacement and not the Core original
|
|
1170
1220
|
// (spec:AST-035 FR11).
|
|
@@ -1175,9 +1225,43 @@ async function gatherComponents(coreDir, cwd) {
|
|
|
1175
1225
|
active.target,
|
|
1176
1226
|
]),
|
|
1177
1227
|
);
|
|
1228
|
+
// The owner rule, as component detail applies it: a candidate `component
|
|
1229
|
+
// <name>` does not open names its package and follows the owner, and a
|
|
1230
|
+
// file-name alias that opens another owner is dropped.
|
|
1231
|
+
const coreNames = new Set(core.map(candidate => candidate.name));
|
|
1232
|
+
/** @param {string} name @param {Candidate} candidate */
|
|
1233
|
+
const opens = (name, candidate) =>
|
|
1234
|
+
!coreNames.has(name) &&
|
|
1235
|
+
findComponentReadme(coreDir, name) == null &&
|
|
1236
|
+
[...(fileNamed.get(name) ?? [])].every(doc => doc === candidate._docPath) &&
|
|
1237
|
+
[...(documented.get(name) ?? [])].every(doc => doc === candidate._docPath);
|
|
1238
|
+
// A legacy docs package keeps an exact name over an export name a doc records.
|
|
1239
|
+
/** @type {ReturnType<typeof discoverExternalPackages> | undefined} */
|
|
1240
|
+
let externals;
|
|
1241
|
+
/** @param {string} name */
|
|
1242
|
+
const legacyDocuments = name =>
|
|
1243
|
+
(externals ??= discoverExternalPackages(cwd)).some(
|
|
1244
|
+
ext => resolveLegacyExternalDoc(ext, name) != null,
|
|
1245
|
+
);
|
|
1246
|
+
const ruled = integrations.map(candidate => {
|
|
1247
|
+
const aliases = (candidate.aliases ?? []).filter(alias =>
|
|
1248
|
+
opens(alias, candidate),
|
|
1249
|
+
);
|
|
1250
|
+
return candidate._hold === 'doc' &&
|
|
1251
|
+
(!opens(candidate.name, candidate) || legacyDocuments(candidate.name))
|
|
1252
|
+
? {
|
|
1253
|
+
...candidate,
|
|
1254
|
+
aliases,
|
|
1255
|
+
_command: `astryx component ${candidate.name} --package ${candidate._package}`,
|
|
1256
|
+
}
|
|
1257
|
+
: {...candidate, aliases};
|
|
1258
|
+
});
|
|
1178
1259
|
return [
|
|
1179
1260
|
...core.filter(candidate => !replacements.forTarget(candidate.name)),
|
|
1180
|
-
...
|
|
1261
|
+
...[
|
|
1262
|
+
...ruled.filter(candidate => !candidate._command),
|
|
1263
|
+
...ruled.filter(candidate => candidate._command),
|
|
1264
|
+
].map(candidate => {
|
|
1181
1265
|
const target = targetOf.get(`${candidate._package}\0${candidate.name}`);
|
|
1182
1266
|
if (target != null) {
|
|
1183
1267
|
return {...candidate, aliases: [...(candidate.aliases ?? []), target]};
|
|
@@ -1244,10 +1328,13 @@ export async function componentKeywords(coreDir, cwd) {
|
|
|
1244
1328
|
const integrations = loadedIntegrations.map(async integration => {
|
|
1245
1329
|
const {components} = await discoverValidIntegrationComponents(integration);
|
|
1246
1330
|
return Promise.all(
|
|
1247
|
-
components.map(async rec =>
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1331
|
+
components.map(async rec => {
|
|
1332
|
+
const loaded = await loadModuleDoc(rec.docPath);
|
|
1333
|
+
return {
|
|
1334
|
+
name: rec.name,
|
|
1335
|
+
keywords: keywordsOf(integrationMemberEntry(loaded, rec) ?? loaded),
|
|
1336
|
+
};
|
|
1337
|
+
}),
|
|
1251
1338
|
);
|
|
1252
1339
|
});
|
|
1253
1340
|
return [
|
|
@@ -21,8 +21,10 @@ import {checkGhCli} from '../_github.mjs';
|
|
|
21
21
|
import {Project} from '../../../foundation/config/project.mjs';
|
|
22
22
|
import {
|
|
23
23
|
CORE_PACKAGE,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
docStem,
|
|
25
|
+
findComponentReadme,
|
|
26
|
+
findIntegrationComponent,
|
|
27
|
+
unscopedComponentOwners,
|
|
26
28
|
} from '../../../foundation/discovery/component-discovery.mjs';
|
|
27
29
|
import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
|
|
28
30
|
import {AstryxError, writeFailed} from '../../error.mjs';
|
|
@@ -126,33 +128,38 @@ async function loadConfigSafely(cwd) {
|
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* Build the set of OWNER packages that provide a component named `name` across
|
|
129
|
-
* core + every loaded integration.
|
|
131
|
+
* core + every loaded integration. Core holds every name it documents, but has
|
|
132
|
+
* source to copy (`sourceDir`) only for a name with its own folder.
|
|
130
133
|
* @param {string} coreDir
|
|
131
134
|
* @param {Array<{name: string, components?: string, issuesUrl?: string}>} loadedIntegrations
|
|
132
135
|
* @param {string} name
|
|
133
136
|
* @param {string|undefined} coreIssuesUrl
|
|
134
|
-
* @returns {Array<{package: string, sourceDir: string|null, ownerPackage: string, issuesUrl: string|undefined}>}
|
|
137
|
+
* @returns {Array<{package: string, sourceDir: string|null, docPath: string|null, ownerPackage: string, issuesUrl: string|undefined, hold: string}>}
|
|
135
138
|
*/
|
|
136
139
|
function resolveOwners(coreDir, loadedIntegrations, name, coreIssuesUrl) {
|
|
137
140
|
const owners = [];
|
|
138
141
|
const coreComponentDir = path.join(coreDir, 'src', name);
|
|
139
|
-
|
|
142
|
+
const coreFolder = fs.existsSync(coreComponentDir);
|
|
143
|
+
if (coreFolder || findComponentReadme(coreDir, name)) {
|
|
140
144
|
owners.push({
|
|
141
145
|
package: CORE_PACKAGE,
|
|
142
|
-
sourceDir: coreComponentDir,
|
|
146
|
+
sourceDir: coreFolder ? coreComponentDir : null,
|
|
147
|
+
docPath: null,
|
|
143
148
|
ownerPackage: CORE_PACKAGE,
|
|
144
149
|
issuesUrl: coreIssuesUrl || DEFAULT_ISSUES_URL,
|
|
150
|
+
hold: 'core',
|
|
145
151
|
});
|
|
146
152
|
}
|
|
147
153
|
for (const integration of loadedIntegrations) {
|
|
148
|
-
const
|
|
149
|
-
if (!
|
|
150
|
-
const sourcePath = findIntegrationComponentSource(integration, name);
|
|
154
|
+
const found = findIntegrationComponent(integration, name);
|
|
155
|
+
if (!found) continue;
|
|
151
156
|
owners.push({
|
|
152
157
|
package: integration.name,
|
|
153
|
-
sourceDir: sourcePath ? path.dirname(sourcePath) : null,
|
|
158
|
+
sourceDir: found.sourcePath ? path.dirname(found.sourcePath) : null,
|
|
159
|
+
docPath: found.docPath,
|
|
154
160
|
ownerPackage: integration.name,
|
|
155
161
|
issuesUrl: integration.issuesUrl,
|
|
162
|
+
hold: found.hold,
|
|
156
163
|
});
|
|
157
164
|
}
|
|
158
165
|
return owners;
|
|
@@ -222,25 +229,32 @@ export async function swizzleCopy(component, options = {}) {
|
|
|
222
229
|
: undefined;
|
|
223
230
|
// An active integration replacement answers to the Core name it replaces,
|
|
224
231
|
// bare or qualified by its own package (spec:AST-035 FR11): copy it under its
|
|
225
|
-
//
|
|
232
|
+
// doc's file name, the folder it has always gone to. `--package
|
|
233
|
+
// @astryxdesign/core` still copies the Core original.
|
|
226
234
|
if (pkg !== CORE_PACKAGE) {
|
|
227
235
|
const replacement = await resolveComponentReplacement(
|
|
228
236
|
coreDir,
|
|
229
237
|
loadedIntegrations,
|
|
230
238
|
dirName,
|
|
231
239
|
);
|
|
240
|
+
const copyName = replacement ? docStem(replacement.docPath) : null;
|
|
232
241
|
if (
|
|
233
242
|
replacement &&
|
|
234
|
-
|
|
243
|
+
copyName &&
|
|
244
|
+
(!pkg || (pkg === replacement.package && copyName !== dirName))
|
|
235
245
|
) {
|
|
236
|
-
return swizzleCopy(
|
|
246
|
+
return swizzleCopy(copyName, {
|
|
237
247
|
...options,
|
|
238
248
|
package: replacement.package,
|
|
239
249
|
});
|
|
240
250
|
}
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const
|
|
253
|
+
const found = resolveOwners(coreDir, loadedIntegrations, dirName, coreIssuesUrl);
|
|
254
|
+
// The owner rule, then a Core name without its own folder drops out.
|
|
255
|
+
const allOwners = (pkg ? found : unscopedComponentOwners(found)).filter(
|
|
256
|
+
owner => owner.package !== CORE_PACKAGE || owner.sourceDir != null,
|
|
257
|
+
);
|
|
244
258
|
|
|
245
259
|
if (allOwners.length === 0) {
|
|
246
260
|
throw new AstryxError(
|
|
@@ -13,6 +13,7 @@ import {themeBuild, themeBuildFamily} from './build.mjs';
|
|
|
13
13
|
import {interceptCore} from './core-interception.mjs';
|
|
14
14
|
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
15
15
|
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
+
import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
|
|
16
17
|
vi.setConfig({testTimeout: 120_000});
|
|
17
18
|
const CLI_ROOT = path.resolve(import.meta.dirname, '../../..');
|
|
18
19
|
const FAMILY_KEY = 'ocean-family';
|
|
@@ -252,22 +253,69 @@ describe('themeBuildFamily()', () => {
|
|
|
252
253
|
expect(fs.existsSync(dtsPath)).toBe(false);
|
|
253
254
|
});
|
|
254
255
|
|
|
255
|
-
it('refuses family outputs outside cwd
|
|
256
|
+
it('refuses to write family outputs outside cwd, and checks them read-only', async () => {
|
|
256
257
|
const dir = makeDir();
|
|
257
258
|
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
258
259
|
const app = path.join(dir, 'app');
|
|
259
260
|
fs.mkdirSync(app);
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
await expect(
|
|
263
|
+
themeBuildFamily(files, {familyKey: FAMILY_KEY}, {cwd: app}),
|
|
264
|
+
).rejects.toMatchObject({code: 'ERR_PATH_TRAVERSAL'});
|
|
265
|
+
const checked = await themeBuildFamily(
|
|
266
|
+
files,
|
|
267
|
+
{familyKey: FAMILY_KEY, check: true},
|
|
268
|
+
{cwd: app},
|
|
269
|
+
);
|
|
270
|
+
expect(checked?.type).toBe('theme.build.check');
|
|
271
|
+
expect(checked?.data.upToDate).toBe(false);
|
|
272
|
+
// The `.css.d.ts` import stub may be absent in check mode.
|
|
273
|
+
expect(checked?.data.stale).toEqual(
|
|
274
|
+
OUTPUTS.filter(file => !file.endsWith('.css.d.ts')).map(file => ({
|
|
275
|
+
path: `../themes/${file}`,
|
|
276
|
+
reason: 'missing',
|
|
277
|
+
})),
|
|
278
|
+
);
|
|
266
279
|
for (const output of OUTPUTS) {
|
|
267
280
|
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(false);
|
|
268
281
|
}
|
|
269
282
|
});
|
|
270
283
|
|
|
284
|
+
it('ends a stale family report for outputs outside cwd with a remedy that works', async () => {
|
|
285
|
+
const dir = makeDir();
|
|
286
|
+
const files = writeFamily(dir).map(file => path.join(dir, file));
|
|
287
|
+
const app = path.join(dir, 'app');
|
|
288
|
+
fs.mkdirSync(app);
|
|
289
|
+
const family = ['--family', ...files, '--family-key', FAMILY_KEY];
|
|
290
|
+
|
|
291
|
+
const stale = await runCli(['theme', 'build', ...family, '--check'], app);
|
|
292
|
+
expect(stale.code).toBe(1);
|
|
293
|
+
// The old remedy ran the build from cwd, which refuses these outputs.
|
|
294
|
+
expect(stale.stderr).not.toContain('Rebuild with:');
|
|
295
|
+
|
|
296
|
+
const lines = stale.stderr.split('\n').map(line => line.trim());
|
|
297
|
+
const runDir = lines.find(line => line.startsWith('cd '))?.slice(3);
|
|
298
|
+
expect(runDir).toBe(fs.realpathSync(path.join(dir, 'themes')));
|
|
299
|
+
const stem = `${getCliInvocation(runDir)} theme build `;
|
|
300
|
+
const [buildArgs, checkArgs] = lines
|
|
301
|
+
.filter(line => line.startsWith(stem))
|
|
302
|
+
.map(line => line.slice(stem.length).split(' '));
|
|
303
|
+
expect(checkArgs).toEqual([...buildArgs, '--check']);
|
|
304
|
+
|
|
305
|
+
// Following the remedy builds, and the check from there passes.
|
|
306
|
+
const built = await runCli(['theme', 'build', ...buildArgs], runDir);
|
|
307
|
+
expect(built.code).toBe(0);
|
|
308
|
+
for (const output of OUTPUTS) {
|
|
309
|
+
expect(fs.existsSync(path.join(dir, 'themes', output))).toBe(true);
|
|
310
|
+
}
|
|
311
|
+
const checked = await runCli(
|
|
312
|
+
['--json', 'theme', 'build', ...checkArgs],
|
|
313
|
+
runDir,
|
|
314
|
+
);
|
|
315
|
+
expect(checked.code).toBe(0);
|
|
316
|
+
expect(JSON.parse(checked.stdout).data.upToDate).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
271
319
|
it('fails without publishing a mixed trio when staging a write fails', async () => {
|
|
272
320
|
const dir = makeDir();
|
|
273
321
|
const files = writeFamily(dir);
|
|
@@ -27,8 +27,10 @@
|
|
|
27
27
|
* files; an internal callback lets the CLI make only the process status fail.
|
|
28
28
|
* Human progress is emitted through the shared `logger` (silent by default), so
|
|
29
29
|
* the CLI keeps its exact output while a programmatic caller stays quiet.
|
|
30
|
-
* Every output, explicit or beside the theme file, stays inside
|
|
31
|
-
* (`ERR_PATH_TRAVERSAL` otherwise)
|
|
30
|
+
* Every output a build writes, explicit or beside the theme file, stays inside
|
|
31
|
+
* `cwd` (`ERR_PATH_TRAVERSAL` otherwise). Check mode writes nothing, so it
|
|
32
|
+
* compares outputs outside `cwd` read-only; only a relative `out` path that
|
|
33
|
+
* escapes `cwd` is refused there too.
|
|
32
34
|
*
|
|
33
35
|
* The installed `@astryxdesign/core` is an independently versioned optional
|
|
34
36
|
* peer, so it can be older than the CLI. A theme that uses only baseline
|
|
@@ -60,7 +62,10 @@ import {isBuiltin} from 'node:module';
|
|
|
60
62
|
import {fileURLToPath, pathToFileURL} from 'node:url';
|
|
61
63
|
import {parse} from '@babel/parser';
|
|
62
64
|
import {createJiti} from 'jiti';
|
|
63
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
formatCliCommand,
|
|
67
|
+
getCliInvocation,
|
|
68
|
+
} from '../../../foundation/env/package-manager.mjs';
|
|
64
69
|
import {
|
|
65
70
|
CLI_ROOT,
|
|
66
71
|
findCoreDir,
|
|
@@ -568,6 +573,86 @@ function confineOutputs(writes, root, remedy) {
|
|
|
568
573
|
for (const write of writes) confineOutput(write.dest, root, remedy);
|
|
569
574
|
}
|
|
570
575
|
|
|
576
|
+
/** @param {string} dest absolute output path @param {string} root @returns {boolean} whether a build at `root` refuses to write `dest` */
|
|
577
|
+
function refusedOutput(dest, root) {
|
|
578
|
+
try {
|
|
579
|
+
confineOutput(dest, root, '');
|
|
580
|
+
return false;
|
|
581
|
+
} catch (err) {
|
|
582
|
+
if (
|
|
583
|
+
err instanceof AstryxError &&
|
|
584
|
+
err.code === ERROR_CODES.ERR_PATH_TRAVERSAL
|
|
585
|
+
)
|
|
586
|
+
return true;
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** @param {string[]} dirs absolute directories @returns {string | null} the deepest directory containing all of them, or null across roots */
|
|
592
|
+
function commonDirectory(dirs) {
|
|
593
|
+
let common = dirs[0];
|
|
594
|
+
for (const dir of dirs.slice(1)) {
|
|
595
|
+
while (
|
|
596
|
+
path.relative(common, dir).startsWith('..') ||
|
|
597
|
+
path.isAbsolute(path.relative(common, dir))
|
|
598
|
+
) {
|
|
599
|
+
const parent = path.dirname(common);
|
|
600
|
+
if (parent === common) return null;
|
|
601
|
+
common = parent;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return common;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** @param {string} value @returns {string} `value` as one shell word */
|
|
608
|
+
function shellWord(value) {
|
|
609
|
+
if (/^[A-Za-z0-9@%+=:,./_-]+$/.test(value)) return value;
|
|
610
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* How a stale check report ends when outputs sit outside the write root. A
|
|
615
|
+
* build there refuses them, and each output's `Source:` header records the
|
|
616
|
+
* directory its build ran from, so the build and the check both run from the
|
|
617
|
+
* nearest directory that contains every theme file and output.
|
|
618
|
+
* @param {Array<{dest: string}>} writes
|
|
619
|
+
* @param {string[]} sources absolute theme files the build reads
|
|
620
|
+
* @param {string} root the directory a build here keeps its writes inside
|
|
621
|
+
* @param {(relative: (file: string) => string) => string[]} args the arguments after `theme build`, given each path relative to the directory to run from
|
|
622
|
+
* @returns {string[] | null} report lines, or null when every output is inside `root`
|
|
623
|
+
*/
|
|
624
|
+
function outsideCheckRemedy(writes, sources, root, args) {
|
|
625
|
+
if (!writes.some(write => refusedOutput(write.dest, root))) return null;
|
|
626
|
+
const real = (/** @type {string} */ file) => realLocation(path.resolve(file));
|
|
627
|
+
const dir = commonDirectory([
|
|
628
|
+
...sources.map(file => path.dirname(real(file))),
|
|
629
|
+
...writes.map(write => path.dirname(real(write.dest))),
|
|
630
|
+
]);
|
|
631
|
+
if (!dir) {
|
|
632
|
+
return [
|
|
633
|
+
'These outputs are outside the working directory, so a build here refuses them. Build and check them from one directory that contains the theme files and the outputs.',
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
const command = (/** @type {string[]} */ extra) =>
|
|
637
|
+
formatCliCommand(
|
|
638
|
+
[
|
|
639
|
+
'theme',
|
|
640
|
+
'build',
|
|
641
|
+
...args(file => path.relative(dir, real(file))),
|
|
642
|
+
...extra,
|
|
643
|
+
]
|
|
644
|
+
.map(shellWord)
|
|
645
|
+
.join(' '),
|
|
646
|
+
dir,
|
|
647
|
+
);
|
|
648
|
+
return [
|
|
649
|
+
'These outputs are outside the working directory, so a build here refuses them, and each output records the directory it was built from. Build and check them from a directory that contains them:',
|
|
650
|
+
` cd ${shellWord(dir)}`,
|
|
651
|
+
` ${command([])}`,
|
|
652
|
+
` ${command(['--check'])}`,
|
|
653
|
+
];
|
|
654
|
+
}
|
|
655
|
+
|
|
571
656
|
/** @param {Array<{dest: string, content: string}>} writes */
|
|
572
657
|
function writeBuildOutputs(writes) {
|
|
573
658
|
if (writes.length === 0) return;
|
|
@@ -2859,9 +2944,13 @@ async function themeBuildInternal(
|
|
|
2859
2944
|
const writeRoot = options.__writeRoot ?? cwd;
|
|
2860
2945
|
let outPath;
|
|
2861
2946
|
if (options.out) {
|
|
2862
|
-
// Relative or absolute, the explicit output stays inside the write root.
|
|
2863
2947
|
outPath = path.resolve(cwd, options.out);
|
|
2864
|
-
|
|
2948
|
+
// A build keeps the explicit output inside the write root, relative or
|
|
2949
|
+
// absolute. Check mode writes nothing, so it refuses only a relative path
|
|
2950
|
+
// that escapes cwd.
|
|
2951
|
+
if (!options.check || !path.isAbsolute(options.out)) {
|
|
2952
|
+
confineOutput(outPath, writeRoot, OUT_REMEDY, options.out);
|
|
2953
|
+
}
|
|
2865
2954
|
} else {
|
|
2866
2955
|
outPath = path.join(path.dirname(filePath), `${baseName}.css`);
|
|
2867
2956
|
}
|
|
@@ -2957,10 +3046,12 @@ async function themeBuildInternal(
|
|
|
2957
3046
|
writes.push({dest: variantDtsPath, content: variantContent});
|
|
2958
3047
|
}
|
|
2959
3048
|
|
|
2960
|
-
// Every output stays inside cwd, including the defaults
|
|
2961
|
-
// file. Check mode
|
|
2962
|
-
//
|
|
2963
|
-
|
|
3049
|
+
// Every output a build writes stays inside cwd, including the defaults
|
|
3050
|
+
// beside the theme file. Check mode writes nothing, so it compares outputs
|
|
3051
|
+
// outside cwd read-only.
|
|
3052
|
+
if (!options.check) {
|
|
3053
|
+
confineOutputs(writes, writeRoot, options.out ? OUT_REMEDY : SOURCE_REMEDY);
|
|
3054
|
+
}
|
|
2964
3055
|
|
|
2965
3056
|
// Check mode: compare generated content against what's on disk instead of
|
|
2966
3057
|
// writing. A file is "stale" if it's missing or its content differs once the
|
|
@@ -2980,7 +3071,10 @@ async function themeBuildInternal(
|
|
|
2980
3071
|
` ${s.reason === 'missing' ? 'missing' : 'stale'}: ${s.path}`,
|
|
2981
3072
|
);
|
|
2982
3073
|
}
|
|
2983
|
-
|
|
3074
|
+
// prettier-ignore
|
|
3075
|
+
const remedy = outsideCheckRemedy(writes, [filePath], writeRoot, relative => [relative(filePath), ...(options.out ? ['--out', relative(outPath)] : []), ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
|
|
3076
|
+
if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
|
|
3077
|
+
else logger.error(`\n Rebuild with: ${buildCommand}`);
|
|
2984
3078
|
}
|
|
2985
3079
|
return {
|
|
2986
3080
|
type: 'theme.build.check',
|
|
@@ -3093,7 +3187,7 @@ function printBuildTrailer(trailer) {
|
|
|
3093
3187
|
logger.log(`
|
|
3094
3188
|
Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
|
|
3095
3189
|
|
|
3096
|
-
import { Theme } from '@astryxdesign/core';
|
|
3190
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3097
3191
|
import { ${trailer.exportName} } from '${trailer.jsImport}';
|
|
3098
3192
|
import '${trailer.cssImport}';
|
|
3099
3193
|
|
|
@@ -3103,7 +3197,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
|
|
|
3103
3197
|
|
|
3104
3198
|
Or with a <link> tag:
|
|
3105
3199
|
|
|
3106
|
-
import { Theme } from '@astryxdesign/core';
|
|
3200
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3107
3201
|
import { ${trailer.exportName} } from '${trailer.jsImport}';
|
|
3108
3202
|
|
|
3109
3203
|
<link rel="stylesheet" href="${trailer.cssImport}" />
|
|
@@ -3129,7 +3223,7 @@ export function printBatchTrailer(trailers) {
|
|
|
3129
3223
|
logger.log(`
|
|
3130
3224
|
Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
|
|
3131
3225
|
|
|
3132
|
-
import { Theme } from '@astryxdesign/core';
|
|
3226
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3133
3227
|
import { ${first.exportName} } from '${first.jsImport}';
|
|
3134
3228
|
import '${first.cssImport}';
|
|
3135
3229
|
|
|
@@ -3139,7 +3233,7 @@ Install in your app (paths are relative to a file in src/; adjust if yours lives
|
|
|
3139
3233
|
|
|
3140
3234
|
Or with a <link> tag:
|
|
3141
3235
|
|
|
3142
|
-
import { Theme } from '@astryxdesign/core';
|
|
3236
|
+
import { Theme } from '@astryxdesign/core/theme';
|
|
3143
3237
|
import { ${first.exportName} } from '${first.jsImport}';
|
|
3144
3238
|
|
|
3145
3239
|
<link rel="stylesheet" href="${first.cssImport}" />
|
|
@@ -3506,8 +3600,8 @@ export async function themeBuildFamily(
|
|
|
3506
3600
|
js: path.relative(cwd, jsPath),
|
|
3507
3601
|
dts: path.relative(cwd, dtsPath),
|
|
3508
3602
|
};
|
|
3509
|
-
//
|
|
3510
|
-
confineOutputs(writes, cwd, FAMILY_REMEDY);
|
|
3603
|
+
// A family build's outputs stay inside cwd too; check mode only reads them.
|
|
3604
|
+
if (!options.check) confineOutputs(writes, cwd, FAMILY_REMEDY);
|
|
3511
3605
|
if (options.check) {
|
|
3512
3606
|
const stale = staleBuildOutputs(writes, cwd);
|
|
3513
3607
|
const upToDate = stale.length === 0;
|
|
@@ -3524,7 +3618,10 @@ export async function themeBuildFamily(
|
|
|
3524
3618
|
` ${entry.reason === 'missing' ? 'missing' : 'stale'}: ${entry.path}`,
|
|
3525
3619
|
);
|
|
3526
3620
|
}
|
|
3527
|
-
|
|
3621
|
+
// prettier-ignore
|
|
3622
|
+
const remedy = outsideCheckRemedy(writes, members.map(member => member.filePath), cwd, relative => ['--family', ...members.map(member => relative(member.filePath)), '--family-key', baseName, ...(options.iconsSpecifier ? ['--icons-specifier', options.iconsSpecifier] : [])]);
|
|
3623
|
+
if (remedy) logger.error(`\n ${remedy.join('\n ')}`);
|
|
3624
|
+
else logger.error(`\n Rebuild with: ${buildCommand}`);
|
|
3528
3625
|
}
|
|
3529
3626
|
return {
|
|
3530
3627
|
type: 'theme.build.check',
|
|
@@ -58,13 +58,13 @@ export const doc = {
|
|
|
58
58
|
name: 'options.out',
|
|
59
59
|
type: 'string',
|
|
60
60
|
description:
|
|
61
|
-
'Override the output CSS path, relative or absolute;
|
|
61
|
+
'Override the output CSS path, relative or absolute; a build refuses a path outside cwd, and check mode refuses only a relative path that escapes cwd. The .js, .d.ts and any .variants.d.ts are written in the same directory, named after the theme (<name>.js), not after the CSS file.',
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: 'options.check',
|
|
65
65
|
type: 'boolean',
|
|
66
66
|
description:
|
|
67
|
-
'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard.',
|
|
67
|
+
'Compile in memory and compare each output against what is on disk instead of writing: the CI drift guard. Because it writes nothing, it also compares outputs outside cwd; when those are stale, the report names the directory that contains them and the build and check commands to run from there, since a build refuses them and each output records the directory it was built from.',
|
|
68
68
|
default: 'false',
|
|
69
69
|
},
|
|
70
70
|
{
|
|
@@ -77,7 +77,7 @@ export const doc = {
|
|
|
77
77
|
name: 'ctx.cwd',
|
|
78
78
|
type: 'string',
|
|
79
79
|
description:
|
|
80
|
-
'Directory the theme file, a relative out path, and the returned output paths resolve against. Every output is
|
|
80
|
+
'Directory the theme file, a relative out path, and the returned output paths resolve against. Every output a build writes is inside it, including the default outputs beside the theme file.',
|
|
81
81
|
default: 'process.cwd()',
|
|
82
82
|
},
|
|
83
83
|
],
|
|
@@ -105,7 +105,7 @@ export const doc = {
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
code: 'ERR_PATH_TRAVERSAL',
|
|
108
|
-
when: 'the theme name contains a path separator or traversal marker, or
|
|
108
|
+
when: 'the theme name contains a path separator or traversal marker, or a written output resolves outside cwd (including through a symlink): an `out` path, relative or absolute, or the default outputs beside a theme file outside cwd. Check mode writes nothing, so it refuses only a relative `out` path that escapes cwd',
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
code: 'ERR_CORE_NOT_FOUND',
|
package/assets/docs/README.md
CHANGED
|
@@ -51,6 +51,16 @@ The material is usually good; the finding is placement, not quality. It goes in
|
|
|
51
51
|
|
|
52
52
|
Worked example: a responsive-and-interaction readiness rubric is grading criteria → **Component-Audit-Rubric**, or **Component-Lifecycle** if it is a promotion gate.
|
|
53
53
|
|
|
54
|
+
## Imports in code samples
|
|
55
|
+
|
|
56
|
+
Import each Core export from the path `astryx component <Name>` prints: the
|
|
57
|
+
component's own entry when Core has one (`@astryxdesign/core/VStack`,
|
|
58
|
+
`@astryxdesign/core/Heading`), and `@astryxdesign/core/theme` for `Theme`,
|
|
59
|
+
`defineTheme` and the rest of the theme API. A reader copies one sample and then
|
|
60
|
+
reads `astryx component`, and two paths for one export read as two different
|
|
61
|
+
things. The no-build CDN page is the exception: its import map names only the
|
|
62
|
+
package root.
|
|
63
|
+
|
|
54
64
|
## Sections are read one at a time
|
|
55
65
|
|
|
56
66
|
`astryx docs <topic> --index` lists a topic's sections, and readers then open
|
|
@@ -120,7 +120,7 @@ export const docs = {
|
|
|
120
120
|
['ocean.variants.d.ts', "(Optional) Module augmentations for custom component prop values"],
|
|
121
121
|
]},
|
|
122
122
|
{type: 'prose', text: 'The `__built: true` flag tells Theme to skip runtime `<style>` injection; the CSS file handles it. Load the generated CSS wherever you load the module.'},
|
|
123
|
-
{type: 'code', lang: 'tsx', label: 'Using a custom built theme', code: "import {Theme} from '@astryxdesign/core';\nimport {oceanTheme} from './themes/ocean';\nimport './themes/ocean.css';\n\n<Theme theme={oceanTheme}>\n <App />\n</Theme>"},
|
|
123
|
+
{type: 'code', lang: 'tsx', label: 'Using a custom built theme', code: "import {Theme} from '@astryxdesign/core/theme';\nimport {oceanTheme} from './themes/ocean';\nimport './themes/ocean.css';\n\n<Theme theme={oceanTheme}>\n <App />\n</Theme>"},
|
|
124
124
|
{type: 'prose', text: "After upgrading Astryx, rerun `astryx theme build` for every custom prebuilt theme. Deploy the regenerated files together. The runtime intentionally trusts `__built: true` and will not repair stale CSS from an older build. The build also warns when the theme names font families it does not load. See {@link generic:font-setup} for the full recipe."},
|
|
125
125
|
{type: 'prose', text: 'For the runtime vs built tradeoff, see the Runtime vs Built section of {@link generic:use-a-theme}.'},
|
|
126
126
|
],
|