Skip to content

feat: share ESM builds and package checks across themes - #5

Merged
rdlabo merged 3 commits into
mainfrom
feat/shared-esm-check
Sep 28, 2026
Merged

rdlabo merged 3 commits into
mainfrom
feat/shared-esm-check

Conversation

@rdlabo

@rdlabo rdlabo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

The iOS 26, iOS 27, and MD3 themes need valid ESM output while keeping TypeScript imports extensionless, matching Ionic's source style. Provide shared build and package-check CLIs so import paths are resolved automatically during normal builds.

rdlabo-build-theme runs the consuming package's tsdown with a shared configuration. It preserves the module structure, generates ESM JavaScript and declarations with resolved relative specifiers, keeps dependencies external, and preserves the theme's CSS output. Sources are not rewritten. Consumers retain tsc --noEmit for type checking and use TypeScript's bundler resolution mode.

rdlabo-check-esm checks the current working directory, or a package directory passed as its first argument. It packs the target without running lifecycle scripts, validates relative JavaScript and declaration specifiers (including literal dynamic imports), and imports the public JavaScript entry points from the extracted tarball. Installed dependencies are reused. The target package supplies TypeScript and tsdown through its development dependencies, so the utilities package gains no runtime dependencies.

Add the CLIs to the published files and npm binaries, document their use and supported export shape, and check the utilities package in CI. Regression tests run the packed CLIs against separate consumer fixtures, covering directory selection, disabled lifecycle scripts, extensionless lazy imports, missing packed files, the ESM declaration, import-time DOM access, declaration references, and a complete build from extensionless sources. The build fixture also verifies dynamic imports, directory imports, unchanged sources, preserved CSS, excluded tests, and a NodeNext TypeScript consumer.

Validation on Node 24.17.0:

  • Package build, lint, and formatting checks passed.
  • CLI regression tests: 7 passed.
  • Existing unit tests: 16 passed.
  • Packed utilities and all public entry points of the iOS 27, iOS 26, and MD3 themes passed the shared ESM check, including declaration references.
  • All three themes passed NodeNext consumer type checking without skipLibCheck.
  • All three themes passed package builds and lint, plus their demo production builds and lint.
  • Demo unit tests passed: iOS 27 (62), iOS 26 (37), and MD3 (36).

Integrations for all three themes are prepared locally and require a published utilities version containing these CLIs. This supersedes the source-extension approach in rdlabo-dev/ionic-theme-ios27#222, which has been closed. Theme integration changes will be separate from this PR.

writeFileSync(join(directory, 'dist/index.js'), "export { value } from './detail.js';");
writeFileSync(join(directory, 'dist/detail.js'), 'export const value = 1;');
writeFileSync(join(directory, 'dist/native.js'), native);
writeFileSync(join(directory, 'dist/lazy.js'), `export const load = () => import(${JSON.stringify(lazy)});`);

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment thread scripts/check-esm.mjs
: ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword
? node.arguments[0]
: undefined;
if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith('.')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 バッククォートの遅延 import にある壊れた参照を見逃す

import(./missing) は文字列リテラル判定から外れ、拡張子と参照先の検査を通過します。公開エントリの読み込み時に実行されなければ、公開後の遅延 import が失敗します。

Learn more

TypeScript の AST では、置換のないバッククォート文字列は StringLiteral ではなく NoSubstitutionTemplateLiteral です。ここでは dist 内の各 JS ファイルを走査しますが、該当する動的 import の参照先は確認されません。公開エントリを読み込むだけでは、実行されない遅延 import の欠落を発見できません。

Example: dist/lazy.js の export const load = () => import(./missing) は検査に成功しますが、利用者が load() を呼ぶとモジュールが見つからず失敗します。

Recommended fix: ts.isNoSubstitutionTemplateLiteral も静的文字列として扱い、既存の拡張子検査とファイル存在検査を適用してください。

Suggested change
if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith('.')) {
if (specifier && (ts.isStringLiteral(specifier) || ts.isNoSubstitutionTemplateLiteral(specifier)) && specifier.text.startsWith('.')) {

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread scripts/check-esm.mjs
// Reuse installed dependencies, but resolve the theme itself from its tarball.
symlinkSync(join(root, 'node_modules'), join(temporary, 'node_modules'), 'dir');
const entries = Object.entries(manifest.exports)
.filter(([, target]) => typeof target === 'object' && typeof target.import === 'string')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 null で除外した export がパッケージ検査を失敗させる

exports に null の除外エントリがあると、オブジェクト判定を通り target.import の参照で失敗します。有効な ESM エントリがあっても検査を完了できません。

Suggested change
.filter(([, target]) => typeof target === 'object' && typeof target.import === 'string')
.filter(([, target]) => target !== null && typeof target === 'object' && typeof target.import === 'string')

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread scripts/check-esm.mjs Outdated
Comment on lines +36 to +37
assert.ok(specifier.text.endsWith('.js'), `${relative}: missing .js extension in ${specifier.text}`);
assert.ok(statSync(resolve(dirname(file), specifier.text)).isFile(), `${relative}: invalid target ${specifier.text}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 クエリ付きの有効な相対 import が検査で失敗する

./detail.js?mode=raw は Node ESM で有効ですが、指定子全体は .js で終わりません。対象ファイルが存在しても検査が失敗し、パッケージの公開を妨げます。

Learn more

Node の ESM 相対指定子では、ファイルパスの後にクエリやフラグメントを付けられます。この検査は指定子全体を .js で終わる文字列として扱い、そのまま statSync に渡します。そのため、有効な指定子でも拡張子検査か存在検査で失敗します。

Example: dist/index.js が import './detail.js?mode=raw' を含み、dist/detail.js が同梱されていても、CLI は「missing .js extension」を出して終了します。

Recommended fix: 相対指定子を参照元ファイルから URL として解決し、URL の pathname に対して拡張子と実在ファイルを検査してください。クエリとフラグメントはファイルシステムのパスに含めないでください。

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@rdlabo rdlabo changed the title feat: share packed ESM checks across theme packages feat: share ESM builds and package checks across themes Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants