-
-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: import maps #11615
base: main
Are you sure you want to change the base?
feat: import maps #11615
Changes from all commits
32af496
98176d8
16541e1
5eb60b7
54d3abf
ab56a63
fa0a13c
6aeb65a
9ebaa59
2ae6118
69e3101
912aaac
4fec9c9
0ef72f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sveltejs/kit': minor | ||
--- | ||
|
||
feat: add importMap.enabled option for generating import maps |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -525,6 +525,8 @@ async function kit({ svelte_config }) { | |
} | ||
}; | ||
|
||
const hash_data_file = `${kit.outDir}/output/hash-data.json`; | ||
|
||
/** @type {import('vite').Plugin} */ | ||
const plugin_compile = { | ||
name: 'vite-plugin-sveltekit-compile', | ||
|
@@ -612,8 +614,16 @@ async function kit({ svelte_config }) { | |
input, | ||
output: { | ||
format: 'esm', | ||
entryFileNames: ssr ? '[name].js' : `${prefix}/[name].[hash].${ext}`, | ||
chunkFileNames: ssr ? 'chunks/[name].js' : `${prefix}/chunks/[name].[hash].${ext}`, | ||
entryFileNames: ssr | ||
? '[name].js' | ||
: kit.importMap.enabled | ||
? `${prefix}/[name].${ext}` | ||
: `${prefix}/[name].[hash].${ext}`, | ||
chunkFileNames: ssr | ||
? 'chunks/[name].js' | ||
: kit.importMap.enabled | ||
? `${prefix}/chunks/[name].${ext}` | ||
: `${prefix}/chunks/[name].[hash].${ext}`, | ||
assetFileNames: `${prefix}/assets/[name].[hash][extname]`, | ||
hoistTransitiveImports: false, | ||
sourcemapIgnoreList | ||
|
@@ -702,8 +712,99 @@ async function kit({ svelte_config }) { | |
*/ | ||
writeBundle: { | ||
sequential: true, | ||
async handler(_options) { | ||
if (secondary_build_started) return; // only run this once | ||
async handler(options, bundle) { | ||
if (secondary_build_started) { | ||
if (svelte_config.kit.importMap.enabled) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would this new chunk of code fit in a separate function? It's hard to tell on github whether it's accessing many things from outside its scope |
||
const length = `${svelte_config.kit.appDir}/immutable/`.length; | ||
|
||
/** @type {Record<string, string>} */ | ||
const renames = {}; | ||
|
||
/** @type {Record<string, string>} */ | ||
const replacements = {}; | ||
|
||
for (const chunk of Object.values(bundle)) { | ||
if (chunk.type === 'chunk') { | ||
const h = hash(chunk.code); | ||
|
||
renames[chunk.fileName] = chunk.fileName.replace(/\.\w+$/, (m) => `.${h}${m}`); | ||
replacements[chunk.fileName] = chunk.fileName.replace(/\.\w+$/, '').slice(length); | ||
} | ||
} | ||
|
||
/** @type {Record<string, string>} */ | ||
const lookup = {}; | ||
|
||
for (const chunk of Object.values(bundle)) { | ||
if (chunk.type !== 'chunk') continue; | ||
|
||
/** @type {string[]} */ | ||
const relative_imports = []; | ||
|
||
/** @param {string} relative */ | ||
const resolve = (relative) => { | ||
const from = chunk.fileName.split('/').slice(0, -1); | ||
const to = relative.split('/'); | ||
|
||
while (to[0] === '..') { | ||
to.shift(); | ||
from.pop(); | ||
} | ||
|
||
if (to[0] === '.') to.shift(); | ||
|
||
return [...from, ...to].join('/'); | ||
}; | ||
|
||
// we do the replace here, instead of in renderChunk, so that it happens after | ||
// other Vite transformations (especially the removal of empty CSS chunks, and | ||
// replacement of __VITE_PRELOAD__ inserts) | ||
let code = chunk.code.replace( | ||
/(from ?|import ?|import\()"(.+?)"/g, | ||
(m, prefix, relative) => { | ||
const resolved = resolve(relative); | ||
const replacement = replacements[resolved]; | ||
|
||
if (replacement) { | ||
relative_imports.push(relative); | ||
lookup[replacement] = renames[resolved]; | ||
return `${prefix}'${replacement}'`; | ||
} | ||
|
||
return m; | ||
} | ||
); | ||
|
||
// this part is super hacky! | ||
if (code.includes('__vite__mapDeps')) { | ||
code = code.replace(/__vite__mapDeps\.viteFileDeps = (\[.+\])/, (_, deps) => { | ||
const mapped = /** @type {string[]} */ (JSON.parse(deps)) | ||
.map((dep) => { | ||
const resolved = resolve(dep); | ||
const renamed = renames[resolved]; | ||
|
||
if (renamed) { | ||
return `"${path.relative(path.dirname(chunk.fileName), renamed)}"`; | ||
} | ||
|
||
return `"${dep}"`; | ||
}) | ||
.join(', '); | ||
|
||
return `__vite__mapDeps.viteFileDeps = [${mapped}]`; | ||
}); | ||
} | ||
|
||
fs.writeFileSync(`${options.dir}/${renames[chunk.fileName]}`, code); | ||
fs.unlinkSync(`${options.dir}/${chunk.fileName}`); | ||
} | ||
|
||
// write metadata to disk, so that it can be used to generate import maps at render time etc | ||
fs.writeFileSync(hash_data_file, JSON.stringify({ renames, lookup })); | ||
} | ||
|
||
return; // only run this once, for the client build | ||
} | ||
|
||
const verbose = vite_config.logLevel === 'info'; | ||
const log = logger({ verbose }); | ||
|
@@ -776,9 +877,22 @@ async function kit({ svelte_config }) { | |
`${out}/client/${kit.appDir}/immutable/assets` | ||
); | ||
|
||
/** @type {Array<[string, string]>} */ | ||
let import_map_lookup = []; | ||
|
||
/** @type {import('vite').Manifest} */ | ||
const client_manifest = JSON.parse(read(`${out}/client/${vite_config.build.manifest}`)); | ||
|
||
if (fs.existsSync(hash_data_file)) { | ||
const hash_data = JSON.parse(read(hash_data_file)); | ||
|
||
for (const chunk of Object.values(client_manifest)) { | ||
chunk.file = hash_data.renames[chunk.file]; | ||
} | ||
|
||
import_map_lookup = Object.entries(hash_data.lookup); | ||
} | ||
|
||
const deps_of = /** @param {string} f */ (f) => | ||
find_deps(client_manifest, posixify(path.relative('.', f)), false); | ||
const start = deps_of(`${runtime_directory}/client/entry.js`); | ||
|
@@ -792,7 +906,8 @@ async function kit({ svelte_config }) { | |
fonts: [...start.fonts, ...app.fonts], | ||
uses_env_dynamic_public: output.some( | ||
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] | ||
) | ||
), | ||
import_map_lookup | ||
}; | ||
|
||
const css = output.filter( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -414,6 +414,15 @@ declare module '@sveltejs/kit' { | |
*/ | ||
errorTemplate?: string; | ||
}; | ||
importMap?: { | ||
/** | ||
* Whether to generate import maps. This will result in better long term cacheability, as changes to a single module will no longer invalidate all its dependents. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there any guidance we can offer as to when this should be done? like maybe it's worth it until the site grows to a certain size? |
||
* However, it will increase the size of the HTML document, and force `modulepreload` links to be part of the document rather than being added as HTTP headers. | ||
* @default false; | ||
* @since 2.4.0 | ||
*/ | ||
enabled?: boolean; | ||
}; | ||
/** | ||
* Inline CSS inside a `<style>` block at the head of the HTML. This option is a number that specifies the maximum length of a CSS file in UTF-16 code units, as specified by the [String.length](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length) property, to be inlined. All CSS files needed for the page and smaller than this value are merged and inlined in a `<style>` block. | ||
* | ||
|
@@ -1557,6 +1566,7 @@ declare module '@sveltejs/kit' { | |
stylesheets: string[]; | ||
fonts: string[]; | ||
uses_env_dynamic_public: boolean; | ||
import_map_lookup: Array<[string, string]>; | ||
} | null; | ||
server_manifest: import('vite').Manifest; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,10 @@ const config = { | |
runtime: 'edge' | ||
}), | ||
|
||
importMap: { | ||
enabled: true | ||
}, | ||
|
||
paths: { | ||
relative: true | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could probably use a comment here about how we're controlling the hashing when import map is enabled and how it interacts with that