feat: share ESM builds and package checks across themes - #5
Conversation
| 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)});`); |
| : ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword | ||
| ? node.arguments[0] | ||
| : undefined; | ||
| if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith('.')) { |
There was a problem hiding this comment.
🔴 バッククォートの遅延 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 も静的文字列として扱い、既存の拡張子検査とファイル存在検査を適用してください。
| if (specifier && ts.isStringLiteral(specifier) && specifier.text.startsWith('.')) { | |
| if (specifier && (ts.isStringLiteral(specifier) || ts.isNoSubstitutionTemplateLiteral(specifier)) && specifier.text.startsWith('.')) { |
Was this helpful? React with 👍 or 👎 to provide feedback.
| // 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') |
There was a problem hiding this comment.
🟡 null で除外した export がパッケージ検査を失敗させる
exports に null の除外エントリがあると、オブジェクト判定を通り target.import の参照で失敗します。有効な ESM エントリがあっても検査を完了できません。
| .filter(([, target]) => typeof target === 'object' && typeof target.import === 'string') | |
| .filter(([, target]) => target !== null && typeof target === 'object' && typeof target.import === 'string') |
Was this helpful? React with 👍 or 👎 to provide feedback.
| 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}`); |
There was a problem hiding this comment.
🟡 クエリ付きの有効な相対 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 に対して拡張子と実在ファイルを検査してください。クエリとフラグメントはファイルシステムのパスに含めないでください。
Was this helpful? React with 👍 or 👎 to provide feedback.
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-themeruns 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 retaintsc --noEmitfor type checking and use TypeScript's bundler resolution mode.rdlabo-check-esmchecks 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:
skipLibCheck.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.