@astryxdesign/cli 0.6.3-canary.d0d49a4 → 0.6.3-canary.dea6813
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 +32 -18
- package/api/build/build.doc.mjs +3 -3
- package/api/build/build.test.mjs +14 -3
- package/api/build/build.type.d.mts +35 -1
- package/api/build/build.type.mjs +24 -3
- package/api/build/help/help.d.mts +8 -5
- package/api/build/help/help.mjs +56 -6
- package/api/component/component.doc.mjs +11 -2
- package/api/component/component.mjs +29 -0
- package/api/component/component.test.mjs +38 -0
- package/api/component/component.type.d.mts +11 -1
- package/api/component/component.type.mjs +8 -1
- package/api/discover/discover.doc.mjs +1 -1
- package/api/gap-report/gap-report.doc.mjs +7 -4
- package/api/init/init.doc.mjs +4 -0
- package/api/init/init.test.mjs +41 -1
- package/api/init/remove/remove.mjs +1 -1
- package/api/init/run/run.mjs +20 -10
- package/api/integration/add-contribution.component-names.test.mjs +120 -0
- package/api/integration/add-contribution.mjs +11 -0
- package/api/integration/add-contribution.test.mjs +80 -0
- package/api/integration/integrationAdd.doc.mjs +7 -4
- package/api/integration/integrationAddComponent.doc.mjs +1 -1
- package/api/integration/validate-integration.mjs +2 -0
- package/api/integration/validate-integration.test.mjs +91 -0
- package/api/integration/validateIntegration.doc.mjs +1 -1
- package/api/json/envelope-types.test.mjs +76 -0
- package/api/json/parseResponse.doc.mjs +2 -2
- package/api/layout/expand/expand.mjs +7 -5
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.mjs +2 -1
- package/api/layout/layoutExpand.doc.mjs +1 -1
- package/api/search/search-return-type.test.mjs +54 -0
- package/api/search/search.d.mts +2 -9
- package/api/search/search.doc.mjs +5 -1
- package/api/search/search.mjs +10 -3
- package/api/search/search.test.mjs +34 -0
- package/api/swizzle/copy/copy.mjs +28 -11
- package/api/swizzle/swizzle.doc.mjs +1 -1
- package/api/template/copy/copy.mjs +17 -23
- package/api/template/copy/copy.test.mjs +17 -0
- package/api/template/template.doc.mjs +2 -1
- package/api/theme/add/add.binary.test.mjs +91 -0
- package/api/theme/add/add.mjs +20 -3
- package/api/theme/add/add.staging.test.mjs +66 -0
- package/api/theme/build/build.mjs +114 -37
- package/api/theme/build/build.public-component-vars.test.mjs +1 -1
- package/api/theme/build/build.receipt-doc.test.mjs +111 -0
- package/api/theme/build/font-warning.mjs +3 -3
- package/api/theme/build/font-warning.test.mjs +5 -2
- package/api/theme/palette/generate/generate.mjs +7 -2
- package/api/theme/palette/generate/generate.test.mjs +96 -0
- package/api/theme/palette/generate/generator.mjs +8 -1
- package/api/theme/palette/generate/generator.test.mjs +10 -0
- package/api/theme/template/template.mjs +11 -2
- package/api/theme/template/template.test.mjs +15 -0
- package/api/theme/themeBuild.doc.mjs +7 -4
- package/api/theme/themeTemplate.doc.mjs +4 -1
- package/api/upgrade/_adapter.mjs +27 -4
- package/api/upgrade/list/list.mjs +2 -1
- package/api/upgrade/list/list.test.mjs +73 -0
- package/api/upgrade/status/status.mjs +2 -2
- package/api/upgrade/upgrade.doc.mjs +2 -1
- package/api/upgrade/upgrade.type.d.mts +4 -0
- package/api/upgrade/upgrade.type.mjs +1 -0
- package/assets/codemods/term-log.mjs +32 -8
- package/assets/codemods/term-log.test.mjs +19 -1
- package/assets/codemods/transforms/v0.6.0/__tests__/next-codemods.test.mjs +47 -0
- package/assets/codemods/transforms/v0.6.0/rename-resizable-pixel-bounds.mjs +57 -10
- package/assets/templates/pages/table-tree/page.tsx +1704 -0
- package/assets/templates/pages/table-tree/template.doc.mjs +12 -0
- package/authoring/config/config.doc.mjs +1 -1
- package/authoring/config/debug-composition.test.mjs +92 -0
- package/authoring/config/type.ts +15 -3
- package/clients/cli/command-load-failure.test.mjs +83 -0
- package/clients/cli/commands/blog.mjs +23 -8
- package/clients/cli/commands/blog.test.mjs +42 -1
- package/clients/cli/commands/build-theme.ascii-output.test.mjs +161 -0
- package/clients/cli/commands/build-theme.flag-docs.test.mjs +110 -0
- package/clients/cli/commands/build-theme.mjs +5 -5
- package/clients/cli/commands/build-theme.path-safety.test.mjs +86 -1
- package/clients/cli/commands/build-theme.variants.test.mjs +76 -0
- package/clients/cli/commands/build.doc.mjs +4 -1
- package/clients/cli/commands/build.exit-codes-doc.test.mjs +50 -0
- package/clients/cli/commands/build.mjs +32 -47
- package/clients/cli/commands/build.playbook.test.mjs +75 -0
- package/clients/cli/commands/build.text-fields.test.mjs +40 -0
- package/clients/cli/commands/component/index.mjs +2 -7
- package/clients/cli/commands/component-package.test.mjs +18 -0
- package/clients/cli/commands/component-resolution.test.mjs +21 -0
- package/clients/cli/commands/component.test.mjs +19 -0
- package/clients/cli/commands/detail-levels.test.mjs +2 -2
- package/clients/cli/commands/discover.broken-integration.test.mjs +48 -0
- package/clients/cli/commands/discover.components-flag.test.mjs +97 -0
- package/clients/cli/commands/discover.doc.mjs +4 -2
- package/clients/cli/commands/discover.mjs +3 -3
- package/clients/cli/commands/discover.text-projection.test.mjs +103 -0
- package/clients/cli/commands/doctor-integration.package-json.test.mjs +53 -0
- package/clients/cli/commands/doctor.mjs +6 -16
- package/clients/cli/commands/doctor.test.mjs +42 -0
- package/clients/cli/commands/gap-report.doc.mjs +16 -5
- package/clients/cli/commands/gap-report.test.mjs +72 -0
- package/clients/cli/commands/hook/index.mjs +7 -17
- package/clients/cli/commands/hook.text-projection.test.mjs +45 -0
- package/clients/cli/commands/init.doc.mjs +19 -8
- package/clients/cli/commands/init.flag-help.test.mjs +153 -0
- package/clients/cli/commands/integration-add.controls.test.mjs +132 -0
- package/clients/cli/commands/integration-add.doc.mjs +21 -5
- package/clients/cli/commands/interactive-guard.test.mjs +101 -24
- package/clients/cli/commands/json-contract.test.mjs +33 -0
- package/clients/cli/commands/layout-check.doc.mjs +14 -3
- package/clients/cli/commands/layout-expand.doc.mjs +21 -4
- package/clients/cli/commands/layout.doc.mjs +2 -2
- package/clients/cli/commands/layout.mjs +21 -9
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/no-prompt-wording.test.mjs +94 -0
- package/clients/cli/commands/search.doc.mjs +6 -3
- package/clients/cli/commands/search.mjs +17 -7
- package/clients/cli/commands/search.test.mjs +75 -0
- package/clients/cli/commands/swizzle.doc.mjs +2 -1
- package/clients/cli/commands/swizzle.path-safety.test.mjs +42 -0
- package/clients/cli/commands/template.doc.mjs +28 -6
- package/clients/cli/commands/template.flag-help.test.mjs +117 -0
- package/clients/cli/commands/template.path-help.test.mjs +40 -0
- package/clients/cli/commands/theme-add.doc.mjs +2 -1
- package/clients/cli/commands/theme-build.doc.mjs +7 -6
- package/clients/cli/commands/theme-palette-generate.test.mjs +19 -0
- package/clients/cli/commands/theme-template.behavior.test.mjs +12 -0
- package/clients/cli/commands/theme-template.doc.mjs +1 -1
- package/clients/cli/commands/upgrade.ascii-output.test.mjs +87 -0
- package/clients/cli/commands/upgrade.doc.mjs +17 -6
- package/clients/cli/commands/upgrade.flag-help.test.mjs +188 -0
- package/clients/cli/commands/upgrade.hook-output.test.mjs +88 -0
- package/clients/cli/index.mjs +13 -30
- package/clients/cli/latest-version-env.test.mjs +50 -0
- package/clients/cli/lib/cli-error.test.mjs +7 -0
- package/clients/cli/lib/component-format.mjs +9 -9
- package/clients/cli/lib/component-format.test.mjs +1 -1
- package/clients/cli/lib/define-command.mjs +32 -6
- package/clients/cli/lib/doc-text-ascii.test.mjs +82 -0
- package/clients/cli/lib/exit-codes.test.mjs +97 -0
- package/clients/cli/lib/hook-format.mjs +5 -5
- package/clients/cli/lib/json-shim.mjs +38 -2
- package/clients/cli/lib/json-shim.test.mjs +83 -0
- package/clients/cli/lib/manifest.d.ts +2 -0
- package/clients/cli/lib/manifest.mjs +22 -0
- package/clients/cli/lib/manifest.test.mjs +17 -0
- package/foundation/agent-docs/agent-docs.d.mts +4 -0
- package/foundation/agent-docs/agent-docs.mjs +69 -3
- package/foundation/agent-docs/agent-docs.path-safety.test.mjs +266 -4
- package/foundation/config/integration-debug.test.mjs +28 -3
- package/foundation/config/project.mjs +123 -9
- package/foundation/config/project.test.mjs +126 -0
- package/foundation/discovery/template-adapter.d.mts +14 -5
- package/foundation/discovery/template-adapter.fixture-refs.test.mjs +248 -0
- package/foundation/discovery/template-adapter.integration-isolation.test.mjs +94 -0
- package/foundation/discovery/template-adapter.mjs +292 -25
- package/foundation/discovery/theming-targets.test.mjs +4 -0
- package/foundation/fs/module-loader.d.mts +1 -0
- package/foundation/fs/module-loader.mjs +50 -1
- package/foundation/fs/module-loader.stdout.test.mjs +332 -0
- package/foundation/fs/path-safety.d.mts +3 -2
- package/foundation/fs/path-safety.mjs +49 -19
- package/foundation/fs/path-safety.test.mjs +50 -0
- package/foundation/fs/publish-file-hardlink-unavailable.test.mjs +129 -94
- package/foundation/integrations/autolink.mjs +6 -1
- package/foundation/response/base.d.ts +8 -4
- package/foundation/response/error-codes.doc.mjs +1 -2
- package/foundation/response/error-codes.mjs +1 -1
- package/foundation/response/error-codes.test.mjs +83 -0
- package/foundation/response/json-contract.test.mjs +11 -0
- package/foundation/response/json.d.mts +4 -2
- package/foundation/response/json.mjs +8 -10
- package/foundation/response/response-types.doc.mjs +3 -3
- package/foundation/xle/expand.d.mts +2 -0
- package/foundation/xle/expand.mjs +3 -2
- package/foundation/xle/expand.test.mjs +54 -0
- package/package.json +9 -9
- package/clients/cli/lib/update-check.mjs +0 -83
- package/clients/cli/lib/update-check.test.mjs +0 -137
- package/clients/cli/update-hint-commands.test.mjs +0 -54
|
@@ -224,10 +224,10 @@ function staleBuildOutputs(writes, cwd) {
|
|
|
224
224
|
/** @param {Array<{dest: string, content: string}>} writes */
|
|
225
225
|
function writeBuildOutputs(writes) {
|
|
226
226
|
if (writes.length === 0) return;
|
|
227
|
-
fs.mkdirSync(path.dirname(writes[0].dest), {recursive: true});
|
|
228
227
|
/** @type {Array<{tmp: string, dest: string}>} */
|
|
229
228
|
const staged = [];
|
|
230
229
|
try {
|
|
230
|
+
fs.mkdirSync(path.dirname(writes[0].dest), {recursive: true});
|
|
231
231
|
for (const write of writes) {
|
|
232
232
|
const tmp = `${write.dest}.${process.pid}.tmp`;
|
|
233
233
|
fs.writeFileSync(tmp, write.content);
|
|
@@ -1871,7 +1871,7 @@ function validatePrivateVars(themeDef) {
|
|
|
1871
1871
|
if (prop.startsWith('--_')) {
|
|
1872
1872
|
errors.push(
|
|
1873
1873
|
`Component "${component}" (${[key, ...path].join(' ')}) sets private var "${prop}". ` +
|
|
1874
|
-
`Private vars (--_*) are internal
|
|
1874
|
+
`Private vars (--_*) are internal; use standard CSS properties ` +
|
|
1875
1875
|
`(e.g. borderRadius, padding) instead. The pipeline expands them automatically.`,
|
|
1876
1876
|
);
|
|
1877
1877
|
}
|
|
@@ -1931,20 +1931,7 @@ function getThemeComponentLayers(themeDef) {
|
|
|
1931
1931
|
*/
|
|
1932
1932
|
function validateCustomHeadingTypes(themeDef) {
|
|
1933
1933
|
const layers = getThemeComponentLayers(themeDef);
|
|
1934
|
-
const customTypes =
|
|
1935
|
-
for (const {components} of layers) {
|
|
1936
|
-
const headingRules = components.heading;
|
|
1937
|
-
if (!headingRules || typeof headingRules !== 'object') continue;
|
|
1938
|
-
|
|
1939
|
-
for (const key of Object.keys(headingRules)) {
|
|
1940
|
-
for (const pair of key.split('+')) {
|
|
1941
|
-
const colon = pair.indexOf(':');
|
|
1942
|
-
if (colon === -1 || pair.slice(0, colon) !== 'type') continue;
|
|
1943
|
-
const value = pair.slice(colon + 1);
|
|
1944
|
-
if (value && !BUILTIN_HEADING_TYPES.has(value)) customTypes.add(value);
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1934
|
+
const customTypes = customHeadingTypes(themeDef);
|
|
1948
1935
|
|
|
1949
1936
|
const errors = [];
|
|
1950
1937
|
for (const type of customTypes) {
|
|
@@ -1971,10 +1958,65 @@ function validateCustomHeadingTypes(themeDef) {
|
|
|
1971
1958
|
return errors;
|
|
1972
1959
|
}
|
|
1973
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* Custom Heading type names declared on the root or media-surface component
|
|
1963
|
+
* maps: the names the generated `HeadingTypeMap` augmentation enrolls.
|
|
1964
|
+
*
|
|
1965
|
+
* @param {{components?: Record<string, Record<string, unknown>>}} themeDef
|
|
1966
|
+
* @returns {Set<string>}
|
|
1967
|
+
*/
|
|
1968
|
+
function customHeadingTypes(themeDef) {
|
|
1969
|
+
/** @type {Set<string>} */
|
|
1970
|
+
const customTypes = new Set();
|
|
1971
|
+
for (const {components} of getThemeComponentLayers(themeDef)) {
|
|
1972
|
+
const headingRules = components.heading;
|
|
1973
|
+
if (!headingRules || typeof headingRules !== 'object') continue;
|
|
1974
|
+
|
|
1975
|
+
for (const key of Object.keys(headingRules)) {
|
|
1976
|
+
for (const pair of key.split('+')) {
|
|
1977
|
+
const colon = pair.indexOf(':');
|
|
1978
|
+
if (colon === -1 || pair.slice(0, colon) !== 'type') continue;
|
|
1979
|
+
const value = pair.slice(colon + 1);
|
|
1980
|
+
if (value && !BUILTIN_HEADING_TYPES.has(value)) customTypes.add(value);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return customTypes;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/**
|
|
1988
|
+
* Custom Heading types with no standalone rule in the generated CSS. A type
|
|
1989
|
+
* whose every declaration the compiler dropped still passes the source check,
|
|
1990
|
+
* and its augmentation would then name a type that has no styles.
|
|
1991
|
+
*
|
|
1992
|
+
* @param {Set<string>} types
|
|
1993
|
+
* @param {string} css - Generated root component and media-surface rules.
|
|
1994
|
+
* @returns {string[]}
|
|
1995
|
+
*/
|
|
1996
|
+
function headingTypesWithoutEmittedRule(types, css) {
|
|
1997
|
+
/** @type {string[]} */
|
|
1998
|
+
const missing = [];
|
|
1999
|
+
for (const type of types) {
|
|
2000
|
+
const selector = `.astryx-heading[data-type="${type}"]`;
|
|
2001
|
+
let emitted = false;
|
|
2002
|
+
for (
|
|
2003
|
+
let at = css.indexOf(selector);
|
|
2004
|
+
at !== -1 && !emitted;
|
|
2005
|
+
at = css.indexOf(selector, at + 1)
|
|
2006
|
+
) {
|
|
2007
|
+
// A following attribute is a combined selector, not the standalone rule.
|
|
2008
|
+
emitted = css[at + selector.length] !== '[';
|
|
2009
|
+
}
|
|
2010
|
+
if (!emitted) missing.push(type);
|
|
2011
|
+
}
|
|
2012
|
+
return missing;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1974
2015
|
/**
|
|
1975
2016
|
* Return true when a style object contains a declaration that the theme
|
|
1976
|
-
* generator can emit. Empty pseudo blocks do not count: they
|
|
1977
|
-
* and would leave the generated type augmentation
|
|
2017
|
+
* generator can emit. Empty pseudo blocks and blank values do not count: they
|
|
2018
|
+
* produce no usable CSS and would leave the generated type augmentation
|
|
2019
|
+
* without a usable rule.
|
|
1978
2020
|
*
|
|
1979
2021
|
* @param {unknown} styles
|
|
1980
2022
|
* @returns {boolean}
|
|
@@ -1988,7 +2030,12 @@ function hasUsableStyleDeclaration(styles) {
|
|
|
1988
2030
|
if (hasUsableStyleDeclaration(value)) return true;
|
|
1989
2031
|
continue;
|
|
1990
2032
|
}
|
|
1991
|
-
if (
|
|
2033
|
+
if (
|
|
2034
|
+
value !== undefined &&
|
|
2035
|
+
value !== null &&
|
|
2036
|
+
typeof value !== 'object' &&
|
|
2037
|
+
String(value).trim() !== ''
|
|
2038
|
+
) {
|
|
1992
2039
|
return true;
|
|
1993
2040
|
}
|
|
1994
2041
|
}
|
|
@@ -2163,14 +2210,14 @@ async function themeBuildInternal(
|
|
|
2163
2210
|
const noticeMessages = [];
|
|
2164
2211
|
for (const w of warnings) {
|
|
2165
2212
|
warningMessages.push(w);
|
|
2166
|
-
logger.warn(`
|
|
2213
|
+
logger.warn(` [warn] ${w}`);
|
|
2167
2214
|
}
|
|
2168
2215
|
|
|
2169
2216
|
// Validate no private vars are set directly
|
|
2170
2217
|
const privateVarErrors = validatePrivateVars(themeDef);
|
|
2171
2218
|
for (const e of privateVarErrors) {
|
|
2172
2219
|
warningMessages.push(e);
|
|
2173
|
-
logger.error(`
|
|
2220
|
+
logger.error(` [error] ${e}`);
|
|
2174
2221
|
}
|
|
2175
2222
|
if (privateVarErrors.length > 0) {
|
|
2176
2223
|
logger.error(
|
|
@@ -2196,7 +2243,7 @@ async function themeBuildInternal(
|
|
|
2196
2243
|
// capability exports are checked against what the theme actually asks for.
|
|
2197
2244
|
if (!_defineTheme || !_generateThemeRulesSplit) {
|
|
2198
2245
|
throw new AstryxError(
|
|
2199
|
-
'Could not load @astryxdesign/core/theme
|
|
2246
|
+
'Could not load @astryxdesign/core/theme: `astryx theme build` requires a ' +
|
|
2200
2247
|
'built, resolvable @astryxdesign/core so it emits the same CSS as the ' +
|
|
2201
2248
|
'runtime <Theme>. Build @astryxdesign/core first (e.g. `pnpm -F @astryxdesign/core ' +
|
|
2202
2249
|
'build`)' +
|
|
@@ -2385,10 +2432,29 @@ async function themeBuildInternal(
|
|
|
2385
2432
|
for (const message of droppedDeclarations) {
|
|
2386
2433
|
const w = `Declaration ${message}. The generated CSS omits it; fix the value in the theme source.`;
|
|
2387
2434
|
warningMessages.push(w);
|
|
2388
|
-
logger.warn(`
|
|
2435
|
+
logger.warn(` [warn] ${w}`);
|
|
2436
|
+
}
|
|
2437
|
+
const unemittedHeadingTypes = headingTypesWithoutEmittedRule(
|
|
2438
|
+
customHeadingTypes(themeDef),
|
|
2439
|
+
`${component.join('\n')}\n${onMediaCss}`,
|
|
2440
|
+
);
|
|
2441
|
+
if (unemittedHeadingTypes.length > 0) {
|
|
2442
|
+
throw new AstryxError(
|
|
2443
|
+
unemittedHeadingTypes
|
|
2444
|
+
.map(
|
|
2445
|
+
type =>
|
|
2446
|
+
`Custom Heading type "${type}" has no declaration the theme ` +
|
|
2447
|
+
`compiler could emit: every declaration in its standalone ` +
|
|
2448
|
+
`"type:${type}" rule was dropped. Give that rule at least one ` +
|
|
2449
|
+
'valid declaration.',
|
|
2450
|
+
)
|
|
2451
|
+
.join('\n'),
|
|
2452
|
+
undefined,
|
|
2453
|
+
ERROR_CODES.ERR_THEME_INVALID,
|
|
2454
|
+
);
|
|
2389
2455
|
}
|
|
2390
2456
|
if (cssParts.length === 0) {
|
|
2391
|
-
logger.log('No overrides found
|
|
2457
|
+
logger.log('No overrides found; nothing to build.');
|
|
2392
2458
|
return null;
|
|
2393
2459
|
}
|
|
2394
2460
|
// The data-token defaults are theme-independent and go in @layer
|
|
@@ -2439,7 +2505,18 @@ async function themeBuildInternal(
|
|
|
2439
2505
|
// Guard: relative paths must not escape cwd via `../`. Absolute paths are
|
|
2440
2506
|
// trusted (the user explicitly controls where output goes, like gcc -o).
|
|
2441
2507
|
if (!path.isAbsolute(options.out)) {
|
|
2442
|
-
|
|
2508
|
+
try {
|
|
2509
|
+
assertWithin(options.out, cwd, {label: 'output path'});
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
if (err instanceof PathSafetyError) {
|
|
2512
|
+
throw new AstryxError(
|
|
2513
|
+
err.message,
|
|
2514
|
+
undefined,
|
|
2515
|
+
ERROR_CODES.ERR_PATH_TRAVERSAL,
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
throw err;
|
|
2519
|
+
}
|
|
2443
2520
|
}
|
|
2444
2521
|
} else {
|
|
2445
2522
|
outPath = path.join(path.dirname(filePath), `${baseName}.css`);
|
|
@@ -2548,10 +2625,10 @@ async function themeBuildInternal(
|
|
|
2548
2625
|
const stale = staleBuildOutputs(writes, cwd);
|
|
2549
2626
|
const upToDate = stale.length === 0;
|
|
2550
2627
|
if (upToDate) {
|
|
2551
|
-
logger.log(`\n
|
|
2628
|
+
logger.log(`\n[ok] Theme outputs are up to date with ${sourceRelative}.`);
|
|
2552
2629
|
} else {
|
|
2553
2630
|
logger.error(
|
|
2554
|
-
`\n
|
|
2631
|
+
`\n[fail] ${stale.length} theme output(s) are out of date with ${sourceRelative}:`,
|
|
2555
2632
|
);
|
|
2556
2633
|
for (const s of stale) {
|
|
2557
2634
|
logger.error(
|
|
@@ -2573,17 +2650,17 @@ async function themeBuildInternal(
|
|
|
2573
2650
|
|
|
2574
2651
|
writeBuildOutputs(writes);
|
|
2575
2652
|
|
|
2576
|
-
logger.log(`\n
|
|
2653
|
+
logger.log(`\n[ok] ${path.relative(cwd, outPath)}`);
|
|
2577
2654
|
logger.log(
|
|
2578
2655
|
` ${tokenCount} token overrides, ${componentCount} component overrides`,
|
|
2579
2656
|
);
|
|
2580
2657
|
logger.log(` ${size} KB`);
|
|
2581
|
-
logger.log(
|
|
2582
|
-
logger.log(
|
|
2658
|
+
logger.log(`[ok] ${path.relative(cwd, jsPath)}`);
|
|
2659
|
+
logger.log(`[ok] ${path.relative(cwd, dtsPath)}`);
|
|
2583
2660
|
if (variantDtsPath && variantDecl) {
|
|
2584
2661
|
const augCount = (variantDecl.match(/': true;/g) || []).length;
|
|
2585
2662
|
logger.log(
|
|
2586
|
-
|
|
2663
|
+
`[ok] ${path.relative(cwd, variantDtsPath)} (${augCount} type augmentations)`,
|
|
2587
2664
|
);
|
|
2588
2665
|
}
|
|
2589
2666
|
|
|
@@ -2593,7 +2670,7 @@ async function themeBuildInternal(
|
|
|
2593
2670
|
const cssImport = importSpecifier(relOutDir, cssBase) + '.css';
|
|
2594
2671
|
const exportName = `${toIdentifier(baseName)}Theme`;
|
|
2595
2672
|
logger.log(`
|
|
2596
|
-
Install in your app (paths are relative to a file in src
|
|
2673
|
+
Install in your app (paths are relative to a file in src/; adjust if yours lives elsewhere):
|
|
2597
2674
|
|
|
2598
2675
|
import { ${exportName} } from '${jsImport}';
|
|
2599
2676
|
import '${cssImport}';
|
|
@@ -2625,7 +2702,7 @@ Or with a <link> tag:
|
|
|
2625
2702
|
// Adaptation rules are resolved theme writes in their own right, so a family
|
|
2626
2703
|
// named only inside one needs the same notice as one named at the root.
|
|
2627
2704
|
for (const family of unloadedFonts) {
|
|
2628
|
-
const msg = `Font "${family}" is named by this theme but not loaded
|
|
2705
|
+
const msg = `Font "${family}" is named by this theme but not loaded; add a <link> or @font-face in your app (recipe: astryx docs typography)`;
|
|
2629
2706
|
noticeMessages.push(msg);
|
|
2630
2707
|
logger.log(` note: ${msg}`);
|
|
2631
2708
|
}
|
|
@@ -2919,11 +2996,11 @@ export async function themeBuildFamily(
|
|
|
2919
2996
|
const upToDate = stale.length === 0;
|
|
2920
2997
|
if (upToDate) {
|
|
2921
2998
|
logger.log(
|
|
2922
|
-
`\n
|
|
2999
|
+
`\n[ok] Theme family outputs are up to date with ${sourceRelative}.`,
|
|
2923
3000
|
);
|
|
2924
3001
|
} else {
|
|
2925
3002
|
logger.error(
|
|
2926
|
-
`\n
|
|
3003
|
+
`\n[fail] ${stale.length} theme family output(s) are out of date:`,
|
|
2927
3004
|
);
|
|
2928
3005
|
for (const entry of stale) {
|
|
2929
3006
|
logger.error(
|
|
@@ -2943,7 +3020,7 @@ export async function themeBuildFamily(
|
|
|
2943
3020
|
};
|
|
2944
3021
|
}
|
|
2945
3022
|
writeBuildOutputs(writes);
|
|
2946
|
-
for (const output of Object.values(outputs)) logger.log(
|
|
3023
|
+
for (const output of Object.values(outputs)) logger.log(`[ok] ${output}`);
|
|
2947
3024
|
const themeBindings = members.map(member => bindings.get(member.theme.name));
|
|
2948
3025
|
const relOutDir = path.relative(cwd, outDir) || '.';
|
|
2949
3026
|
const jsImport = importSpecifier(relOutDir, baseName);
|
|
@@ -2961,7 +3038,7 @@ Or load the same stylesheet natively:
|
|
|
2961
3038
|
];
|
|
2962
3039
|
const notices = unloadedFonts.map(
|
|
2963
3040
|
family =>
|
|
2964
|
-
`Font "${family}" is named by this theme family but not loaded
|
|
3041
|
+
`Font "${family}" is named by this theme family but not loaded; add a <link> or @font-face in your app (recipe: astryx docs typography)`,
|
|
2965
3042
|
);
|
|
2966
3043
|
for (const notice of notices) logger.log(` note: ${notice}`);
|
|
2967
3044
|
return {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* next one is covered without touching this file.
|
|
20
20
|
*
|
|
21
21
|
* Asserted on the receipt's `warnings` rather than on a rejection: a private
|
|
22
|
-
* var is reported (logged
|
|
22
|
+
* var is reported (logged `[error]`, collected into the receipt) and the build then
|
|
23
23
|
* emits its CSS and resolves anyway. Asserting a throw would pass for the
|
|
24
24
|
* wrong reason — it never throws, which is why a throwaway build read as a
|
|
25
25
|
* pass on the first version of #5214.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Every field `themeBuild()` returns is named in its FunctionDoc and in the
|
|
5
|
+
* response-types doc.
|
|
6
|
+
*
|
|
7
|
+
* The receipt is a stable machine-readable response, so each field needs
|
|
8
|
+
* consumer documentation, not only a typedef. The fields are read off a real
|
|
9
|
+
* build, so a field added to the receipt without documentation fails here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {describe, it, expect, beforeAll, afterEach} from 'vitest';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import {themeBuild} from './build.mjs';
|
|
17
|
+
import {doc} from '../themeBuild.doc.mjs';
|
|
18
|
+
import {doc as responseTypes} from '../../../foundation/response/response-types.doc.mjs';
|
|
19
|
+
import {ensureCoreBuilt} from '../../../clients/cli/commands/ensure-core-built.mjs';
|
|
20
|
+
|
|
21
|
+
/** @type {string[]} */
|
|
22
|
+
const dirs = [];
|
|
23
|
+
|
|
24
|
+
beforeAll(() => {
|
|
25
|
+
ensureCoreBuilt();
|
|
26
|
+
}, 200_000);
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
for (const dir of dirs.splice(0)) {
|
|
30
|
+
fs.rmSync(dir, {recursive: true, force: true});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** @returns {string} a temp dir holding a theme that exercises every field */
|
|
35
|
+
function themeDir() {
|
|
36
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-receipt-doc-'));
|
|
37
|
+
dirs.push(dir);
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
path.join(dir, 'ocean.mjs'),
|
|
40
|
+
`export default {
|
|
41
|
+
name: 'ocean',
|
|
42
|
+
tokens: {'--color-background-body': '#ffffff', '--font-family-body': '"Inter", sans-serif'},
|
|
43
|
+
components: {progressbar: {base: {color: 'red'}}},
|
|
44
|
+
};
|
|
45
|
+
`,
|
|
46
|
+
);
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @param {string} type */
|
|
51
|
+
function returnsDescription(type) {
|
|
52
|
+
const entry = doc.returns?.find(r => r.type === type);
|
|
53
|
+
expect(entry, `themeBuild doc has no ${type} return`).toBeDefined();
|
|
54
|
+
return /** @type {{description: string}} */ (entry).description;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {string} description @param {string[]} fields */
|
|
58
|
+
function undocumented(description, fields) {
|
|
59
|
+
return fields.filter(
|
|
60
|
+
field => !new RegExp(String.raw`\b${field}\b`).test(description),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('themeBuild() consumer docs name every receipt field', () => {
|
|
65
|
+
it('theme.build', async () => {
|
|
66
|
+
const dir = themeDir();
|
|
67
|
+
const result = await themeBuild('ocean.mjs', {}, {cwd: dir});
|
|
68
|
+
|
|
69
|
+
expect(result?.type).toBe('theme.build');
|
|
70
|
+
const data = /** @type {Record<string, any>} */ (result?.data);
|
|
71
|
+
// The theme is chosen so the optional arrays are non-empty.
|
|
72
|
+
expect(data.warnings.length).toBeGreaterThan(0);
|
|
73
|
+
expect(data.notices.length).toBeGreaterThan(0);
|
|
74
|
+
|
|
75
|
+
const fields = [
|
|
76
|
+
...Object.keys(data),
|
|
77
|
+
...Object.keys(data.outputs),
|
|
78
|
+
'variantsDts',
|
|
79
|
+
];
|
|
80
|
+
expect(undocumented(returnsDescription('theme.build'), fields)).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('theme.build in the response-types doc', async () => {
|
|
84
|
+
const dir = themeDir();
|
|
85
|
+
const result = await themeBuild('ocean.mjs', {}, {cwd: dir});
|
|
86
|
+
const data = /** @type {Record<string, any>} */ (result?.data);
|
|
87
|
+
const member = responseTypes.members.find(m => m.value === 'theme.build');
|
|
88
|
+
expect(member, 'response-types has no theme.build member').toBeDefined();
|
|
89
|
+
|
|
90
|
+
const fields = [
|
|
91
|
+
...Object.keys(data),
|
|
92
|
+
...Object.keys(data.outputs),
|
|
93
|
+
'variantsDts',
|
|
94
|
+
];
|
|
95
|
+
expect(
|
|
96
|
+
undocumented(/** @type {{description: string}} */ (member).description, fields),
|
|
97
|
+
).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('theme.build.check', async () => {
|
|
101
|
+
const dir = themeDir();
|
|
102
|
+
const result = await themeBuild('ocean.mjs', {check: true}, {cwd: dir});
|
|
103
|
+
|
|
104
|
+
expect(result?.type).toBe('theme.build.check');
|
|
105
|
+
const data = /** @type {Record<string, any>} */ (result?.data);
|
|
106
|
+
const fields = [...Object.keys(data), ...Object.keys(data.stale[0])];
|
|
107
|
+
expect(
|
|
108
|
+
undocumented(returnsDescription('theme.build.check'), fields),
|
|
109
|
+
).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -211,11 +211,11 @@ export function formatFontLoadingHelp(themeName, families) {
|
|
|
211
211
|
const first = families[0];
|
|
212
212
|
const slug = first.toLowerCase().replace(/ /g, '-');
|
|
213
213
|
return `
|
|
214
|
-
|
|
215
|
-
The built CSS only sets font-family
|
|
214
|
+
[note] Theme "${themeName}" names fonts it does not load: ${named}
|
|
215
|
+
The built CSS only sets font-family; load these in your app, or every
|
|
216
216
|
browser quietly falls back.
|
|
217
217
|
|
|
218
|
-
Google Fonts (add :wght
|
|
218
|
+
Google Fonts (add :wght@... axes as your theme's weights require):
|
|
219
219
|
|
|
220
220
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
221
221
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
@@ -227,12 +227,15 @@ describe('formatFontLoadingHelp', () => {
|
|
|
227
227
|
const help = formatFontLoadingHelp('ocean', ['Fraunces', 'JetBrains Mono']);
|
|
228
228
|
|
|
229
229
|
it('names the theme and every family in the headline', () => {
|
|
230
|
-
expect(help).toContain('
|
|
231
|
-
expect(help).toContain('ocean');
|
|
230
|
+
expect(help).toContain('[note] Theme "ocean"');
|
|
232
231
|
expect(help).toContain('"Fraunces"');
|
|
233
232
|
expect(help).toContain('"JetBrains Mono"');
|
|
234
233
|
});
|
|
235
234
|
|
|
235
|
+
it('is plain ASCII', () => {
|
|
236
|
+
expect(help).not.toMatch(/[\u0080-\uffff]/);
|
|
237
|
+
});
|
|
238
|
+
|
|
236
239
|
it('includes the Google Fonts recipe: preconnect pair + the exact css2 URL', () => {
|
|
237
240
|
expect(help).toContain(
|
|
238
241
|
'<link rel="preconnect" href="https://fonts.googleapis.com"',
|
|
@@ -86,7 +86,8 @@ function serializePalette(palette, indentation = 0) {
|
|
|
86
86
|
|
|
87
87
|
/** @param {TonalPaletteCandidate} candidate */
|
|
88
88
|
export function serializePaletteCandidate(candidate) {
|
|
89
|
-
|
|
89
|
+
const stops = candidate.stops.map(stop => ` ${JSON.stringify(stop)}`);
|
|
90
|
+
return `{\n "schemaVersion": 1,\n "status": "candidate",\n "recipe": ${JSON.stringify(candidate.recipe)},\n "black": ${JSON.stringify(candidate.black)},\n "white": ${JSON.stringify(candidate.white)},\n "stops": [\n${stops.join(',\n')}\n ],\n "palette": ${serializePalette(candidate.palette, 2)}\n}\n`;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/** @param {TonalPaletteCandidate} candidate @param {string} outputPath */
|
|
@@ -168,7 +169,11 @@ function existingFileIdentity(filePath) {
|
|
|
168
169
|
) {
|
|
169
170
|
return null;
|
|
170
171
|
}
|
|
171
|
-
throw
|
|
172
|
+
throw new AstryxError(
|
|
173
|
+
`Could not write palette candidate: ${error instanceof Error ? error.message : String(error)}`,
|
|
174
|
+
undefined,
|
|
175
|
+
ERROR_CODES.ERR_WRITE_FAILED,
|
|
176
|
+
);
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
3
4
|
import * as fs from 'node:fs';
|
|
4
5
|
import * as os from 'node:os';
|
|
5
6
|
import * as path from 'node:path';
|
|
6
7
|
import {afterEach, describe, expect, it} from 'vitest';
|
|
8
|
+
import {isErrorCode} from '../../../../foundation/response/error-codes.mjs';
|
|
7
9
|
import {themePaletteGenerate} from './generate.mjs';
|
|
8
10
|
|
|
9
11
|
const temporaryDirectories = [];
|
|
@@ -101,6 +103,81 @@ describe('themePaletteGenerate', () => {
|
|
|
101
103
|
expect(result.data.receipt).toBe('ocean.palette.receipt.json');
|
|
102
104
|
});
|
|
103
105
|
|
|
106
|
+
it('writes candidate JSON byte-identical to the recipe fixtures', () => {
|
|
107
|
+
const families = [
|
|
108
|
+
{id: 'neutral', name: 'Neutral', seed: '#777777', kind: 'neutral'},
|
|
109
|
+
{id: 'blue', name: 'Blue', seed: '#0074e2'},
|
|
110
|
+
{id: 'orange', name: 'Orange', seed: '#d57113'},
|
|
111
|
+
];
|
|
112
|
+
const fixtures = [
|
|
113
|
+
[
|
|
114
|
+
{families},
|
|
115
|
+
3755,
|
|
116
|
+
'11c40191d508274d89d631bb4e1cb662f70ff0dce6c0dec101c317f9f69d3e25',
|
|
117
|
+
],
|
|
118
|
+
[
|
|
119
|
+
{
|
|
120
|
+
modeStrategy: 'light-only',
|
|
121
|
+
stops: [20, 50, 80],
|
|
122
|
+
families: [
|
|
123
|
+
{
|
|
124
|
+
id: 'blue',
|
|
125
|
+
name: 'Blue',
|
|
126
|
+
seed: '#0074e2',
|
|
127
|
+
anchors: [
|
|
128
|
+
{mode: 'light', stop: 50, color: '#1682d5', policy: 'exact'},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
327,
|
|
134
|
+
'c42929be3c4b5cb857cada078f62bb5a2242c1a22cfa4ae7546a18592540f7f3',
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
modeStrategy: 'dark-only',
|
|
139
|
+
stops: [40],
|
|
140
|
+
families: [{id: 'red', name: 'Red', seed: '#d62830'}],
|
|
141
|
+
},
|
|
142
|
+
258,
|
|
143
|
+
'88d3d69865c74c7fb14347b9967575285a7d44ab893087a08bc842bb66b29bbb',
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
{
|
|
147
|
+
stops: [60, 80, 95],
|
|
148
|
+
families: [
|
|
149
|
+
{id: 'green', name: 'Green', seed: '#358a3a'},
|
|
150
|
+
{id: 'teal', name: 'Teal', seed: '#0c7365'},
|
|
151
|
+
{id: 'cyan', name: 'Cyan', seed: '#0c6f82'},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
910,
|
|
155
|
+
'873821574fdbe3357304dbc06986bd2e5ec88af80d0f36305626fd826c3cf07b',
|
|
156
|
+
],
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
for (const [request, bytes, sha256] of fixtures) {
|
|
160
|
+
const cwd = fixture();
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(cwd, 'palette.config.json'),
|
|
163
|
+
JSON.stringify(request),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const printed = themePaletteGenerate('palette.config.json', {}, {cwd});
|
|
167
|
+
expect(printed.data.generationReceipt.candidateSha256).toBe(sha256);
|
|
168
|
+
|
|
169
|
+
const result = themePaletteGenerate(
|
|
170
|
+
'palette.config.json',
|
|
171
|
+
{out: 'candidate.json'},
|
|
172
|
+
{cwd},
|
|
173
|
+
);
|
|
174
|
+
const written = fs.readFileSync(path.join(cwd, 'candidate.json'));
|
|
175
|
+
expect(written.length).toBe(bytes);
|
|
176
|
+
expect(createHash('sha256').update(written).digest('hex')).toBe(sha256);
|
|
177
|
+
expect(result.data.generationReceipt.candidateSha256).toBe(sha256);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
104
181
|
it('preserves mixed decimal and integer stop order in every artifact', () => {
|
|
105
182
|
const cwd = fixture();
|
|
106
183
|
fs.writeFileSync(
|
|
@@ -267,4 +344,23 @@ describe('themePaletteGenerate', () => {
|
|
|
267
344
|
themePaletteGenerate('palette.config.json', {}, {cwd}),
|
|
268
345
|
).toThrow('Could not parse palette config');
|
|
269
346
|
});
|
|
347
|
+
|
|
348
|
+
it('reports an output path it cannot use with a registered error code', () => {
|
|
349
|
+
const cwd = fixture();
|
|
350
|
+
fs.writeFileSync(path.join(cwd, 'blocker'), '');
|
|
351
|
+
|
|
352
|
+
for (const options of [
|
|
353
|
+
{out: 'blocker/ocean.palette.json'},
|
|
354
|
+
{preview: 'blocker/ocean.palette.html'},
|
|
355
|
+
]) {
|
|
356
|
+
let error;
|
|
357
|
+
try {
|
|
358
|
+
themePaletteGenerate('palette.config.json', options, {cwd});
|
|
359
|
+
} catch (caught) {
|
|
360
|
+
error = caught;
|
|
361
|
+
}
|
|
362
|
+
expect(error).toMatchObject({code: 'ERR_WRITE_FAILED'});
|
|
363
|
+
expect(isErrorCode(error.code)).toBe(true);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
270
366
|
});
|
|
@@ -37,6 +37,7 @@ export const COMPACT_11_STOPS = Object.freeze([
|
|
|
37
37
|
|
|
38
38
|
const DARK_CHROMA_FACTOR = 0.85;
|
|
39
39
|
const RESERVED_FAMILY_IDS = new Set(['black', 'white']);
|
|
40
|
+
const NEUTRAL_PROFILES = ['neutral-v1', 'warm-v1', 'cool-v1', 'custom'];
|
|
40
41
|
|
|
41
42
|
/** @param {number} value @param {number} minimum @param {number} maximum */
|
|
42
43
|
function clamp(value, minimum, maximum) {
|
|
@@ -665,11 +666,17 @@ export function normalizeGenerationRequest(input) {
|
|
|
665
666
|
anchors,
|
|
666
667
|
};
|
|
667
668
|
});
|
|
669
|
+
const neutralProfile = input.neutralProfile ?? 'neutral-v1';
|
|
670
|
+
// The receipt records this request, so an unknown profile fails even when no
|
|
671
|
+
// neutral family reads it.
|
|
672
|
+
if (!NEUTRAL_PROFILES.includes(neutralProfile)) {
|
|
673
|
+
throw new Error(`Unknown neutral profile: ${String(neutralProfile)}`);
|
|
674
|
+
}
|
|
668
675
|
/** @type {NormalizedRequest} */
|
|
669
676
|
const request = {
|
|
670
677
|
recipe: PALETTE_RECIPE,
|
|
671
678
|
vibrancy: input.vibrancy ?? 50,
|
|
672
|
-
neutralProfile
|
|
679
|
+
neutralProfile,
|
|
673
680
|
modeStrategy,
|
|
674
681
|
stops,
|
|
675
682
|
families: normalizedFamilies,
|
|
@@ -293,6 +293,16 @@ describe('astryx-oklch-v1 palette generator', () => {
|
|
|
293
293
|
).toThrow('Anchor mode dark is not generated by mode strategy light-only');
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
it('rejects an unknown neutral profile even without a neutral family', () => {
|
|
297
|
+
expect(() =>
|
|
298
|
+
generatePaletteSet({neutralProfile: 'warm-v2', families: [families[1]]}),
|
|
299
|
+
).toThrow('Unknown neutral profile: warm-v2');
|
|
300
|
+
expect(
|
|
301
|
+
generatePaletteSet({neutralProfile: 'warm-v1', families: [families[1]]})
|
|
302
|
+
.request.neutralProfile,
|
|
303
|
+
).toBe('warm-v1');
|
|
304
|
+
});
|
|
305
|
+
|
|
296
306
|
it('normalizes anchor colors in the generation receipt', () => {
|
|
297
307
|
const result = generatePaletteSet({
|
|
298
308
|
modeStrategy: 'light-only',
|
|
@@ -62,8 +62,17 @@ export function themeTemplate(options = {}) {
|
|
|
62
62
|
|
|
63
63
|
// Our repo header has no business in someone else's source tree.
|
|
64
64
|
const contents = stripCopyrightHeader(fs.readFileSync(THEME_TEMPLATE_SRC, 'utf-8'));
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(path.dirname(resolved), {recursive: true});
|
|
67
|
+
fs.writeFileSync(resolved, contents);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
70
|
+
throw new AstryxError(
|
|
71
|
+
`Failed to write the theme template to ${relative}: ${message}`,
|
|
72
|
+
undefined,
|
|
73
|
+
ERROR_CODES.ERR_WRITE_FAILED,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
67
76
|
|
|
68
77
|
return {type: 'theme.template', data: {path: relative, written: true, reason: null}};
|
|
69
78
|
}
|
|
@@ -14,6 +14,7 @@ import * as fs from 'node:fs';
|
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import * as os from 'node:os';
|
|
16
16
|
import {themeTemplate, THEME_TEMPLATE_DEFAULT_PATH} from './template.mjs';
|
|
17
|
+
import {isErrorCode} from '../../../foundation/response/error-codes.mjs';
|
|
17
18
|
|
|
18
19
|
let tmpDir;
|
|
19
20
|
beforeEach(() => {
|
|
@@ -79,4 +80,18 @@ describe('themeTemplate()', () => {
|
|
|
79
80
|
);
|
|
80
81
|
expect(fs.existsSync(path.join(path.dirname(tmpDir), 'escaped.ts'))).toBe(false);
|
|
81
82
|
});
|
|
83
|
+
|
|
84
|
+
it('reports a failed write with a registered error code', () => {
|
|
85
|
+
fs.writeFileSync(path.join(tmpDir, 'blocker'), '');
|
|
86
|
+
|
|
87
|
+
let error;
|
|
88
|
+
try {
|
|
89
|
+
themeTemplate({cwd: tmpDir, targetPath: 'blocker/theme.ts'});
|
|
90
|
+
} catch (caught) {
|
|
91
|
+
error = caught;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
expect(error).toMatchObject({code: 'ERR_WRITE_FAILED'});
|
|
95
|
+
expect(isErrorCode(error.code)).toBe(true);
|
|
96
|
+
});
|
|
82
97
|
});
|