Merge branch 'main' into patch-21

This commit is contained in:
Michael Brevard 2024-03-30 16:12:02 +03:00 committed by GitHub
commit 586cfa5a3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 623 additions and 422 deletions

View File

@ -1,37 +1,19 @@
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org)
Please carefully read the contribution docs before creating a pull request
👉 https://nuxt.com/docs/community/contribution
-->
### 🔗 Linked issue
<!-- Please ensure there is an open issue and mention its number as #123 -->
### ❓ Type of change
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
- [ ] 📖 Documentation (updates to the documentation, readme or JSdoc annotations)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [ ] 👌 Enhancement (improving an existing functionality like performance)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
<!-- Please ensure there is an open issue and mention its number. For example, "resolves #123" -->
### 📚 Description
<!-- Describe your changes in detail -->
<!-- Why is this change required? What problem does it solve? -->
<!-- If it resolves an open issue, please link to the issue here. For example "Resolves #1337" -->
<!-- Describe your changes in detail. Why is this change required? What problem does it solve? -->
### 📝 Checklist
<!----------------------------------------------------------------------
Before creating the pull request, please make sure you do the following:
<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- Check that there isn't already a PR that solves the problem the same way. If you find a duplicate, please help us reviewing it.
- Read the contribution docs at https://nuxt.com/docs/community/contribution
- Ensure that PR title follows conventional commits (https://conventionalcommits.org)
- Update the corresponding documentation if needed.
- Include relevant tests that fail without this PR but pass with it.
- [ ] I have linked an issue or discussion.
- [ ] I have added tests (if possible).
- [ ] I have updated the documentation accordingly.
Thank you for contributing to Nuxt!
----------------------------------------------------------------------->

View File

@ -99,7 +99,7 @@ Nuxt uses a custom merging strategy for the `AppConfig` within [the layers](/doc
This strategy is implemented using a [Function Merger](https://github.com/unjs/defu#function-merger), which allows defining a custom merging strategy for every key in `app.config` that has an array as value.
::note
The Function Merger should only be used in the base `app.config` of your application.
The function merger can only be used in the extended layers and not the main `app.config` in project.
::
Here's an example of how you can use:

View File

@ -87,6 +87,8 @@ console.log(layoutCustomProps.title) // I am a custom layout
`<NuxtLayout />` renders incoming content via `<slot />`, which is then wrapped around Vues `<Transition />` component to activate layout transition. For this to work as expected, it is recommended that `<NuxtLayout />` is **not** the root element of the page component.
::code-group
```vue [pages/index.vue]
<template>
<div>
@ -97,13 +99,27 @@ console.log(layoutCustomProps.title) // I am a custom layout
</template>
```
```vue [layouts/custom.vue]
<template>
<div>
<!-- named slot -->
<slot name="header" />
<slot />
</div>
</template>
```
::
:read-more{to="/docs/getting-started/transitions"}
## Layout's Ref
To get the ref of a layout component, access it through `ref.value.layoutRef`.
````vue [app.vue]
::code-group
```vue [app.vue]
<script setup lang="ts">
const layout = ref()
@ -113,8 +129,28 @@ function logFoo () {
</script>
<template>
<NuxtLayout ref="layout" />
<NuxtLayout ref="layout">
default layout
</NuxtLayout>
</template>
````
```
```vue [layouts/default.vue]
<script setup lang="ts">
const foo = () => console.log('foo')
defineExpose({
foo
})
</script>
<template>
<div>
default layout
<slot />
</div>
</template>
```
::
:read-more{to="/docs/guide/directory-structure/layouts"}

View File

@ -14,6 +14,10 @@ When prerendering, you can hint to Nitro to prerender additional paths, even if
`prerenderRoutes` can only be called within the [Nuxt context](/docs/guide/going-further/nuxt-app#the-nuxt-context).
::
::note
`prerenderRoutes` has to be executed during prerendering. If the `prerenderRoutes` is used in dynamic pages/routes which are not prerendered, then it will not be executed.
::
```js
const route = useRoute()

View File

@ -31,9 +31,7 @@ Check [Discussions](https://github.com/nuxt/nuxt/discussions) and [RFCs](https:/
Milestone | Expected date | Notes | Description
-------------|---------------|------------------------------------------------------------------------|-----------------------
SEO & PWA | 2024 | [nuxt/nuxt#18395](https://github.com/nuxt/nuxt/discussions/18395) | Migrating from [nuxt-community/pwa-module](https://github.com/nuxt-community/pwa-module) for built-in SEO utils and service worker support
Scripts | 2024 | [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016) | Easy 3rd party script management.
Assets | 2024 | [nuxt/nuxt#22012](https://github.com/nuxt/nuxt/discussions/22012) | Allow developers and modules to handle loading third-party assets.
A11y | 2024 | [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255) | Accessibility hinting and utilities
Translations | - | [nuxt/translations#4](https://github.com/nuxt/translations/discussions/4) ([request access](https://github.com/nuxt/nuxt/discussions/16054)) | A collaborative project for a stable translation process for Nuxt 3 docs. Currently pending for ideas and documentation tooling support (content v2 with remote sources).
## Core Modules Roadmap
@ -42,7 +40,10 @@ In addition to the Nuxt framework, there are modules that are vital for the ecos
Module | Status | Nuxt Support | Repository | Description
---------------|---------------------|--------------|------------|-------------------
Scripts | April 2024 | 3.x | `nuxt/scripts` to be announced | Easy 3rd party script management. [nuxt/nuxt#22016](https://github.com/nuxt/nuxt/discussions/22016)
A11y | Planned | 3.x | `nuxt/a11y` to be announced | Accessibility hinting and utilities [nuxt/nuxt#23255](https://github.com/nuxt/nuxt/issues/23255)
Auth | Planned | 3.x | `nuxt/auth` to be announced | Nuxt 3 support is planned after session support
Hints | Planned | 3.x | `nuxt/hints` to be announced | Guidance and suggestions for enhancing development practices
## Release Cycle

View File

@ -37,9 +37,9 @@
"@nuxt/schema": "workspace:*",
"@nuxt/vite-builder": "workspace:*",
"@nuxt/webpack-builder": "workspace:*",
"rollup": "^4.13.1",
"rollup": "^4.13.2",
"nuxt": "workspace:*",
"vite": "5.2.6",
"vite": "5.2.7",
"vue": "3.4.21",
"magic-string": "^0.30.8"
},
@ -50,7 +50,7 @@
"@nuxt/webpack-builder": "workspace:*",
"@testing-library/vue": "8.0.3",
"@types/fs-extra": "11.0.4",
"@types/node": "20.11.30",
"@types/node": "20.12.2",
"@types/semver": "7.5.8",
"@vitest/coverage-v8": "1.4.0",
"@vue/test-utils": "2.4.5",
@ -61,7 +61,7 @@
"eslint": "8.57.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsdoc": "48.2.1",
"eslint-plugin-jsdoc": "48.2.2",
"eslint-plugin-no-only-tests": "3.1.0",
"eslint-plugin-unicorn": "51.0.1",
"execa": "8.0.1",

View File

@ -52,7 +52,7 @@
"lodash-es": "4.17.21",
"nitropack": "2.9.5",
"unbuild": "latest",
"vite": "5.2.6",
"vite": "5.2.7",
"vitest": "1.4.0",
"webpack": "5.91.0"
},

View File

@ -60,20 +60,20 @@
},
"dependencies": {
"@nuxt/devalue": "^2.0.2",
"@nuxt/devtools": "^1.1.4",
"@nuxt/devtools": "^1.1.5",
"@nuxt/kit": "workspace:*",
"@nuxt/schema": "workspace:*",
"@nuxt/telemetry": "^2.5.3",
"@nuxt/ui-templates": "^1.3.1",
"@nuxt/vite-builder": "workspace:*",
"@unhead/dom": "^1.9.1",
"@unhead/ssr": "^1.9.1",
"@unhead/vue": "^1.9.1",
"@unhead/dom": "^1.9.3",
"@unhead/ssr": "^1.9.3",
"@unhead/vue": "^1.9.3",
"@vue/shared": "^3.4.21",
"acorn": "8.11.3",
"c12": "^1.10.0",
"chokidar": "^3.6.0",
"cookie-es": "^1.0.0",
"cookie-es": "^1.1.0",
"defu": "^6.1.4",
"destr": "^2.0.3",
"devalue": "^4.3.2",
@ -100,14 +100,14 @@
"radix3": "^1.1.2",
"scule": "^1.3.0",
"std-env": "^3.7.0",
"strip-literal": "^2.0.0",
"strip-literal": "^2.1.0",
"ufo": "^1.5.3",
"ultrahtml": "^1.5.3",
"uncrypto": "^0.1.3",
"unctx": "^2.3.1",
"unenv": "^1.9.0",
"unimport": "^3.7.1",
"unplugin": "^1.10.0",
"unplugin": "^1.10.1",
"unplugin-vue-router": "^0.7.0",
"unstorage": "^1.10.2",
"untyped": "^1.4.2",
@ -122,7 +122,7 @@
"@types/fs-extra": "11.0.4",
"@vitejs/plugin-vue": "5.0.4",
"unbuild": "latest",
"vite": "5.2.6",
"vite": "5.2.7",
"vitest": "1.4.0"
},
"peerDependencies": {

View File

@ -88,30 +88,32 @@ export default defineComponent({
onMounted(() => { mounted.value = true; teleportKey.value++ })
function setPayload (key: string, result: NuxtIslandResponse) {
const toRevive: Partial<NuxtIslandResponse> = {}
if (result.props) { toRevive.props = result.props }
if (result.slots) { toRevive.slots = result.slots }
if (result.components) { toRevive.components = result.components }
nuxtApp.payload.data[key] = {
__nuxt_island: {
key,
...(import.meta.server && import.meta.prerender)
? {}
: { params: { ...props.context, props: props.props ? JSON.stringify(props.props) : undefined } },
result: {
props: result.props,
slots: result.slots,
components: result.components
}
result: toRevive
},
...result
}
}
const payloads: Required<Pick<NuxtIslandResponse, 'slots' | 'components'>> = {
slots: {},
components: {}
}
const payloads: Partial<Pick<NuxtIslandResponse, 'slots' | 'components'>> = {}
if (instance.vnode.el) {
payloads.slots = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots ?? {}
payloads.components = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components ?? {}
const slots = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.slots
if (slots) { payloads.slots = slots }
if (selectiveClient) {
const components = toRaw(nuxtApp.payload.data[`${props.name}_${hashId.value}`])?.components
if (components) { payloads.components = components }
}
}
const ssrHTML = ref<string>('')
@ -137,12 +139,15 @@ export default defineComponent({
}
}
return html.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => {
if (!currentSlots.includes(slotName)) {
return full + (payloads.slots[slotName]?.fallback || '')
}
return full
})
if (payloads.slots) {
return html.replaceAll(SLOT_FALLBACK_RE, (full, slotName) => {
if (!currentSlots.includes(slotName)) {
return full + (payloads.slots?.[slotName]?.fallback || '')
}
return full
})
}
return html
})
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
@ -256,24 +261,26 @@ export default defineComponent({
teleports.push(createVNode(Teleport,
// use different selectors for even and odd teleportKey to force trigger the teleport
{ to: import.meta.client ? `${isKeyOdd ? 'div' : ''}[data-island-uid="${uid.value}"][data-island-slot="${slot}"]` : `uid=${uid.value};slot=${slot}` },
{ default: () => (payloads.slots[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
{ default: () => (payloads.slots?.[slot].props?.length ? payloads.slots[slot].props : [{}]).map((data: any) => slots[slot]?.(data)) })
)
}
}
if (selectiveClient) {
if (import.meta.server) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
const { html, slots } = info
let replaced = html.replaceAll('data-island-uid', `data-island-uid="${uid.value}"`)
for (const slot in slots) {
replaced = replaced.replaceAll(`data-island-slot="${slot}">`, full => full + slots[slot])
if (payloads.components) {
for (const [id, info] of Object.entries(payloads.components)) {
const { html, slots } = info
let replaced = html.replaceAll('data-island-uid', `data-island-uid="${uid.value}"`)
for (const slot in slots) {
replaced = replaced.replaceAll(`data-island-slot="${slot}">`, full => full + slots[slot])
}
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
default: () => [createStaticVNode(replaced, 1)]
}))
}
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
default: () => [createStaticVNode(replaced, 1)]
}))
}
} else if (canLoadClientComponent.value) {
for (const [id, info] of Object.entries(payloads.components ?? {})) {
} else if (canLoadClientComponent.value && payloads.components) {
for (const [id, info] of Object.entries(payloads.components)) {
const { props, slots } = info
const component = components!.get(id)!
// use different selectors for even and odd teleportKey to force trigger the teleport

View File

@ -351,13 +351,11 @@ export function useAsyncData<
if (instance && !instance._nuxtOnBeforeMountCbs) {
instance._nuxtOnBeforeMountCbs = []
const cbs = instance._nuxtOnBeforeMountCbs
if (instance) {
onBeforeMount(() => {
cbs.forEach((cb) => { cb() })
cbs.splice(0, cbs.length)
})
onUnmounted(() => cbs.splice(0, cbs.length))
}
onBeforeMount(() => {
cbs.forEach((cb) => { cb() })
cbs.splice(0, cbs.length)
})
onUnmounted(() => cbs.splice(0, cbs.length))
}
if (fetchOnServer && nuxtApp.isHydrating && (asyncData.error.value || hasCachedData())) {

View File

@ -125,18 +125,16 @@ export const navigateTo = (to: RouteLocationRaw | undefined | null, options?: Na
const toPath = typeof to === 'string' ? to : (withQuery((to as RouteLocationPathRaw).path || '/', to.query || {}) + (to.hash || ''))
// Early open handler
if (options?.open) {
if (import.meta.client) {
const { target = '_blank', windowFeatures = {} } = options.open
if (import.meta.client && options?.open) {
const { target = '_blank', windowFeatures = {} } = options.open
const features = Object.entries(windowFeatures)
.filter(([_, value]) => value !== undefined)
.map(([feature, value]) => `${feature.toLowerCase()}=${value}`)
.join(', ')
const features = Object.entries(windowFeatures)
.filter(([_, value]) => value !== undefined)
.map(([feature, value]) => `${feature.toLowerCase()}=${value}`)
.join(', ')
open(toPath, target, features)
return Promise.resolve()
}
open(toPath, target, features)
return Promise.resolve()
}
const isExternal = options?.external || hasProtocol(toPath, { acceptRelative: true })

View File

@ -338,7 +338,7 @@ export function createNuxtApp (options: CreateOptions) {
}
// Expose runtime config
const runtimeConfig = import.meta.server ? options.ssrContext!.runtimeConfig : reactive(nuxtApp.payload.config!)
const runtimeConfig = import.meta.server ? options.ssrContext!.runtimeConfig : nuxtApp.payload.config!
nuxtApp.provide('config', runtimeConfig)
return nuxtApp

View File

@ -37,7 +37,11 @@ export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.hook('dev:ssr-logs', (logs) => {
for (const log of logs) {
// deduplicate so we don't print out things that are logged on client
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
try {
if (!hydrationLogs.size || !hydrationLogs.has(JSON.stringify(log.args))) {
logger.log(normalizeServerLog({ ...log }))
}
} catch {
logger.log(normalizeServerLog({ ...log }))
}
}
@ -50,7 +54,8 @@ export default defineNuxtPlugin((nuxtApp) => {
// pass SSR logs after hydration
nuxtApp.hooks.hook('app:suspense:resolve', async () => {
if (typeof window !== 'undefined') {
const logs = parse(document.getElementById('__NUXT_LOGS__')?.textContent || '[]', nuxtApp._payloadRevivers) as LogObject[]
const content = document.getElementById('__NUXT_LOGS__')?.textContent
const logs = content ? parse(content, nuxtApp._payloadRevivers) as LogObject[] : []
await nuxtApp.hooks.callHook('dev:ssr-logs', logs)
}
})

View File

@ -109,12 +109,22 @@ export const componentsTypeTemplate = {
: c.filePath.replace(/(?<=\w)\.(?!vue)\w+$/g, ''), { wrapper: false })}['${c.export}']`
])
return `// Generated by components discovery
return `
interface _GlobalComponents {
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
}
declare module '@vue/runtime-core' {
export interface GlobalComponents extends _GlobalComponents { }
}
declare module '@vue/runtime-dom' {
export interface GlobalComponents extends _GlobalComponents { }
}
declare module 'vue' {
export interface GlobalComponents {
${componentTypes.map(([pascalName, type]) => ` '${pascalName}': ${type}`).join('\n')}
${componentTypes.map(([pascalName, type]) => ` 'Lazy${pascalName}': ${type}`).join('\n')}
}
export interface GlobalComponents extends _GlobalComponents { }
}
${componentTypes.map(([pascalName, type]) => `export const ${pascalName}: ${type}`).join('\n')}

View File

@ -649,7 +649,7 @@ const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
const SSR_CLIENT_SLOT_MARKER = /^island-slot=(?:[^;]*);(.*)$/
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
if (!ssrContext.islandContext) { return {} }
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.slots).length) { return undefined }
const response: NuxtIslandResponse['slots'] = {}
for (const slot in ssrContext.islandContext.slots) {
response[slot] = {
@ -661,7 +661,7 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
}
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
if (!ssrContext.islandContext) { return {} }
if (!ssrContext.islandContext || !Object.keys(ssrContext.islandContext.components).length) { return undefined }
const response: NuxtIslandResponse['components'] = {}
for (const clientUid in ssrContext.islandContext.components) {

View File

@ -38,7 +38,7 @@
"@types/file-loader": "5.0.4",
"@types/pug": "2.0.10",
"@types/sass-loader": "8.0.8",
"@unhead/schema": "1.9.1",
"@unhead/schema": "1.9.3",
"@vitejs/plugin-vue": "5.0.4",
"@vitejs/plugin-vue-jsx": "3.1.0",
"@vue/compiler-core": "3.4.21",
@ -52,13 +52,13 @@
"unbuild": "latest",
"unctx": "2.3.1",
"unenv": "1.9.0",
"vite": "5.2.6",
"vite": "5.2.7",
"vue": "3.4.21",
"vue-bundle-renderer": "2.0.0",
"vue-loader": "17.4.2",
"vue-router": "4.3.0",
"webpack": "5.91.0",
"webpack-dev-middleware": "7.1.1"
"webpack-dev-middleware": "7.2.0"
},
"dependencies": {
"@nuxt/ui-templates": "^1.3.1",

View File

@ -32,7 +32,22 @@ export default defineUntypedSchema({
*/
hoist: {
$resolve: (val) => {
const defaults = ['nitropack', 'defu', 'h3', '@unhead/vue', 'vue', 'vue-router', '@nuxt/schema', 'nuxt', 'consola', 'ofetch']
const defaults = [
// Nitro auto-imported/augmented dependencies
'nitropack',
'defu',
'h3',
'consola',
'ofetch',
// Key nuxt dependencies
'@unhead/vue',
'vue',
'@vue/runtime-core',
'@vue/runtime-dom',
'vue-router',
'@nuxt/schema',
'nuxt'
]
return val === false ? [] : (Array.isArray(val) ? val.concat(defaults) : defaults)
}
},

View File

@ -58,11 +58,11 @@
"postcss": "^8.4.38",
"rollup-plugin-visualizer": "^5.12.0",
"std-env": "^3.7.0",
"strip-literal": "^2.0.0",
"strip-literal": "^2.1.0",
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.10.0",
"vite": "^5.2.6",
"unplugin": "^1.10.1",
"vite": "^5.2.7",
"vite-node": "^1.4.0",
"vite-plugin-checker": "^0.6.4",
"vue-bundle-renderer": "^2.0.0"

View File

@ -58,13 +58,13 @@
"time-fix-plugin": "^2.0.7",
"ufo": "^1.5.3",
"unenv": "^1.9.0",
"unplugin": "^1.10.0",
"unplugin": "^1.10.1",
"url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.0.0",
"vue-loader": "^17.4.2",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-dev-middleware": "^7.1.1",
"webpack-dev-middleware": "^7.2.0",
"webpack-hot-middleware": "^2.26.1",
"webpack-virtual-modules": "^0.6.1",
"webpackbar": "^6.0.1"

File diff suppressed because it is too large Load Diff

View File

@ -1968,14 +1968,12 @@ describe('component islands', () => {
expect(result).toMatchInlineSnapshot(`
{
"components": {},
"head": {
"link": [],
"style": [],
},
"html": "<pre data-island-uid> Route: /foo
</pre>",
"slots": {},
"state": {},
}
`)
@ -1993,7 +1991,6 @@ describe('component islands', () => {
result.html = result.html.replaceAll(/ (data-island-uid|data-island-component)="([^"]*)"/g, '')
expect(result).toMatchInlineSnapshot(`
{
"components": {},
"head": {
"link": [],
"style": [],

View File

@ -19,7 +19,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
for (const outputDir of ['.output', '.output-inline']) {
it('default client bundle size', async () => {
const clientStats = await analyzeSizes('**/*.js', join(rootDir, outputDir, 'public'))
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"106k"')
expect.soft(roundToKilobytes(clientStats.totalBytes)).toMatchInlineSnapshot('"105k"')
expect(clientStats.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[
"_nuxt/entry.js",
@ -75,7 +75,7 @@ describe.skipIf(process.env.SKIP_BUNDLE_SIZE === 'true' || process.env.ECOSYSTEM
expect.soft(roundToKilobytes(serverStats.totalBytes)).toMatchInlineSnapshot('"526k"')
const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"76.0k"')
expect.soft(roundToKilobytes(modules.totalBytes)).toMatchInlineSnapshot('"75.5k"')
const packages = modules.files
.filter(m => m.endsWith('package.json'))