@sveltejs/kit
Advanced tools
Comparing version
{ | ||
"name": "@sveltejs/kit", | ||
"version": "2.21.2", | ||
"version": "2.21.3", | ||
"description": "SvelteKit is the fastest way to build Svelte apps", | ||
@@ -32,3 +32,4 @@ "keywords": [ | ||
"set-cookie-parser": "^2.6.0", | ||
"sirv": "^3.0.0" | ||
"sirv": "^3.0.0", | ||
"vitefu": "^1.0.6" | ||
}, | ||
@@ -35,0 +36,0 @@ "devDependencies": { |
@@ -66,2 +66,3 @@ import { join } from 'node:path'; | ||
/** @type {Map<string, { page_options: Record<string, any> | null, children: string[] }>} */ | ||
const static_exports = new Map(); | ||
@@ -77,2 +78,3 @@ | ||
null, | ||
null, | ||
output_config, | ||
@@ -79,0 +81,0 @@ static_exports |
@@ -16,7 +16,8 @@ import fs from 'node:fs'; | ||
* @param {import('vite').Manifest | null} client_manifest | ||
* @param {import('vite').Rollup.OutputAsset[] | null} css | ||
* @param {import('vite').Rollup.OutputBundle | null} server_bundle | ||
* @param {import('vite').Rollup.RollupOutput['output'] | null} client_bundle | ||
* @param {import('types').RecursiveRequired<import('types').ValidatedConfig['kit']['output']>} output_config | ||
* @param {Map<string, Record<string, any> | null>} static_exports | ||
* @param {Map<string, { page_options: Record<string, any> | null, children: string[] }>} static_exports | ||
*/ | ||
export async function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, css, output_config, static_exports) { | ||
export async function build_server_nodes(out, kit, manifest_data, server_manifest, client_manifest, server_bundle, client_bundle, output_config, static_exports) { | ||
mkdirp(`${out}/server/nodes`); | ||
@@ -26,53 +27,35 @@ mkdirp(`${out}/server/stylesheets`); | ||
/** @type {Map<string, string>} */ | ||
const stylesheet_lookup = new Map(); | ||
const stylesheets_to_inline = new Map(); | ||
if (css) { | ||
/** @type {Set<string>} */ | ||
const client_stylesheets = new Set(); | ||
for (const key in client_manifest) { | ||
client_manifest[key].css?.forEach((filename) => { | ||
client_stylesheets.add(filename); | ||
}); | ||
} | ||
if (server_bundle && client_bundle && kit.inlineStyleThreshold > 0) { | ||
const client = get_stylesheets(client_bundle); | ||
/** @type {Map<number, string[]>} */ | ||
const server_stylesheets = new Map(); | ||
manifest_data.nodes.forEach((node, i) => { | ||
if (!node.component || !server_manifest[node.component]) return; | ||
const server_chunks = Object.values(server_bundle); | ||
const server = get_stylesheets(server_chunks); | ||
const { stylesheets } = find_deps(server_manifest, node.component, false); | ||
if (stylesheets.length) { | ||
server_stylesheets.set(i, stylesheets); | ||
// map server stylesheet name to the client stylesheet name | ||
for (const [id, client_stylesheet] of client.stylesheets_used) { | ||
const server_stylesheet = server.stylesheets_used.get(id); | ||
if (!server_stylesheet) { | ||
continue; | ||
} | ||
}); | ||
client_stylesheet.forEach((file, i) => { | ||
stylesheets_to_inline.set(file, server_stylesheet[i]); | ||
}) | ||
} | ||
for (const asset of css) { | ||
// ignore dynamically imported stylesheets since we don't need to inline those | ||
if (!client_stylesheets.has(asset.fileName) || asset.source.length >= kit.inlineStyleThreshold) { | ||
continue; | ||
// filter out stylesheets that should not be inlined | ||
for (const [fileName, content] of client.stylesheet_content) { | ||
if (content.length >= kit.inlineStyleThreshold) { | ||
stylesheets_to_inline.delete(fileName); | ||
} | ||
} | ||
// We know that the names for entry points are numbers. | ||
const [index] = basename(asset.fileName).split('.'); | ||
// There can also be other CSS files from shared components | ||
// for example, which we need to ignore here. | ||
if (isNaN(+index)) continue; | ||
const file = `${out}/server/stylesheets/${index}.js`; | ||
// we need to inline the server stylesheet instead of the client one | ||
// so that asset paths are correct on document load | ||
const filenames = server_stylesheets.get(+index); | ||
if (!filenames) { | ||
throw new Error('This should never happen, but if it does, it means we failed to find the server stylesheet for a node.'); | ||
// map server stylesheet source to the client stylesheet name | ||
for (const [client_file, server_file] of stylesheets_to_inline) { | ||
const source = server.stylesheet_content.get(server_file); | ||
if (!source) { | ||
throw new Error(`Server stylesheet source not found for client stylesheet ${client_file}`); | ||
} | ||
const sources = filenames.map((filename) => { | ||
return fs.readFileSync(`${out}/server/${filename}`, 'utf-8'); | ||
}); | ||
fs.writeFileSync(file, `// ${filenames.join(', ')}\nexport default ${s(sources.join('\n'))};`); | ||
stylesheet_lookup.set(asset.fileName, index); | ||
stylesheets_to_inline.set(client_file, source); | ||
} | ||
@@ -144,4 +127,5 @@ } | ||
// eagerly load stylesheets and fonts imported by the SSR-ed page to avoid FOUC. | ||
// If it is not used during SSR, it can be lazily loaded in the browser. | ||
// eagerly load client stylesheets and fonts imported by the SSR-ed page to avoid FOUC. | ||
// However, if it is not used during SSR (not present in the server manifest), | ||
// then it can be lazily loaded in the browser. | ||
@@ -161,16 +145,16 @@ /** @type {import('types').AssetDependencies | undefined} */ | ||
/** @type {Set<string>} */ | ||
const css_used_by_server = new Set(); | ||
const eager_css = new Set(); | ||
/** @type {Set<string>} */ | ||
const assets_used_by_server = new Set(); | ||
const eager_assets = new Set(); | ||
entry.stylesheet_map.forEach((value, key) => { | ||
// pages and layouts are named as node indexes in the client manifest | ||
// so we need to use the original filename when checking against the server manifest | ||
if (key === entry_path) { | ||
key = node.component ?? key; | ||
entry.stylesheet_map.forEach((value, filepath) => { | ||
// pages and layouts are renamed to node indexes when optimised for the client | ||
// so we use the original filename instead to check against the server manifest | ||
if (filepath === entry_path) { | ||
filepath = node.component ?? filepath; | ||
} | ||
if (component?.stylesheet_map.has(key) || universal?.stylesheet_map.has(key)) { | ||
value.css.forEach(file => css_used_by_server.add(file)); | ||
value.assets.forEach(file => assets_used_by_server.add(file)); | ||
if (component?.stylesheet_map.has(filepath) || universal?.stylesheet_map.has(filepath)) { | ||
value.css.forEach(file => eager_css.add(file)); | ||
value.assets.forEach(file => eager_assets.add(file)); | ||
} | ||
@@ -180,4 +164,4 @@ }); | ||
imported = entry.imports; | ||
stylesheets = Array.from(css_used_by_server); | ||
fonts = filter_fonts(Array.from(assets_used_by_server)); | ||
stylesheets = Array.from(eager_css); | ||
fonts = filter_fonts(Array.from(eager_assets)); | ||
} | ||
@@ -192,15 +176,22 @@ | ||
/** @type {string[]} */ | ||
const styles = []; | ||
const inline_styles = []; | ||
stylesheets.forEach((file) => { | ||
if (stylesheet_lookup.has(file)) { | ||
const index = stylesheet_lookup.get(file); | ||
const name = `stylesheet_${index}`; | ||
imports.push(`import ${name} from '../stylesheets/${index}.js';`); | ||
styles.push(`\t${s(file)}: ${name}`); | ||
stylesheets.forEach((file, i) => { | ||
if (stylesheets_to_inline.has(file)) { | ||
const filename = basename(file); | ||
const dest = `${out}/server/stylesheets/${filename}.js`; | ||
const source = stylesheets_to_inline.get(file); | ||
if (!source) { | ||
throw new Error(`Server stylesheet source not found for client stylesheet ${file}`); | ||
} | ||
fs.writeFileSync(dest, `// ${filename}\nexport default ${s(source)};`); | ||
const name = `stylesheet_${i}`; | ||
imports.push(`import ${name} from '../stylesheets/${filename}.js';`); | ||
inline_styles.push(`\t${s(file)}: ${name}`); | ||
} | ||
}); | ||
if (styles.length > 0) { | ||
exports.push(`export const inline_styles = () => ({\n${styles.join(',\n')}\n});`); | ||
if (inline_styles.length > 0) { | ||
exports.push(`export const inline_styles = () => ({\n${inline_styles.join(',\n')}\n});`); | ||
} | ||
@@ -214,1 +205,35 @@ | ||
} | ||
/** | ||
* @param {(import('vite').Rollup.OutputAsset | import('vite').Rollup.OutputChunk)[]} chunks | ||
*/ | ||
function get_stylesheets(chunks) { | ||
/** | ||
* A map of module IDs and the stylesheets they use. | ||
* @type {Map<string, string[]>} | ||
*/ | ||
const stylesheets_used = new Map(); | ||
/** | ||
* A map of stylesheet names and their content. | ||
* @type {Map<string, string>} | ||
*/ | ||
const stylesheet_content = new Map(); | ||
for (const chunk of chunks) { | ||
if (chunk.type === 'asset') { | ||
if (chunk.fileName.endsWith('.css')) { | ||
stylesheet_content.set(chunk.fileName, chunk.source.toString()); | ||
} | ||
continue; | ||
} | ||
if (chunk.viteMetadata?.importedCss.size) { | ||
const css = Array.from(chunk.viteMetadata.importedCss); | ||
for (const id of chunk.moduleIds) { | ||
stylesheets_used.set(id, css ); | ||
} | ||
} | ||
} | ||
return { stylesheets_used, stylesheet_content }; | ||
} |
@@ -358,2 +358,3 @@ import fs from 'node:fs'; | ||
// they fire in rapid succession, causing needless invocations. | ||
// These watchers only run for routes, param matchers, and client hooks. | ||
watch('add', () => debounce(update_manifest)); | ||
@@ -365,4 +366,7 @@ watch('unlink', () => debounce(update_manifest)); | ||
if (/\+(page|layout).*$/.test(file)) { | ||
invalidate_page_options(path.relative(cwd, file)); | ||
} | ||
sync.update(svelte_config, manifest_data, file); | ||
invalidate_page_options(path.relative(cwd, file)); | ||
}); | ||
@@ -394,6 +398,6 @@ | ||
// changing the svelte config requires restarting the dev server | ||
// the config is only read on start and passed on to vite-plugin-svelte | ||
// which needs up-to-date values to operate correctly | ||
vite.watcher.on('change', async (file) => { | ||
// changing the svelte config requires restarting the dev server | ||
// the config is only read on start and passed on to vite-plugin-svelte | ||
// which needs up-to-date values to operate correctly | ||
if (path.basename(file) === 'svelte.config.js') { | ||
@@ -400,0 +404,0 @@ console.log(`svelte config changed, restarting vite dev-server. changed file: ${file}`); |
@@ -39,2 +39,3 @@ import fs from 'node:fs'; | ||
import { compact } from '../../utils/array.js'; | ||
import { crawlFrameworkPkgs } from 'vitefu'; | ||
@@ -231,3 +232,3 @@ const cwd = process.cwd(); | ||
*/ | ||
config(config, config_env) { | ||
async config(config, config_env) { | ||
initial_config = config; | ||
@@ -255,2 +256,16 @@ vite_config_env = config_env; | ||
const packages_depending_on_svelte_kit = ( | ||
await crawlFrameworkPkgs({ | ||
root: cwd, | ||
isBuild: is_build, | ||
viteUserConfig: config, | ||
isSemiFrameworkPkgByJson: (pkg_json) => { | ||
return ( | ||
!!pkg_json.dependencies?.['@sveltejs/kit'] || | ||
!!pkg_json.peerDependencies?.['@sveltejs/kit'] | ||
); | ||
} | ||
}) | ||
).ssr.noExternal; | ||
// dev and preview config can be shared | ||
@@ -312,3 +327,7 @@ /** @type {import('vite').UserConfig} */ | ||
// and https://vitest.dev/config/#deps-registernodeloader | ||
'@sveltejs/kit' | ||
'@sveltejs/kit', | ||
// We need to bundle any packages depending on @sveltejs/kit so that | ||
// everyone uses the same instances of classes such as `Redirect` | ||
// which we use in `instanceof` checks | ||
...packages_depending_on_svelte_kit | ||
] | ||
@@ -794,3 +813,3 @@ } | ||
sequential: true, | ||
async handler(_options) { | ||
async handler(_options, bundle) { | ||
if (secondary_build_started) return; // only run this once | ||
@@ -851,3 +870,3 @@ | ||
const { output } = /** @type {import('vite').Rollup.RollupOutput} */ ( | ||
const { output: client_chunks } = /** @type {import('vite').Rollup.RollupOutput} */ ( | ||
await vite.build({ | ||
@@ -895,3 +914,3 @@ configFile: vite_config.configFile, | ||
fonts: [...start.fonts, ...app.fonts], | ||
uses_env_dynamic_public: output.some( | ||
uses_env_dynamic_public: client_chunks.some( | ||
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] | ||
@@ -945,3 +964,3 @@ ) | ||
fonts: start.fonts, | ||
uses_env_dynamic_public: output.some( | ||
uses_env_dynamic_public: client_chunks.some( | ||
(chunk) => chunk.type === 'chunk' && chunk.modules[env_dynamic_public] | ||
@@ -953,3 +972,3 @@ ) | ||
const style = /** @type {import('rollup').OutputAsset} */ ( | ||
output.find( | ||
client_chunks.find( | ||
(chunk) => | ||
@@ -969,7 +988,2 @@ chunk.type === 'asset' && | ||
const css = output.filter( | ||
/** @type {(value: any) => value is import('vite').Rollup.OutputAsset} */ | ||
(value) => value.type === 'asset' && value.fileName.endsWith('.css') | ||
); | ||
// regenerate manifest now that we have client entry... | ||
@@ -993,3 +1007,4 @@ fs.writeFileSync( | ||
client_manifest, | ||
css, | ||
bundle, | ||
client_chunks, | ||
svelte_config.kit.output, | ||
@@ -996,0 +1011,0 @@ static_exports |
@@ -7,6 +7,6 @@ import { tsPlugin } from '@sveltejs/acorn-typescript'; | ||
const page_options = new Set([...inheritable_page_options, 'entries']); | ||
const valid_page_options = new Set([...inheritable_page_options, 'entries']); | ||
const skip_parsing_regex = new RegExp( | ||
`${Array.from(page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)` | ||
`${Array.from(valid_page_options).join('|')}|(?:export[\\s\\n]+\\*[\\s\\n]+from)` | ||
); | ||
@@ -17,10 +17,9 @@ | ||
/** | ||
* Collects exported page options from a +page.js/+layout.js file. | ||
* We ignore reassignments and use the declared value. | ||
* Returns `null` if any export is too difficult to analyse. | ||
* @param {string} filename | ||
* Collects page options from a +page.js/+layout.js file, ignoring reassignments | ||
* and using the declared value. Returns `null` if any export is too difficult to analyse. | ||
* @param {string} filename The name of the file to report when an error occurs | ||
* @param {string} input | ||
* @returns {Record<string, any> | null} | ||
*/ | ||
export function statically_analyse_exports(filename, input) { | ||
export function statically_analyse_page_options(filename, input) { | ||
// if there's a chance there are no page exports or export all declaration, | ||
@@ -39,3 +38,3 @@ // then we can skip the AST parsing which is expensive | ||
/** @type {Map<string, import('acorn').Literal['value']>} */ | ||
const static_exports = new Map(); | ||
const page_options = new Map(); | ||
@@ -47,3 +46,3 @@ for (const statement of source.body) { | ||
statement.exported && | ||
!page_options.has(get_name(statement.exported)) | ||
!valid_page_options.has(get_name(statement.exported)) | ||
) { | ||
@@ -67,3 +66,3 @@ continue; | ||
const exported_name = get_name(specifier.exported); | ||
if (!page_options.has(exported_name)) { | ||
if (!valid_page_options.has(exported_name)) { | ||
continue; | ||
@@ -117,3 +116,3 @@ } | ||
if (variable_declarator.init?.type === 'Literal') { | ||
static_exports.set( | ||
page_options.set( | ||
/** @type {string} */ (export_specifiers.get(variable_declarator.id.name)), | ||
@@ -147,3 +146,3 @@ variable_declarator.init.value | ||
if (statement.declaration.type !== 'VariableDeclaration') { | ||
if (page_options.has(statement.declaration.id.name)) { | ||
if (valid_page_options.has(statement.declaration.id.name)) { | ||
return null; | ||
@@ -159,3 +158,3 @@ } | ||
if (!page_options.has(declaration.id.name)) { | ||
if (!valid_page_options.has(declaration.id.name)) { | ||
continue; | ||
@@ -165,3 +164,3 @@ } | ||
if (declaration.init?.type === 'Literal') { | ||
static_exports.set(declaration.id.name, declaration.init.value); | ||
page_options.set(declaration.id.name, declaration.init.value); | ||
continue; | ||
@@ -175,6 +174,6 @@ } | ||
return Object.fromEntries(static_exports); | ||
return Object.fromEntries(page_options); | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
error.message = `Failed to statically analyse ${filename}. ${error.message}`; | ||
error.message = `Failed to statically analyse page options for ${filename}. ${error.message}`; | ||
} | ||
@@ -196,3 +195,3 @@ throw error; | ||
* resolve: (file: string) => Promise<Record<string, any>>; | ||
* static_exports?: Map<string, Record<string, any> | null>; | ||
* static_exports?: Map<string, { page_options: Record<string, any> | null, children: string[] }>; | ||
* }} opts | ||
@@ -204,54 +203,56 @@ */ | ||
* @param {import('types').PageNode} node | ||
* @returns {Promise<import('types').UniversalNode | null>} | ||
* @returns {Promise<Record<string, any> | null>} | ||
*/ | ||
const get_page_options = async (node) => { | ||
if (node.universal && static_exports.has(node.universal)) { | ||
return /** @type {import('types').UniversalNode | null} */ ( | ||
static_exports.get(node.universal) | ||
); | ||
const key = node.universal || node.server; | ||
if (key && static_exports.has(key)) { | ||
return { ...static_exports.get(key)?.page_options }; | ||
} | ||
/** @type {Record<string, any> | null} */ | ||
/** @type {Record<string, any>} */ | ||
let page_options = {}; | ||
if (node.server) { | ||
const module = await resolve(node.server); | ||
for (const key in inheritable_page_options) { | ||
if (key in module) { | ||
page_options[key] = module[key]; | ||
} | ||
} | ||
} | ||
if (node.parent) { | ||
const parent_options = await get_page_options(node.parent); | ||
if (node.universal) { | ||
let universal_exports = static_exports.get(node.universal); | ||
if (universal_exports === undefined) { | ||
const input = read(node.universal); | ||
universal_exports = statically_analyse_exports(node.universal, input); | ||
const parent_key = node.parent.universal || node.parent.server; | ||
if (key && parent_key) { | ||
static_exports.get(parent_key)?.children.push(key); | ||
} | ||
if (universal_exports === null) { | ||
static_exports.set(node.universal, null); | ||
if (parent_options === null) { | ||
// if the parent cannot be analysed, we can't know what page options | ||
// the child node inherits, so we also mark it as unanalysable | ||
if (key) { | ||
static_exports.set(key, { page_options: null, children: [] }); | ||
} | ||
return null; | ||
} | ||
page_options = { ...page_options, ...universal_exports }; | ||
page_options = { ...parent_options }; | ||
} | ||
if (node.parent) { | ||
const parent_options = await get_page_options(node.parent); | ||
if (parent_options === null) { | ||
// if the parent cannot be statically analysed, we can't know what | ||
// page options the current node inherits, so we invalidate it too | ||
if (node.universal) { | ||
static_exports.set(node.universal, null); | ||
if (node.server) { | ||
const module = await resolve(node.server); | ||
for (const page_option in inheritable_page_options) { | ||
if (page_option in module) { | ||
page_options[page_option] = module[page_option]; | ||
} | ||
} | ||
} | ||
if (node.universal) { | ||
const input = read(node.universal); | ||
const universal_page_options = statically_analyse_page_options(node.universal, input); | ||
if (universal_page_options === null) { | ||
static_exports.set(node.universal, { page_options: null, children: [] }); | ||
return null; | ||
} | ||
page_options = { ...parent_options, ...page_options }; | ||
page_options = { ...page_options, ...universal_page_options }; | ||
} | ||
if (node.universal) { | ||
static_exports.set(node.universal, page_options); | ||
if (key) { | ||
static_exports.set(key, { page_options, children: [] }); | ||
} | ||
@@ -264,9 +265,12 @@ | ||
* @param {string} file | ||
* @returns {void} | ||
*/ | ||
const invalidate_page_options = (file) => { | ||
static_exports.get(file)?.children.forEach((child) => static_exports.delete(child)); | ||
static_exports.delete(file); | ||
}; | ||
return { get_page_options, invalidate_page_options }; | ||
return { | ||
get_page_options, | ||
invalidate_page_options | ||
}; | ||
} |
import * as devalue from 'devalue'; | ||
import { DEV } from 'esm-env'; | ||
import { BROWSER, DEV } from 'esm-env'; | ||
import { invalidateAll } from './navigation.js'; | ||
import { app, applyAction } from '../client/client.js'; | ||
import { app as client_app, applyAction } from '../client/client.js'; | ||
import { app as server_app } from '../server/app.js'; | ||
export { applyAction }; | ||
const decoders = BROWSER ? client_app.decoders : server_app?.decoders; | ||
/** | ||
@@ -34,3 +37,3 @@ * Use this function to deserialize the response from a form submission. | ||
if (parsed.data) { | ||
parsed.data = devalue.parse(parsed.data, app.decoders); | ||
parsed.data = devalue.parse(parsed.data, decoders); | ||
} | ||
@@ -37,0 +40,0 @@ |
@@ -8,2 +8,3 @@ import { respond } from './respond.js'; | ||
import { set_read_implementation, set_manifest } from '__sveltekit/server'; | ||
import { set_app } from './app.js'; | ||
@@ -87,2 +88,8 @@ /** @type {ProxyHandler<{ type: 'public' | 'private' }>} */ | ||
set_app({ | ||
decoders: module.transport | ||
? Object.fromEntries(Object.entries(module.transport).map(([k, v]) => [k, v.decode])) | ||
: {} | ||
}); | ||
if (module.init) { | ||
@@ -102,2 +109,6 @@ await module.init(); | ||
}; | ||
set_app({ | ||
decoders: {} | ||
}); | ||
} else { | ||
@@ -104,0 +115,0 @@ throw error; |
@@ -54,2 +54,4 @@ import { DEV } from 'esm-env'; | ||
let warned_on_devtools_json_request = false; | ||
/** | ||
@@ -577,2 +579,17 @@ * @param {Request} request | ||
if (state.depth === 0) { | ||
// In local development, Chrome requests this file for its 'automatic workspace folders' feature, | ||
// causing console spam. If users want to serve this file they can install | ||
// https://github.com/ChromeDevTools/vite-plugin-devtools-json | ||
if (DEV && event.url.pathname === '/.well-known/appspecific/com.chrome.devtools.json') { | ||
if (!warned_on_devtools_json_request) { | ||
console.warn( | ||
`\nGoogle Chrome is requesting ${event.url.pathname} to automatically configure devtools project settings. To serve this file, add this plugin to your Vite config:\n\nhttps://github.com/ChromeDevTools/vite-plugin-devtools-json\n` | ||
); | ||
warned_on_devtools_json_request = true; | ||
} | ||
return new Response(undefined, { status: 404 }); | ||
} | ||
return await respond_with_error({ | ||
@@ -579,0 +596,0 @@ event, |
// generated during release, do not modify | ||
/** @type {string} */ | ||
export const VERSION = '2.21.2'; | ||
export const VERSION = '2.21.3'; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
835332
0.52%139
0.72%23899
0.43%16
6.67%+ Added