源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { after, describe, test } from 'node:test';
|
||||
|
||||
import {
|
||||
analyzeTsx,
|
||||
assertAstryxComponentSet,
|
||||
loadAstryxComponents,
|
||||
loadMakaUiBarrel,
|
||||
parseAstryxDistComponents,
|
||||
} from './generate-astryx-surface-inventory.mjs';
|
||||
|
||||
const tmpRoots = [];
|
||||
/** Materialize a fake repo root (`packages/ui/src/*`) from a {relPath: source} map. */
|
||||
function fixtureRepo(files) {
|
||||
const repoRoot = mkdtempSync(join(tmpdir(), 'astryx-inv-'));
|
||||
tmpRoots.push(repoRoot);
|
||||
for (const [rel, contents] of Object.entries(files)) {
|
||||
const abs = join(repoRoot, rel);
|
||||
mkdirSync(join(abs, '..'), { recursive: true });
|
||||
writeFileSync(abs, contents);
|
||||
}
|
||||
return repoRoot;
|
||||
}
|
||||
after(() => {
|
||||
for (const dir of tmpRoots) rmSync(dir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe('Astryx component set derivation (#3868)', () => {
|
||||
test('is derived from @astryxdesign/core with the resolved version', () => {
|
||||
const { version, components } = loadAstryxComponents();
|
||||
assert.match(version, /^\d+\.\d+\.\d+/);
|
||||
assert.ok(components instanceof Set);
|
||||
// Components the old 47-name hand-list omitted — the drift #3868 fixes.
|
||||
for (const name of ['Timestamp', 'ChatMessage', 'ChatMessageMetadata', 'Thumbnail']) {
|
||||
assert.ok(components.has(name), `expected derived set to include ${name}`);
|
||||
}
|
||||
// Fail-closed floor: a partial parse must not slip through.
|
||||
assert.ok(components.size >= 150, `expected >=150 components, got ${components.size}`);
|
||||
// Type/prop exports are not components.
|
||||
assert.ok(!components.has('ButtonProps'));
|
||||
assert.ok(!components.has('BaseProps'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Astryx declaration parsing is fail-closed (#3868)', () => {
|
||||
test('traverses a genuine multi-level `export *` barrel graph', () => {
|
||||
// dist/index.d.ts --export*--> Group --export*--> Sub --export*--> Leaf,
|
||||
// where the component is only declared in the deepest file. A one-layer
|
||||
// parser would stop at Group and silently omit Deep; the recursive walk
|
||||
// (relative to each file's own dir) must reach it.
|
||||
const repoRoot = fixtureRepo({
|
||||
'dist/index.d.ts': ["export { Direct } from './Direct';", "export * from './Group';"].join(
|
||||
'\n',
|
||||
),
|
||||
'dist/Group/index.d.ts': "export * from './Sub';\n",
|
||||
'dist/Group/Sub/index.d.ts': "export * from './Leaf';\n",
|
||||
'dist/Group/Sub/Leaf/index.d.ts': 'export declare const Deep: unknown;\n',
|
||||
});
|
||||
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
|
||||
assert.ok(set.has('Direct'), 'top-level named re-export resolves');
|
||||
assert.ok(set.has('Deep'), 'component behind a 3-level barrel graph resolves');
|
||||
});
|
||||
|
||||
test('resolves a flat `Dir.d.ts` barrel target alongside directory barrels', () => {
|
||||
const repoRoot = fixtureRepo({
|
||||
'dist/index.d.ts': "export * from './Flat';\n",
|
||||
'dist/Flat.d.ts': 'export declare function FlatOne(): null;\n',
|
||||
});
|
||||
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
|
||||
assert.ok(set.has('FlatOne'));
|
||||
});
|
||||
|
||||
test('is cycle-safe when barrels re-export each other', () => {
|
||||
// A ⇄ B mutual `export *` must terminate, not overflow the stack.
|
||||
const repoRoot = fixtureRepo({
|
||||
'dist/index.d.ts': "export * from './A';\n",
|
||||
'dist/A/index.d.ts': ["export * from '../B';", 'export declare const FromA: unknown;'].join(
|
||||
'\n',
|
||||
),
|
||||
'dist/B/index.d.ts': ["export * from '../A';", 'export declare const FromB: unknown;'].join(
|
||||
'\n',
|
||||
),
|
||||
});
|
||||
const set = parseAstryxDistComponents(join(repoRoot, 'dist'));
|
||||
assert.ok(set.has('FromA'));
|
||||
assert.ok(set.has('FromB'));
|
||||
});
|
||||
|
||||
test('throws on an unresolved barrel target', () => {
|
||||
const repoRoot = fixtureRepo({ 'dist/index.d.ts': "export * from './Missing';\n" });
|
||||
assert.throws(() => parseAstryxDistComponents(join(repoRoot, 'dist')), /unresolved/i);
|
||||
});
|
||||
|
||||
test('throws on an unresolved target nested below the first barrel layer', () => {
|
||||
const repoRoot = fixtureRepo({
|
||||
'dist/index.d.ts': "export * from './Group';\n",
|
||||
'dist/Group/index.d.ts': "export * from './Gone';\n",
|
||||
});
|
||||
assert.throws(() => parseAstryxDistComponents(join(repoRoot, 'dist')), /unresolved/i);
|
||||
});
|
||||
|
||||
test('rejects a partial parse (too few components / missing a key component)', () => {
|
||||
assert.throws(() => assertAstryxComponentSet(new Set(['Button']), '0.5.0'), /incomplete/i);
|
||||
const bigButMissingKey = new Set(Array.from({ length: 200 }, (_, i) => `Widget${i}`));
|
||||
assert.throws(() => assertAstryxComponentSet(bigButMissingKey, '0.5.0'), /missing/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@maka/ui barrel resolution (#3868)', () => {
|
||||
test('derives the Astryx re-export map from the real barrel', () => {
|
||||
const { reexports } = loadMakaUiBarrel();
|
||||
// Auto-derived (replaces a hand-written 9-name list) — much broader coverage.
|
||||
assert.ok(reexports.size >= 20, `expected a broad re-export map, got ${reexports.size}`);
|
||||
// A direct re-export maps to its canonical Astryx name.
|
||||
assert.equal(reexports.get('Selector'), 'Selector');
|
||||
assert.equal(reexports.get('Button'), 'Button');
|
||||
assert.equal(reexports.get('CommandPalette'), 'CommandPalette');
|
||||
});
|
||||
|
||||
test('resolves nested export graphs and flags only local public shadows', () => {
|
||||
const repoRoot = fixtureRepo({
|
||||
'packages/ui/src/index.ts': [
|
||||
"export { Button, Badge as UiBadge } from '@astryxdesign/core';",
|
||||
"export * from './local.js';",
|
||||
'export function Card() { return null; }', // local public shadow: Card is an Astryx component
|
||||
'export function MyWidget() { return null; }', // not an Astryx name → not a shadow
|
||||
].join('\n'),
|
||||
'packages/ui/src/local.ts': [
|
||||
"export { Timestamp } from '@astryxdesign/core';",
|
||||
'export function helperThing() { return null; }',
|
||||
].join('\n'),
|
||||
});
|
||||
const { reexports, shadowsByFile } = loadMakaUiBarrel(repoRoot);
|
||||
// Direct + aliased + transitive (through export *) re-exports all resolve.
|
||||
assert.equal(reexports.get('Button'), 'Button');
|
||||
assert.equal(reexports.get('UiBadge'), 'Badge');
|
||||
assert.equal(reexports.get('Timestamp'), 'Timestamp');
|
||||
// Only the locally-defined public export whose name shadows Astryx is flagged.
|
||||
assert.equal(shadowsByFile.get('packages/ui/src/index.ts')?.has('Card'), true);
|
||||
assert.equal(shadowsByFile.get('packages/ui/src/index.ts')?.has('MyWidget'), false);
|
||||
});
|
||||
|
||||
test('fails closed on an unresolved relative barrel target', () => {
|
||||
const repoRoot = fixtureRepo({
|
||||
'packages/ui/src/index.ts': "export * from './does-not-exist.js';\n",
|
||||
});
|
||||
assert.throws(() => loadMakaUiBarrel(repoRoot), /unresolved/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyzeTsx severity (#3868)', () => {
|
||||
const astryxComponents = loadAstryxComponents().components;
|
||||
const base = { astryxComponents, makaUiReexports: new Map(), shadowNames: null };
|
||||
|
||||
test('counts a rendered Astryx component in the "Astryx used" column', () => {
|
||||
const src = `
|
||||
import { Timestamp } from '@astryxdesign/core';
|
||||
export function Row() { return <Timestamp value={0} />; }
|
||||
`;
|
||||
const result = analyzeTsx('packages/ui/src/row.tsx', src, base);
|
||||
assert.match(result.astryx, /Timestamp/);
|
||||
assert.equal(result.severity, 'aligned');
|
||||
});
|
||||
|
||||
test('counts an Astryx component imported via the @maka/ui re-export map', () => {
|
||||
const src = `
|
||||
import { Selector } from '@maka/ui';
|
||||
export function Row() { return <Selector value="x" />; }
|
||||
`;
|
||||
const result = analyzeTsx('apps/desktop/src/renderer/row.tsx', src, {
|
||||
...base,
|
||||
makaUiReexports: new Map([['Selector', 'Selector']]),
|
||||
});
|
||||
assert.match(result.astryx, /Selector/);
|
||||
assert.equal(result.severity, 'aligned');
|
||||
});
|
||||
|
||||
test('flags a public export that shadows an Astryx component', () => {
|
||||
const src = 'export function Button() { return null; }\n';
|
||||
const result = analyzeTsx('packages/ui/src/button.tsx', src, {
|
||||
...base,
|
||||
shadowNames: new Set(['Button']),
|
||||
});
|
||||
assert.equal(result.severity, 'reimplementation');
|
||||
assert.match(result.gaps, /public export `Button` shadows Astryx component/);
|
||||
});
|
||||
|
||||
test('does not flag a private local component that merely shares a name', () => {
|
||||
// No shadowNames for this file (it is not a public @maka/ui export).
|
||||
const src = `
|
||||
export function ModuleHubSelector() {
|
||||
function Selector() { return null; }
|
||||
return <Selector />;
|
||||
}
|
||||
`;
|
||||
const result = analyzeTsx('packages/ui/src/module-hub-selector.tsx', src, base);
|
||||
assert.equal(result.severity, 'aligned');
|
||||
assert.doesNotMatch(result.gaps, /shadows/);
|
||||
});
|
||||
|
||||
test('still flags a raw interactive control as a blocker', () => {
|
||||
const src = 'export function Bad() { return <button type="button">x</button>; }\n';
|
||||
const result = analyzeTsx('packages/ui/src/bad.tsx', src, base);
|
||||
assert.equal(result.severity, 'blocker');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user