Skip to content

Commit 79c7614

Browse files
serpentbladeclaude
andcommitted
test(quick-260623-jwh): strict consumer-surface vue-tsc gate (Layer 2)
- strict tsconfig (strict:true + noImplicitAny:true) — full typed-consumer rigor the relaxed leaf tsconfig hides - Consumer.vue imports sampled leaf public API by package name (data-table mandatory + listbox + sortable-list) via `.` exports → compiled dist .d.ts; typeof-position assignments force full type resolution - harness mirrors VUE-TSC shape: mkdtemp + symlink node_modules + execFileSync vue-tsc --noEmit; fails loud if sampled dist absent - add @tanstack/table-core, @tanstack/virtual-core, sortablejs/@types + leaf workspace devDeps; regenerate pnpm-lock.yaml (CI --frozen-lockfile) - GREEN against compiled dist; deliberately-wrong typed import correctly fails Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JE6gykywo57CcqUJZTsB9
1 parent 7a59e06 commit 79c7614

5 files changed

Lines changed: 271 additions & 10 deletions

File tree

pnpm-lock.yaml

Lines changed: 71 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!--
2+
Consumer.vue — Layer 2 strict consumer-surface stub (quick task 260623-jwh).
3+
4+
A real Vue consumer that imports the PUBLIC API of the sampled @rozie-ui Vue
5+
leaves BY PACKAGE NAME (resolved through each package's `.` exports map → the
6+
compiled dist/index.d.ts). Each import is used in a typed position so vue-tsc
7+
actually checks the imported component types instead of tree-shaking them.
8+
9+
- data-table (MANDATORY): the heaviest leaf; its public surface references
10+
@tanstack/table-core peer types. If its dist .d.ts is type-broken under a
11+
strict consumer build, this stub fails vue-tsc (the 49-error regression class).
12+
- listbox: a pure-Rozie (no-engine) leaf.
13+
- sortable-list: an engine-wrapper leaf (sortablejs peer).
14+
15+
This SFC is dropped into a tmpdir alongside the strict tsconfig and typechecked
16+
with `vue-tsc --noEmit` by vue-consumer-surface.test.ts.
17+
-->
18+
<script setup lang="ts">
19+
import {
20+
DataTable,
21+
Column,
22+
EditorText,
23+
EditorNumber,
24+
EditorSelect,
25+
EditorCheckbox,
26+
EditorDate,
27+
FilterText,
28+
FilterNumberRange,
29+
FilterSelect,
30+
GroupBar,
31+
DetailPanel,
32+
} from '@rozie-ui/data-table-vue';
33+
import { Listbox } from '@rozie-ui/listbox-vue';
34+
import { SortableList } from '@rozie-ui/sortable-list-vue';
35+
36+
// Use each import in a typed position so vue-tsc checks the component types
37+
// rather than eliding the import. `typeof Import` resolves the component's
38+
// declared type from its dist .d.ts; an `any`-typed or type-broken export would
39+
// surface here under noImplicitAny:true.
40+
const dataTable: typeof DataTable = DataTable;
41+
const column: typeof Column = Column;
42+
const editorText: typeof EditorText = EditorText;
43+
const editorNumber: typeof EditorNumber = EditorNumber;
44+
const editorSelect: typeof EditorSelect = EditorSelect;
45+
const editorCheckbox: typeof EditorCheckbox = EditorCheckbox;
46+
const editorDate: typeof EditorDate = EditorDate;
47+
const filterText: typeof FilterText = FilterText;
48+
const filterNumberRange: typeof FilterNumberRange = FilterNumberRange;
49+
const filterSelect: typeof FilterSelect = FilterSelect;
50+
const groupBar: typeof GroupBar = GroupBar;
51+
const detailPanel: typeof DetailPanel = DetailPanel;
52+
const listbox: typeof Listbox = Listbox;
53+
const sortableList: typeof SortableList = SortableList;
54+
55+
// Reference the bindings so they are not reported as unused.
56+
void [
57+
dataTable,
58+
column,
59+
editorText,
60+
editorNumber,
61+
editorSelect,
62+
editorCheckbox,
63+
editorDate,
64+
filterText,
65+
filterNumberRange,
66+
filterSelect,
67+
groupBar,
68+
detailPanel,
69+
listbox,
70+
sortableList,
71+
];
72+
</script>
73+
74+
<template>
75+
<!--
76+
No render usage here: rendering a component forces every required prop (e.g.
77+
DataTable's `data`), which is per-leaf and fragile. The `typeof Import`
78+
typed-position assignments in <script setup> already force vue-tsc to fully
79+
resolve + typecheck each imported component's type (props interface included)
80+
from its dist .d.ts — which is exactly the leaf-surface rigor this gate exists
81+
to enforce. An empty template keeps the SFC valid without inventing prop data.
82+
-->
83+
<div />
84+
</template>

tests/vue-typecheck/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212
"@rozie/target-vue": "workspace:*"
1313
},
1414
"devDependencies": {
15+
"@rozie-ui/data-table-vue": "workspace:*",
16+
"@rozie-ui/listbox-vue": "workspace:*",
17+
"@rozie-ui/sortable-list-vue": "workspace:*",
18+
"@tanstack/table-core": "^8.21",
19+
"@tanstack/virtual-core": "^3",
20+
"@types/sortablejs": "^1.15",
21+
"sortablejs": "^1.15",
1522
"typescript": "~5.6.0",
1623
"vitest": "^4",
1724
"vue": "^3.5.33",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"//": "STRICT consumer tsconfig (quick task 260623-jwh, Layer 2). Unlike the relaxed",
3+
"//noImplicitAny": "tsconfig.json (noImplicitAny:false), this sets noImplicitAny:true to apply the",
4+
"//rigor": "full typed-consumer rigor the leaf's relaxed tsconfig hides. A type-broken leaf",
5+
"//catches": "dist .d.ts (the 49-error regression class) fails vue-tsc under this config.",
6+
"compilerOptions": {
7+
"target": "ES2022",
8+
"module": "ESNext",
9+
"moduleResolution": "bundler",
10+
"jsx": "preserve",
11+
"strict": true,
12+
"noImplicitAny": true,
13+
"skipLibCheck": true,
14+
"isolatedModules": true,
15+
"esModuleInterop": true,
16+
"allowSyntheticDefaultImports": true,
17+
"lib": ["ES2022", "DOM"],
18+
"types": ["vue"]
19+
},
20+
"include": ["**/*.vue", "**/*.ts"]
21+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* VUE-CONSUMER-SURFACE — Layer 2 strict consumer-surface gate (quick task 260623-jwh).
3+
*
4+
* Automates the verdaccio dogfood: proves a STRICT (strict:true + noImplicitAny:true)
5+
* typed Vue consumer of the COMPILED leaf dist stays vue-tsc clean. This catches the
6+
* "source-only Vue leaf breaks consumer vue-tsc with 49 errors" class for FUTURE leaves
7+
* (threat T-jwh-02).
8+
*
9+
* Mirrors the existing VUE-TSC harness shape: mkdtempSync tmpdir, copy in the strict
10+
* tsconfig (as tsconfig.json) + the Consumer.vue stub, symlink this package's
11+
* node_modules (so the workspace-symlinked @rozie-ui leaf packages + vue + peer types
12+
* resolve), then run `node_modules/.bin/vue-tsc --noEmit -p tsconfig.json` and throw
13+
* with the captured stdout/stderr on non-zero.
14+
*
15+
* The consumer stub imports the sampled leaves BY PACKAGE NAME, so resolution goes
16+
* through each package's `.` exports map → the compiled dist/index.d.ts. The sampled
17+
* leaves MUST be built first (their dist .d.ts must exist):
18+
* pnpm turbo run build --filter "@rozie-ui/*-vue"
19+
* In CI this is covered by the preceding `pnpm turbo run build` step (Layer 3).
20+
*
21+
* Sample = data-table (MANDATORY) + listbox (pure-Rozie) + sortable-list (engine-wrapper).
22+
*/
23+
import { describe, it, expect } from 'vitest';
24+
import { execFileSync } from 'node:child_process';
25+
import { mkdtempSync, rmSync, copyFileSync, symlinkSync, mkdirSync, existsSync } from 'node:fs';
26+
import { join, resolve, dirname } from 'node:path';
27+
import { tmpdir } from 'node:os';
28+
import { fileURLToPath } from 'node:url';
29+
30+
const HERE = dirname(fileURLToPath(import.meta.url));
31+
const ROOT = resolve(HERE, '../..');
32+
33+
// Sampled leaves whose compiled dist .d.ts the consumer stub imports. The test
34+
// asserts each is built before running so a missing-dist run fails loud (not a
35+
// false green) rather than silently resolving nothing.
36+
const SAMPLED_DIST = [
37+
'packages/ui/data-table/packages/vue/dist/index.d.ts',
38+
'packages/ui/listbox/packages/vue/dist/index.d.ts',
39+
'packages/ui/sortable-list/packages/vue/dist/index.d.ts',
40+
];
41+
42+
describe('VUE-CONSUMER-SURFACE — strict typed consumer of compiled leaf dist is vue-tsc clean (Layer 2)', () => {
43+
it('Consumer.vue importing sampled @rozie-ui leaf dist typechecks clean under strict tsconfig', () => {
44+
// Fail loud if the sampled leaves were not built — the consumer resolves their
45+
// compiled dist .d.ts, so a missing dist is a setup error, not a pass.
46+
const missing = SAMPLED_DIST.filter((p) => !existsSync(resolve(ROOT, p)));
47+
if (missing.length > 0) {
48+
throw new Error(
49+
'Sampled Vue leaf dist .d.ts missing — build them first ' +
50+
'(`pnpm turbo run build --filter "@rozie-ui/*-vue"`):\n ' +
51+
missing.join('\n '),
52+
);
53+
}
54+
55+
const tmpDir = mkdtempSync(join(tmpdir(), 'rozie-vue-consumer-'));
56+
try {
57+
// Strict consumer tsconfig (strict + noImplicitAny) → tsconfig.json in tmp.
58+
copyFileSync(join(HERE, 'tsconfig.consumer.strict.json'), join(tmpDir, 'tsconfig.json'));
59+
// The typed consumer stub.
60+
mkdirSync(join(tmpDir, 'consumer-stub'), { recursive: true });
61+
copyFileSync(
62+
join(HERE, 'consumer-stub', 'Consumer.vue'),
63+
join(tmpDir, 'consumer-stub', 'Consumer.vue'),
64+
);
65+
// Symlink this package's node_modules so the workspace-symlinked @rozie-ui
66+
// leaves + vue + @tanstack peer types resolve through their real exports maps.
67+
symlinkSync(join(HERE, 'node_modules'), join(tmpDir, 'node_modules'), 'dir');
68+
69+
const vueTscBin = resolve(HERE, 'node_modules/.bin/vue-tsc');
70+
try {
71+
execFileSync(vueTscBin, ['--noEmit', '-p', 'tsconfig.json'], {
72+
cwd: tmpDir,
73+
stdio: 'pipe',
74+
});
75+
} catch (err) {
76+
const stdout = (err as { stdout?: Buffer }).stdout?.toString() ?? '';
77+
const stderr = (err as { stderr?: Buffer }).stderr?.toString() ?? '';
78+
throw new Error(
79+
'vue-tsc --noEmit exited non-zero for the strict consumer surface:\n' + stdout + '\n' + stderr,
80+
);
81+
}
82+
} finally {
83+
rmSync(tmpDir, { recursive: true, force: true });
84+
}
85+
86+
expect(true).toBe(true);
87+
});
88+
});

0 commit comments

Comments
 (0)