mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
GET /v1/billing/tier's displayName names the tier class ("Pro" for every Max
rung), so it never names the plan. The removal guard also reads the emitted
declarations, where an aliased type re-export would hide.
Co-authored-by: Hanzo Dev <[email protected]>
242 lines
8.3 KiB
JavaScript
242 lines
8.3 KiB
JavaScript
/**
|
|
* The barrel is this package's whole contract, and it is hand-written, so a
|
|
* rename lands in three places or it does not land: the file, the file's own
|
|
* export, and `index.ts`. Miss the third and `tsc` is happy — the module still
|
|
* compiles — while every consumer gets a name that no longer exists.
|
|
*
|
|
* So this reads `src/index.ts` and holds it against the sources it names:
|
|
* every module resolves, every named export exists there, and the retired names
|
|
* stay retired.
|
|
*
|
|
* node --test barrel.test.mjs
|
|
*/
|
|
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
import { basename, join, dirname } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const SRC = join(dirname(fileURLToPath(import.meta.url)), 'src')
|
|
const barrel = readFileSync(join(SRC, 'index.ts'), 'utf8')
|
|
|
|
/** Every `export { … } from './x'` / `export type { … } from './x'` clause. */
|
|
function clauses(source) {
|
|
const out = []
|
|
const re = /export\s+(type\s+)?\{([^}]*)\}\s+from\s+'\.\/([\w.-]+)'/g
|
|
for (const [, , names, module] of source.matchAll(re)) {
|
|
const specifiers = names
|
|
.split(',')
|
|
.map((n) => n.trim())
|
|
.filter(Boolean)
|
|
// `CommandItem as OrgCommandItem` — the LOCAL name is what must exist.
|
|
.map((n) => n.split(/\s+as\s+/)[0].replace(/^type\s+/, ''))
|
|
out.push({ module, specifiers })
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Names `file` declares as its own exports. */
|
|
function exported(file) {
|
|
const source = readFileSync(file, 'utf8')
|
|
const names = new Set()
|
|
const re =
|
|
/export\s+(?:declare\s+)?(?:async\s+)?(?:function|const|let|var|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g
|
|
for (const [, name] of source.matchAll(re)) names.add(name)
|
|
return names
|
|
}
|
|
|
|
function resolve(module) {
|
|
// A specifier names its file, so the name is tried as written before the
|
|
// extensions a bare name would take.
|
|
for (const ext of ['', '.ts', '.tsx']) {
|
|
const file = join(SRC, module + ext)
|
|
if (existsSync(file)) return file
|
|
}
|
|
return null
|
|
}
|
|
|
|
const surface = clauses(barrel)
|
|
|
|
test('the barrel names at least the whole signed-in chrome', () => {
|
|
const all = surface.flatMap((c) => c.specifiers)
|
|
for (const name of [
|
|
'OrgHeader',
|
|
'OrgCommandPalette',
|
|
'UserOrgDropdown',
|
|
'HanzoAppLauncher',
|
|
'HanzoHeader',
|
|
'HanzoMark',
|
|
'ORG_DOMAINS',
|
|
]) {
|
|
assert.ok(all.includes(name), `${name} is missing from the barrel`)
|
|
}
|
|
})
|
|
|
|
test('every module the barrel re-exports from exists', () => {
|
|
for (const { module } of surface) {
|
|
assert.ok(resolve(module), `index.ts re-exports from missing ./${module}`)
|
|
}
|
|
})
|
|
|
|
test('every name the barrel re-exports is declared by that module', () => {
|
|
for (const { module, specifiers } of surface) {
|
|
const file = resolve(module)
|
|
if (!file) continue
|
|
const declared = exported(file)
|
|
for (const name of specifiers) {
|
|
assert.ok(
|
|
declared.has(name),
|
|
`index.ts exports ${name} from ./${module}, which does not declare it`
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('org, never tenant — the vocabulary law holds across the package', () => {
|
|
const offenders = readdirSync(SRC, { recursive: true })
|
|
.filter((f) => /\.tsx?$/.test(f))
|
|
.filter((f) => /tenant/i.test(readFileSync(join(SRC, f), 'utf8')))
|
|
assert.deepEqual(offenders, [], 'these files still say "tenant"')
|
|
})
|
|
|
|
test('the retired switcher and its stale app list stay retired', () => {
|
|
const all = surface.flatMap((c) => c.specifiers)
|
|
// AppSwitcher was a second, denser cross-app popover over a 26-app list that
|
|
// had drifted from the product. HanzoAppLauncher is the one switcher, over
|
|
// the one list (HANZO_APPS).
|
|
for (const name of [
|
|
'AppSwitcher',
|
|
'DEFAULT_TENANT_APPS',
|
|
'getAppsForOrg',
|
|
'useTenantAuth',
|
|
]) {
|
|
assert.ok(!all.includes(name), `${name} is exported again`)
|
|
assert.equal(resolve(name), null, `${name}.tsx is back on disk`)
|
|
}
|
|
})
|
|
|
|
test('the retired signed-in bars stay retired', () => {
|
|
const all = surface.flatMap((c) => c.specifiers)
|
|
// Three components drew the same 56px of signed-in chrome behind three sets
|
|
// of props. HanzoAppBar had no consumer; HanzoAppHeader's one consumer needed
|
|
// `search` and `account`, which OrgHeader now carries. OrgHeader is the bar.
|
|
for (const name of ['HanzoAppBar', 'HanzoAppHeader']) {
|
|
assert.ok(!all.includes(name), `${name} is exported again`)
|
|
assert.equal(resolve(name), null, `${name}.tsx is back on disk`)
|
|
}
|
|
for (const type of [
|
|
'HanzoAppBarProps',
|
|
'HanzoAppBarAction',
|
|
'HanzoAppHeaderProps',
|
|
'HanzoAppHeaderAction',
|
|
'HanzoContextNode',
|
|
]) {
|
|
assert.ok(!all.includes(type), `${type} is exported again`)
|
|
}
|
|
})
|
|
|
|
// Every name the static plan ladder exported. Plans are the catalog's
|
|
// (GET /v1/billing/plans) and the held one is billing's (GET /v1/billing/tier).
|
|
const LADDER = [
|
|
'HANZO_PLANS',
|
|
'HANZO_PLAN_TIERS',
|
|
'DEFAULT_PLAN_TIER',
|
|
'UNLIMITED',
|
|
'getPlanTier',
|
|
'APP_ENTITLEMENTS',
|
|
'HanzoPlanKind',
|
|
'HanzoPlanTier',
|
|
'HanzoPlanLimits',
|
|
'HanzoPlanTierDef',
|
|
'entitlementFor',
|
|
'isEntitled',
|
|
'requiredTier',
|
|
'rankOf',
|
|
'normalizeTier',
|
|
'useEntitlement',
|
|
'FREE_TIER',
|
|
'Entitlement',
|
|
'EntitlementState',
|
|
'UseEntitlementOptions',
|
|
'HanzoAccessGate',
|
|
'HanzoAccessGateProps',
|
|
'HanzoPlans',
|
|
'HanzoPlansProps',
|
|
]
|
|
|
|
test('the shell keeps no plan list of its own', async () => {
|
|
assert.equal(LADDER.length, 24)
|
|
const barrelNames = surface.flatMap((c) => c.specifiers)
|
|
const registryNames = exported(join(SRC, 'hanzo-registry.ts'))
|
|
// The built entries too: `.` and `./registry` are what a consumer imports.
|
|
const built = [
|
|
...Object.keys(await import('./dist/esm/index.js')),
|
|
...Object.keys(await import('./dist/esm/hanzo-registry.js')),
|
|
]
|
|
// The emitted declarations carry the type-only names the runtime keys cannot,
|
|
// under either side of an `as`.
|
|
const declared = new Set()
|
|
for (const file of ['index.d.ts', 'hanzo-registry.d.ts']) {
|
|
const dts = readFileSync(join(SRC, '..', 'types', file), 'utf8')
|
|
for (const [, names] of dts.matchAll(/export\s+(?:type\s+)?\{([^}]*)\}/g)) {
|
|
for (const n of names.split(/[\s,]+/))
|
|
if (n && n !== 'as' && n !== 'type') declared.add(n)
|
|
}
|
|
for (const [, n] of dts.matchAll(
|
|
/export\s+(?:declare\s+)?(?:const|let|function|class|type|interface|enum)\s+([A-Za-z_$][\w$]*)/g
|
|
)) {
|
|
declared.add(n)
|
|
}
|
|
}
|
|
assert.ok(
|
|
declared.has('OrgCommandItem') && declared.has('HanzoLink'),
|
|
'the declarations were not read'
|
|
)
|
|
for (const name of LADDER) {
|
|
assert.ok(!declared.has(name), `${name} is in the emitted declarations again`)
|
|
assert.ok(!barrelNames.includes(name), `${name} is exported from the barrel again`)
|
|
assert.ok(!registryNames.has(name), `${name} is exported from ./registry again`)
|
|
assert.ok(!built.includes(name), `${name} is in the built output again`)
|
|
}
|
|
const gone =
|
|
/^(HanzoPlans|HanzoAccessGate|StudioAccessGate|entitlements|useEntitlement)\.[cm]?[jt]sx?$/
|
|
const back = readdirSync(SRC, { recursive: true }).filter((f) =>
|
|
gone.test(basename(String(f)))
|
|
)
|
|
assert.deepEqual(back, [], 'a deleted plan module is back under src/')
|
|
})
|
|
|
|
test('the one signed-in bar carries the folded-in props', () => {
|
|
const source = readFileSync(join(SRC, 'OrgHeader.tsx'), 'utf8')
|
|
for (const prop of ['search', 'account', 'headerRight', 'currentApp']) {
|
|
assert.match(
|
|
source,
|
|
new RegExp(`^\\s+${prop}\\??:`, 'm'),
|
|
`OrgHeaderProps dropped ${prop}`
|
|
)
|
|
}
|
|
})
|
|
|
|
test('no source imports a module that no longer exists', () => {
|
|
// Naming a retired bar in prose is knowledge — this package keeps a record of
|
|
// WHY a thing went. Importing one is a build break, so that is what is held.
|
|
const offenders = readdirSync(SRC, { recursive: true })
|
|
.filter((f) => /\.tsx?$/.test(f))
|
|
.filter((f) => {
|
|
const source = readFileSync(join(SRC, f), 'utf8')
|
|
return [...source.matchAll(/from\s+'\.\/([\w.-]+)'/g)].some(
|
|
([, module]) => !resolve(module)
|
|
)
|
|
})
|
|
assert.deepEqual(offenders, [], 'these files import a missing module')
|
|
})
|
|
|
|
test('HANZO_APPS is the only app registry left in the package', () => {
|
|
const lists = readdirSync(SRC, { recursive: true })
|
|
.filter((f) => /\.tsx?$/.test(f))
|
|
.filter((f) =>
|
|
/^export const [A-Z_]*APPS\b/m.test(readFileSync(join(SRC, f), 'utf8'))
|
|
)
|
|
assert.deepEqual(lists, ['hanzo-apps.tsx'])
|
|
})
|