Skip to content

Commit b29894d

Browse files
SisyphusZhengDevBot
andauthored
docs(www,tools): B2.3 website qualification on the v0.44 product surface (#1177) (#1300)
* test(tools): gate www public-import boundary against published export maps (#1177, B2.3) The website is an npm-first consumer of the five retained packages. Prove mechanically that every @openelement/* specifier in the shipped site surface (www/app plus vite.config/content-collections/build-pagefind) resolves to a published export subpath — never a private source path — so in-repo workspace resolution is identical to the packed npm artifact boundary. Negative control: a temporary private-source import fails the gate. * docs(www): retire present-tense 0.43 product claims from current-truth pages (#1177, B2.3) The architecture collection described the WC SSR classification, corpus and the Supabase x Cloudflare composition path as 'the 0.43 line' in present tense while the current line is the compiled v0.44 line; the machinery is CI-gated on dev (third-party-wc:smoke, fullstack qualification), so the claims now name the current line and record the 0.43 origin as history. comparison.md scopes its adoption caution to the stable 0.43 line explicitly. The 0.41 migration guide gains a historical-record banner: the current line is consumed fresh via @openelement/create, with no supported 0.43 -> 0.44 in-place upgrade (B2.5 ruling). --------- Co-authored-by: DevBot <devbot@openelement.dev>
1 parent fe4d531 commit b29894d

11 files changed

Lines changed: 197 additions & 10 deletions

tools/autoflow/policy.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ const GATES: readonly GateDefinition[] = [
3737
name: 'package-surface:check',
3838
command: ['deno', 'task', 'package-surface:check'],
3939
tiers: ['push', 'ci', 'release'],
40-
triggers: [/^packages\//, /^deno\.json$/, /^tools\/lib\/package-graph\.ts$/],
40+
triggers: [
41+
/^packages\//,
42+
/^deno\.json$/,
43+
/^tools\/lib\/package-graph\.ts$/,
44+
// #1177 (B2.3): the gate also asserts the www public-import boundary.
45+
/^www\//,
46+
/^tools\/check-package-surface\.ts$/,
47+
/^tools\/lib\/typescript-ast\.ts$/,
48+
],
4149
},
4250
{
4351
name: 'interface:snapshot',

tools/check-package-surface.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
EXPORT_STABILITY_CLASSES,
44
exportClassDrift,
55
extractExportClassMap,
6+
extractWwwPackageSpecifiers,
7+
wwwImportBoundaryDrift,
68
} from './check-package-surface.ts';
79

810
const DOC = `# Package Surface Inventory
@@ -82,3 +84,75 @@ Deno.test('exportClassDrift rejects unknown stability classes and missing entrie
8284
const missing = exportClassDrift(map, '@openelement/element', 'sanitize', []);
8385
assertStringIncludes(missing.join('\n'), 'sanitize');
8486
});
87+
88+
// ─── www public-import boundary (#1177, B2.3) ─────────────
89+
// The website must consume @openelement/* exactly as an external npm consumer
90+
// would: every specifier resolves to a published export subpath of one of the
91+
// five retained packages, never to a private source path. www-local import-map
92+
// aliases declared in www/deno.json are the only permitted non-package
93+
// @openelement specifiers.
94+
95+
const WWW_EXPORTS = new Map([
96+
['@openelement/element', new Set(['.', 'jsx-runtime', 'jsx-dev-runtime', 'sanitize'])],
97+
['@openelement/ui', new Set(['.', 'open-theme-toggle'])],
98+
]);
99+
const WWW_LOCAL_ALIASES = ['@openelement/site-ui/', '@openelement/generated/'];
100+
101+
Deno.test('wwwImportBoundaryDrift accepts published root and subpath specifiers', () => {
102+
assertEquals(
103+
wwwImportBoundaryDrift(
104+
[
105+
'@openelement/element',
106+
'@openelement/ui/open-theme-toggle',
107+
'@openelement/element/sanitize',
108+
],
109+
WWW_EXPORTS,
110+
WWW_LOCAL_ALIASES,
111+
),
112+
[],
113+
);
114+
});
115+
116+
Deno.test('wwwImportBoundaryDrift accepts www-local aliases declared in www/deno.json', () => {
117+
assertEquals(
118+
wwwImportBoundaryDrift(
119+
['@openelement/site-ui/locale.ts', '@openelement/generated/nav'],
120+
WWW_EXPORTS,
121+
WWW_LOCAL_ALIASES,
122+
),
123+
[],
124+
);
125+
});
126+
127+
Deno.test('wwwImportBoundaryDrift rejects unpublished subpaths and unknown packages', () => {
128+
const drift = wwwImportBoundaryDrift(
129+
['@openelement/element/src/protocol/data.ts', '@openelement/content'],
130+
WWW_EXPORTS,
131+
WWW_LOCAL_ALIASES,
132+
);
133+
assertEquals(drift.length, 2);
134+
assertStringIncludes(drift[0], '@openelement/content');
135+
assertStringIncludes(drift[1], 'src/protocol/data.ts');
136+
});
137+
138+
Deno.test('wwwImportBoundaryDrift ignores non-openelement specifiers', () => {
139+
assertEquals(
140+
wwwImportBoundaryDrift(['vite', './local.ts', 'npm:marked@15'], WWW_EXPORTS, []),
141+
[],
142+
);
143+
});
144+
145+
Deno.test('extractWwwPackageSpecifiers collects imports, exports and jsxImportSource', () => {
146+
const source = `/** @jsxImportSource @openelement/element */
147+
import { OpenElement } from '@openelement/element';
148+
import '@openelement/ui/open-theme-toggle';
149+
export { signal } from '@openelement/element';
150+
const lazy = import('@openelement/element/sanitize');
151+
import './relative.ts';
152+
`;
153+
assertEquals(extractWwwPackageSpecifiers(source).sort(), [
154+
'@openelement/element',
155+
'@openelement/element/sanitize',
156+
'@openelement/ui/open-theme-toggle',
157+
]);
158+
});

tools/check-package-surface.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import {
44
RETAINED_PACKAGE_NAMES,
55
} from './project-constants.ts';
66
import { readPackages, releasePublishOrder } from './lib/package-graph.ts';
7+
import { extractStaticModuleSpecifiers } from './lib/typescript-ast.ts';
78
import { OPENELEMENT_EXPORT_FILES } from '../packages/adapter-vite/src/generated-export-files.ts';
89
import { resolve } from '@std/path';
10+
import { exists } from '@std/fs';
11+
import { walk } from '@std/fs/walk';
912

1013
const retainedPackages = [...RETAINED_PACKAGE_NAMES].sort();
1114
const removedPackages = [...REMOVED_PACKAGE_NAMES].sort();
@@ -167,6 +170,53 @@ const APILIST_REQUIRED_PACKAGES = [
167170
'@openelement/adapter-vite',
168171
];
169172

173+
// ─── www public-import boundary (#1177, B2.3) ─────────────
174+
// The website must consume @openelement/* exactly as an external npm consumer
175+
// would: every specifier in the shipped site surface (www/app plus the build
176+
// entry points) resolves to a published export subpath of one of the five
177+
// retained packages — never a private source path. The only permitted
178+
// non-package @openelement specifiers are the www-local import-map aliases
179+
// declared in www/deno.json. www/e2e is deliberately out of scope: its probe
180+
// harness (browser-bundle.ts) bundles package sources in memory and ships
181+
// nothing.
182+
183+
export function extractWwwPackageSpecifiers(source: string, path = 'source.ts'): string[] {
184+
const specifiers = new Set<string>();
185+
for (const { value } of extractStaticModuleSpecifiers(source, path)) {
186+
if (value.startsWith('@openelement/')) specifiers.add(value);
187+
}
188+
for (
189+
const match of source.matchAll(/\/\*\*?\s*@jsxImportSource\s+(@openelement\/[^\s*]+)/g)
190+
) {
191+
specifiers.add(match[1]);
192+
}
193+
return [...specifiers];
194+
}
195+
196+
export function wwwImportBoundaryDrift(
197+
specifiers: readonly string[],
198+
publishedSubpaths: ReadonlyMap<string, ReadonlySet<string>>,
199+
localAliasPrefixes: readonly string[],
200+
): string[] {
201+
const drift: string[] = [];
202+
for (const specifier of specifiers) {
203+
if (!specifier.startsWith('@openelement/')) continue;
204+
if (localAliasPrefixes.some((prefix) => specifier.startsWith(prefix))) continue;
205+
const segments = specifier.split('/');
206+
const pkgName = segments.slice(0, 2).join('/');
207+
const subpath = segments.slice(2).join('/') || '.';
208+
const published = publishedSubpaths.get(pkgName);
209+
if (!published) {
210+
drift.push(`${specifier} does not resolve to a retained published package.`);
211+
continue;
212+
}
213+
if (!published.has(subpath)) {
214+
drift.push(`${specifier} is not a published export subpath of ${pkgName}.`);
215+
}
216+
}
217+
return drift.sort();
218+
}
219+
170220
async function main(): Promise<void> {
171221
for (const dir of ['packages', 'examples', 'www/app', 'tools/third-party-wc-smoke']) {
172222
await rejectRetiredImports(dir);
@@ -391,6 +441,56 @@ async function main(): Promise<void> {
391441
}
392442
}
393443

444+
// ─── www public-import boundary (#1177, B2.3) ─────────────
445+
// Prove the shipped site surface (www/app + its build entry points) imports
446+
// only published export subpaths, so workspace resolution during in-repo
447+
// development is byte-identical to the packed npm artifacts.
448+
449+
const publishedSubpaths = new Map(
450+
packages.map((pkg) => [
451+
pkg.name,
452+
new Set(Object.keys(normalizeExports(pkg.exports))),
453+
]),
454+
);
455+
const wwwConfig = JSON.parse(await Deno.readTextFile('www/deno.json'));
456+
const localAliasPrefixes = Object.keys(wwwConfig.imports ?? {})
457+
.filter((key) => key.startsWith('@openelement/'));
458+
459+
const wwwSurfaceFiles = [
460+
'www/vite.config.ts',
461+
'www/content-collections.ts',
462+
'www/build-pagefind.ts',
463+
];
464+
for await (
465+
const { path: file } of walk('www/app', {
466+
includeDirs: false,
467+
skip: [/(^|\/)dist(\/|$)/],
468+
})
469+
) {
470+
if (/\.(?:ts|tsx)$/.test(file)) wwwSurfaceFiles.push(file);
471+
}
472+
const specifierOrigins = new Map<string, string[]>();
473+
for (const file of wwwSurfaceFiles.sort()) {
474+
if (!await exists(file)) continue;
475+
const text = await Deno.readTextFile(file);
476+
for (const specifier of extractWwwPackageSpecifiers(text, file)) {
477+
const origins = specifierOrigins.get(specifier) ?? [];
478+
origins.push(file);
479+
specifierOrigins.set(specifier, origins);
480+
}
481+
}
482+
for (
483+
const item of wwwImportBoundaryDrift(
484+
[...specifierOrigins.keys()],
485+
publishedSubpaths,
486+
localAliasPrefixes,
487+
)
488+
) {
489+
const specifier = item.split(' ')[0];
490+
const origins = specifierOrigins.get(specifier) ?? [];
491+
failures.push(`www import boundary: ${item} (imported by ${origins.join(', ')})`);
492+
}
493+
394494
if (failures.length > 0) {
395495
console.error('Package surface check failed:');
396496
for (const failure of failures) console.error(`- ${failure}`);

www/content/architecture/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The roadmap earns WC fullstack leadership through compatibility evidence, comple
3737

3838
### WC SSR
3939

40-
The 0.43 line classifies admitted standard, Lit, FAST and Stencil elements for DSD, light DOM or client-only rendering with actionable diagnostics and corpus evidence.
40+
The current line classifies admitted standard, Lit, FAST and Stencil elements for DSD, light DOM or client-only rendering with actionable diagnostics and corpus evidence — a contract first shipped on the 0.43 line and kept green by CI on the compiled line.
4141

4242
### Application loop
4343

www/content/architecture/architecture.zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ order: 10
3535

3636
### WC SSR
3737

38-
0.43 版本线会把已准入的标准、Lit、FAST 与 Stencil 元素分类为 DSD、light DOM 或仅客户端渲染,并提供可操作的诊断与语料证据。
38+
当前版本线会把已准入的标准、Lit、FAST 与 Stencil 元素分类为 DSD、light DOM 或仅客户端渲染,并提供可操作的诊断与语料证据——该契约最初随 0.43 线交付,并在编译型版本线上由 CI 持续验证
3939

4040
### 应用闭环
4141

www/content/architecture/comparison.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ order: 20
4747
- Choose **Astro / Enhance / Lit / Stencil** when a standards-first Web Components story matters and you want to avoid a heavy application runtime.
4848
- Choose **Next.js / Nuxt / SvelteKit** when your product is intentionally built around a React, Vue, or Svelte application model.
4949
- Choose **Fresh** when you want a Deno-native, near-zero-build Preact island experience.
50-
- Do not choose **openElement** when a mature ecosystem, a framework-specific UI runtime, or a ready-made enterprise design system is the main requirement. Teams adopting 0.43.x should validate the documented starter and deployment path against their own production environment.
50+
- Do not choose **openElement** when a mature ecosystem, a framework-specific UI runtime, or a ready-made enterprise design system is the main requirement. Teams adopting the stable 0.43 line should validate the documented starter and deployment path against their own production environment.
5151

5252
## The official composition path
5353

5454
OpenElement × Supabase × Cloudflare is the verified fullstack delivery path, with explicit ownership boundaries: OpenElement owns the application UX; Supabase owns data, Auth, RLS, Storage and Realtime; Cloudflare owns edge delivery, security, cache and async execution. Supabase and Cloudflare are composed providers — never built-in framework features — and a tier-1 boundary gate keeps provider code out of the framework packages.
5555

56-
Delivered in the 0.43 line together with Universal WC SSR. Framework-owned
56+
First shipped on the 0.43 line together with Universal WC SSR and carried by the current compiled line. Framework-owned
5757
production-runtime recovery and cache semantics remain outside the current
5858
contract and have no assigned release version.
5959

www/content/architecture/comparison.zh.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ order: 20
4747
- 选择 **Astro / Enhance / Lit / Stencil** 当标准优先的 Web Components 方案很重要,且想避开沉重的应用运行时时。
4848
- 选择 **Next.js / Nuxt / SvelteKit** 当你的产品明确围绕 React、Vue 或 Svelte 应用模型构建时。
4949
- 选择 **Fresh** 当你想要 Deno 原生、近乎零构建的 Preact island 体验时。
50-
- 不要选择 **openElement** 当主要诉求是成熟生态、框架专属 UI 运行时或现成的企业级设计系统时。采用 0.43.x 的团队仍应在自己的生产环境中验证文档里的 starter 与部署路径。
50+
- 不要选择 **openElement** 当主要诉求是成熟生态、框架专属 UI 运行时或现成的企业级设计系统时。采用稳定 0.43 线的团队仍应在自己的生产环境中验证文档里的 starter 与部署路径。
5151

5252
## 官方组合路径
5353

5454
OpenElement × Supabase × Cloudflare 是经过验证的全栈交付路径,所有权边界明确:OpenElement 负责应用 UX;Supabase 负责数据、Auth、RLS、Storage 与 Realtime;Cloudflare 负责边缘交付、安全、缓存与异步执行。Supabase 与 Cloudflare 是被组合的服务提供方——绝不是框架内建功能——tier-1 边界门禁保证服务提供方代码不进入框架包。
5555

56-
0.43 线与 Universal WC SSR 一同交付。框架自有的生产运行时恢复与缓存语义仍在当前契约之外,尚未分配发布版本。
56+
最初随 0.43 线与 Universal WC SSR 一同交付,并由当前编译型版本线继承。框架自有的生产运行时恢复与缓存语义仍在当前契约之外,尚未分配发布版本。
5757

5858
- [Supabase 配方](https://github.com/open-element/openelement/blob/main/docs/integrations/supabase.md)
5959
- [已验证的参考应用](https://github.com/open-element/openelement/tree/main/examples/supabase-cloudflare-starter)

www/content/architecture/package-compatibility.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ Known packages can be configured as package islands and use available CEM metada
1414

1515
## Current diagnostics
1616

17-
The 0.43 line ships Universal DSD/light/client-only classification,
18-
hydration-mismatch diagnostics and the tracked third-party WC SSR corpus.
17+
The current line ships Universal DSD/light/client-only classification,
18+
hydration-mismatch diagnostics and the tracked third-party WC SSR corpus —
19+
first shipped on the 0.43 line and kept green by CI on the compiled line.
1920
Admission still depends on explicit package-island configuration and observed
2021
metadata; it is not a blanket certification of every third-party component.

www/content/architecture/package-compatibility.zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ order: 90
1414

1515
## 当前诊断
1616

17-
0.43 版本线已经交付通用 DSD/light/client-only 分类、hydration 不匹配诊断与已跟踪的第三方 WC SSR 语料库。准入仍依赖显式 package-island 配置与已观测 metadata,并不意味着对所有第三方组件作笼统认证。
17+
当前版本线交付通用 DSD/light/client-only 分类、hydration 不匹配诊断与已跟踪的第三方 WC SSR 语料库——最初随 0.43 线交付,并在编译型版本线上由 CI 持续验证。准入仍依赖显式 package-island 配置与已观测 metadata,并不意味着对所有第三方组件作笼统认证。

www/content/guide/migration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ lede: 'Every breaking change since 0.40.x, grouped by the version that shipped i
44
order: 75
55
---
66

7+
> Historical record: this page archives the 0.40.x → 0.41/0.42 migrations, written for the retired runtime-authoring line. The current line ({{OPENELEMENT_VERSION}}) is consumed fresh through `@openelement/create` — there is no supported 0.43 → 0.44 in-place upgrade. New projects start from the current starter and the [getting-started guide](/guide/getting-started).
8+
79
## 0.41.x → 0.42
810

911
The stable 0.42 line added request-time surfaces — loaders, actions, progressive-enhancement forms, redirects and Nitro server output. Static-first sites upgraded without a breaking change to the frozen 0.41 surface. ADR-0122 records the accepted 0.42.0 contract.

0 commit comments

Comments
 (0)