diff --git a/.github/workflows/actions/build-core-stencil-prerelease/action.yml b/.github/workflows/actions/build-core-stencil-prerelease/action.yml index 7a51a5df2d6..8cc7016349f 100644 --- a/.github/workflows/actions/build-core-stencil-prerelease/action.yml +++ b/.github/workflows/actions/build-core-stencil-prerelease/action.yml @@ -25,6 +25,10 @@ runs: run: npm run build -- --ci --debug --verbose working-directory: ./core shell: bash + - name: 🔍 Verify Lazy Imports + run: npm run test.lazy-imports + working-directory: ./core + shell: bash - uses: ./.github/workflows/actions/upload-archive with: name: ionic-core diff --git a/.github/workflows/actions/build-core/action.yml b/.github/workflows/actions/build-core/action.yml index 2ac2fd00836..523b303139a 100644 --- a/.github/workflows/actions/build-core/action.yml +++ b/.github/workflows/actions/build-core/action.yml @@ -27,6 +27,10 @@ runs: run: npm run build -- --ci working-directory: ./core shell: bash + - name: 🔍 Verify Lazy Imports + run: npm run test.lazy-imports + working-directory: ./core + shell: bash - uses: ./.github/workflows/actions/upload-archive with: name: ionic-core diff --git a/core/package.json b/core/package.json index 7145248f915..59789c45b23 100644 --- a/core/package.json +++ b/core/package.json @@ -99,7 +99,8 @@ "test.e2e.update-snapshots": "npm run test.e2e -- --update-snapshots='changed'", "test.watch": "jest --watch --no-cache", "test.treeshake": "node scripts/treeshaking.js dist/index.js", - "validate": "npm run lint && npm run test && npm run build && npm run test.treeshake", + "test.lazy-imports": "node scripts/verify/lazy-imports.js", + "validate": "npm run lint && npm run test && npm run build && npm run test.lazy-imports && npm run test.treeshake", "docker.build": "docker build -t ionic-playwright .", "test.e2e.docker": "npm run docker.build && node ./scripts/docker.mjs", "test.e2e.docker.update-snapshots": "npm run test.e2e.docker -- --update-snapshots='changed'", diff --git a/core/scripts/verify/lazy-imports.js b/core/scripts/verify/lazy-imports.js new file mode 100644 index 00000000000..d0008130474 --- /dev/null +++ b/core/scripts/verify/lazy-imports.js @@ -0,0 +1,56 @@ +/** + * Fails if the built lazy loader is missing a literal import path for any component bundle. + * Not in `scripts/testing/` because `vercel-build.sh` publishes that directory. + * https://github.com/ionic-team/ionic-framework/issues/31333 + */ +const fs = require('fs'); +const path = require('path'); + +const PACKAGE_ROOT = path.resolve(__dirname, '..', '..'); +const OUTPUT_DIRS = ['dist/esm', 'dist/cjs']; + +function main() { + const failures = OUTPUT_DIRS.map(verify).filter((message) => message !== null); + + if (failures.length > 0) { + failures.forEach((message) => console.error(message)); + process.exit(1); + } + + console.error(`Success! Every lazy bundle is imported by path in ${OUTPUT_DIRS.join(' and ')}`); +} + +function verify(dir) { + const resolved = path.join(PACKAGE_ROOT, dir); + + if (!fs.existsSync(resolved)) { + return `${dir} does not exist. Run npm run build first.`; + } + + const files = fs.readdirSync(resolved).filter((file) => file.endsWith('.js')); + const entries = files.filter((file) => file.endsWith('.entry.js')); + + if (entries.length === 0) { + return `${dir} does not contain any .entry.js files.`; + } + + // A non-loader chunk that happens to name an entry file would be a false pass. + const loader = files + .filter((file) => !file.endsWith('.entry.js')) + .map((file) => fs.readFileSync(path.join(resolved, file), 'utf-8')) + .find((source) => /switch\s*\(bundleId\)/.test(source)); + + if (loader === undefined) { + return `${dir} has no lazy loader with a bundle id switch. Check that extras.enableImportInjection is still set in core/stencil.config.ts.`; + } + + const missing = entries.filter((entry) => !loader.includes(`'./${entry}'`) && !loader.includes(`"./${entry}"`)); + + if (missing.length > 0) { + return `${dir} is missing an import for ${missing.length} of ${entries.length} lazy bundles: ${missing.join(', ')}`; + } + + return null; +} + +main(); diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 8a090b65bff..948c5bff4a9 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -274,5 +274,11 @@ export const config: Config = { * the default behavior (slated for a future Stencil major version). */ experimentalScopedSlotChanges: true, + /** + * Vite 8 / Rolldown honors the `@vite-ignore` comment on the lazy loader's dynamic import + * and never emits the `.entry.js` chunks, so requests for them 404. This flag prepends a + * switch of literal import paths those bundlers can resolve. + */ + enableImportInjection: true, } };