Add support for auto-discovering fallback content#320
Conversation
commit: |
Fryuni
left a comment
There was a problem hiding this comment.
Dynamically importing a runtime defined module goes against a lot of work that has been going on the ecosystem. This can break the library for many users.
| "compilerOptions": { | ||
| "module": "CommonJS", | ||
| "target": "ES2019", | ||
| "module": "ES2020", |
There was a problem hiding this comment.
This drops support for everyone using CJS on their projects. I'm in favor of it, CJS is outdated and a anchor dragging us, but just making sure this is a an intended decision.
It might still work for most cases, but TS won't be guaranteeing that so seemingly unrelated changes could suddenly downstream CJS consumers.
There was a problem hiding this comment.
We need at least ES2020 to preserve the import statement, otherwise, it will be compiled into require, and won't work as intended when bundled since it's synchronous and we need asynchronous importing so the bundler can do code splitting.
There was a problem hiding this comment.
From my experience, it usually works out of the box, as modern setups support both CJS and ES6 imports. The only exception that typically requires manual configuration is Jest. Do you have any other concerns here?
src/api/fetchContent.ts
Outdated
|
|
||
| return promise; | ||
| try { | ||
| return {content: (await import(`@croct/content/slot/${file}`)).default}; |
There was a problem hiding this comment.
This might cause problems when using the plug with a bundler since they won't be able to resolve the dynamic import.
Vite/Rollup, for example, will straight out refuse to bundle this even with the official dynamic import plugin1 and require a custom plugin to hook into the process and ignore the errors. The value from the fallback won't be loadable from the bundle even with the plugin. It would need to be import(/* @vite-ignore */ `@croct/content/slot/${file}`).
Esbuild will have a similar problem. And maintaining comments to have every bundler ignore this will:
- Be a pain to maintain;
- Only make the code fail at runtime instead of during build since the bundle won't have the code for the module
Footnotes
This PR was originally focused on supporting Next.js only, because both Webpack and Turbopack can automatically recognize the path and generate separate chunks for each JSON file. However, I then decided to include support for Plug JS, taking a feature-degradation approach. I didn't anticipate that the builds would fail to compile instead of simply retaining the import statement in the final output. It would have been acceptable for the import to fail at runtime, as the current behavior doesn't change if that happens. Moreover, using import maps would enable this in browsers as well. I can continue to provide support exclusively for Next.js, but then projects using Webpack won't benefit from this new feature. PS: I bundled the Plug itself using rollout before proposing it, and it worked just fine. Output: fetch(e, t = {}) {
const [n, r = "latest"] = e.split("@"), i = this.sdk.getLogger();
return this.sdk.contentFetcher.fetch(n, "latest" === r ? t : {...t, version: r}).catch((async o => {
i.error(`Failed to fetch content for slot "${n}@${r}": ${ni.formatCause(o)}`);
const s = t.preferredLocale ?? null, a = `${null === s ? "" : `${s}/`}${e}.json`;
try {
return {content: (await import(`@croct/content/slot/${a}`)).default}
} catch {
throw o
}
}))
}What are your thoughts? |
|
After our discussion here, I believe I've come up with a much more robust and widely supported proposal: All widely used bundlers, such as Vite, Rollup, and Webpack, support dynamic imports. The main issue arises when importing dynamic paths because the bundler has no way of determining which imports will be used at build time. Even if it can partially identify them (as Webpack does), it's tricky to decide how to split them into chunks that load only when needed. A simple way to address this is to avoid dynamic paths altogether and instead generate a static import map with literal relative paths, which all production-grade bundlers support. In practical terms:
Here's what the generated module looks like: const contentMap = {
"en": {
"magic-ui-user-reviews": () => import("./en/magic-ui-user-reviews@1.json"),
"magic-ui-user-reviews@1": () => import("./en/magic-ui-user-reviews@1.json"),
},
};
const defaultLocale = "en";
export function loadContent(slotId, language = defaultLocale) {
if (contentMap[language]?.[slotId]) {
return contentMap[language][slotId]().then((module) => module.default);
}
return language !== defaultLocale ? loadContent(slotId) : Promise.resolve(null);
}You can see here the changes to the Now, not only the fallback can be automatically found, but bundlers create chunks for each content and only load them when needed:
|



Summary
This pull request adds support for the SDK to auto-discover fallback content from the
@croct/contentpackage generated by the CLI.Checklist