-
+
diff --git a/packages/devtools/client/pages/modules/assets.vue b/packages/devtools/client/pages/modules/assets.vue
new file mode 100644
index 000000000..d03d71378
--- /dev/null
+++ b/packages/devtools/client/pages/modules/assets.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+ {{ filtered.length }} matched ยท
+ {{ assets?.length }} assets in total
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/devtools/client/pages/modules/overview.vue b/packages/devtools/client/pages/modules/overview.vue
index 5ae215d2f..864c75a77 100644
--- a/packages/devtools/client/pages/modules/overview.vue
+++ b/packages/devtools/client/pages/modules/overview.vue
@@ -9,7 +9,7 @@ definePageMeta({
const client = useClient()
const config = useServerConfig()
-const versions = usePackageVersions()
+const versions = getPackageVersions()
const components = useComponents()
const autoImports = useAutoImports()
const routes = useAllRoutes()
@@ -52,7 +52,7 @@ function goIntro() {
-
+
{{ `v${nuxtVersion.current}` }}
IGNORE_STORAGE_MOUNTS.includes(key.split(':')[0])
@@ -32,22 +35,26 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
const serverPages: NuxtPage[] = []
const iframeTabs: ModuleCustomTab[] = []
const serverHooks: Record = setupHooksDebug(nuxt.hooks)
- let storage: Storage | undefined
const storageMounts: StorageMounts = {}
+
+ let storage: Storage | undefined
let unimport: Unimport | undefined
let app: NuxtApp | undefined
let checkForUpdatePromise: Promise | undefined
- let versions: UpdateInfo[] = usePackageVersions()
+ let versions: UpdateInfo[] = getPackageVersions()
const customTabs: ModuleCustomTab[] = []
- if (options.customTabs?.length)
- customTabs.push(...options.customTabs)
-
const serverFunctions = {} as ServerFunctions
const clients = new Set()
const birpc = createBirpcGroup(serverFunctions, [])
+ const _imageMetaCache = new Map()
+
+ // Add static custom tabs from the config
+ if (options.customTabs?.length)
+ customTabs.push(...options.customTabs)
+
function refresh(event: keyof ServerFunctions) {
birpc.broadcast.refresh.asEvent(event)
}
@@ -141,10 +148,10 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
getServerHooks() {
return Object.values(serverHooks)
},
- usePackageVersions() {
+ getPackageVersions() {
checkForUpdatePromise = checkForUpdatePromise || checkForUpdates().then((v) => {
versions = v
- refresh('usePackageVersions')
+ refresh('getPackageVersions')
})
return versions
},
@@ -164,16 +171,16 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
suffix = match[2]
}
- // search for existing file
- const file = [
+ // search for existing path
+ const path = [
input,
`${input}.js`,
`${input}.mjs`,
`${input}.ts`,
].find(i => existsSync(i))
- if (file) {
+ if (path) {
// @ts-expect-error missin types
- await import('launch-editor').then(r => (r.default || r)(file + suffix))
+ await import('launch-editor').then(r => (r.default || r)(path + suffix))
}
else {
console.error('File not found:', input)
@@ -200,7 +207,65 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
nuxt.callHook('devtools:customTabs:refresh')
return true
},
+ async getStaticAssets() {
+ const dir = resolve(nuxt.options.srcDir, nuxt.options.dir.public)
+ const baseURL = nuxt.options.app.baseURL
+ const files = await fg(['**/*'], {
+ cwd: dir,
+ onlyFiles: true,
+ })
+ function guessType(path: string): AssetType {
+ if (/\.(png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?)$/i.test(path))
+ return 'image'
+ if (/\.(mp4|webm|ogv|mov|avi|flv|wmv|mpg|mpeg|mkv|3gp|3g2|ts|mts|m2ts|vob|ogm|ogx|rm|rmvb|asf|amv|divx|m4v|svi|viv|f4v|f4p|f4a|f4b)$/i.test(path))
+ return 'video'
+ if (/\.(mp3|wav|ogg|flac|aac|wma|alac|ape|ac3|dts|tta|opus|amr|aiff|au|mid|midi|ra|rm|wv|weba|dss|spx|vox|tak|dsf|dff|dsd|cda)$/i.test(path))
+ return 'audio'
+ if (/\.(woff2?|eot|ttf|otf|ttc|pfa|pfb|pfm|afm)/i.test(path))
+ return 'font'
+ if (/\.(json[5c]?|te?xt|[mc]?[jt]sx?|md[cx]?|markdown)/i.test(path))
+ return 'text'
+ return 'other'
+ }
+
+ return await Promise.all(files.map(async (path) => {
+ const filePath = resolve(dir, path)
+ const stat = await fs.lstat(filePath)
+ return {
+ path,
+ publicPath: join(baseURL, path),
+ filePath,
+ type: guessType(path),
+ size: stat.size,
+ mtime: stat.mtimeMs,
+ }
+ }))
+ },
+ async getImageMeta(filepath: string) {
+ if (_imageMetaCache.has(filepath))
+ return _imageMetaCache.get(filepath)
+ try {
+ const meta = imageMeta(await fs.readFile(filepath)) || undefined
+ _imageMetaCache.set(filepath, meta)
+ return meta
+ }
+ catch (e) {
+ _imageMetaCache.set(filepath, undefined)
+ console.error(e)
+ return undefined
+ }
+ },
+ async getTextAssetContent(filepath: string, limit = 300) {
+ try {
+ const content = await fs.readFile(filepath, 'utf-8')
+ return content.slice(0, limit)
+ }
+ catch (e) {
+ console.error(e)
+ return undefined
+ }
+ },
} satisfies ServerFunctions)
// Nuxt Hooks to collect data
@@ -226,7 +291,7 @@ export function setupRPC(nuxt: Nuxt, options: ModuleOptions) {
page.children?.forEach(searchChildren)
}
v.forEach(searchChildren)
- serverPages.push(...Array.from(pagesSet).sort((a, b) => a.file.localeCompare(b.file)))
+ serverPages.push(...Array.from(pagesSet).sort((a, b) => a.path.localeCompare(b.path)))
refresh('getServerPages')
})
diff --git a/packages/devtools/src/types/client-api.ts b/packages/devtools/src/types/client-api.ts
index 33290d208..2bc562905 100644
--- a/packages/devtools/src/types/client-api.ts
+++ b/packages/devtools/src/types/client-api.ts
@@ -50,7 +50,7 @@ export interface NuxtDevtoolsHostClient {
export interface NuxtDevtoolsClient {
rpc: BirpcReturn
- renderCodeHighlight: (code: string, lang: string, theme?: string) => string
+ renderCodeHighlight: (code: string, lang: string, lines?: boolean, theme?: string) => string
renderMarkdown: (markdown: string) => string
colorMode: string
}
diff --git a/packages/devtools/src/types/integrations.ts b/packages/devtools/src/types/integrations.ts
index 75fcedfb1..ac4f48973 100644
--- a/packages/devtools/src/types/integrations.ts
+++ b/packages/devtools/src/types/integrations.ts
@@ -1,6 +1,9 @@
import type { Import, UnimportMeta } from 'unimport'
import type { VueInspectorClient } from 'vite-plugin-vue-inspector'
import type { RouteRecordNormalized } from 'vue-router'
+import type { imageMeta } from 'image-meta'
+
+export type ImageMeta = ReturnType extends infer T | void ? T : never
export interface UpdateInfo {
name: string
@@ -79,3 +82,14 @@ export interface HookInfo {
}
export type VueInspectorData = VueInspectorClient['linkParams'] & VueInspectorClient['position']
+
+export type AssetType = 'image' | 'font' | 'video' | 'audio' | 'text' | 'other'
+
+export interface AssetInfo {
+ path: string
+ type: AssetType
+ publicPath: string
+ filePath: string
+ size: number
+ mtime: number
+}
diff --git a/packages/devtools/src/types/rpc.ts b/packages/devtools/src/types/rpc.ts
index 2cc03e3f6..b379c1c1c 100644
--- a/packages/devtools/src/types/rpc.ts
+++ b/packages/devtools/src/types/rpc.ts
@@ -4,15 +4,11 @@ import type { StorageValue } from 'unstorage'
import type { Component } from 'vue'
import type { WizardActions, WizardArgs } from '../wizard'
import type { ModuleCustomTab } from './custom-tabs'
-import type { AutoImportsWithMetadata, HookInfo, UpdateInfo } from './integrations'
+import type { AssetInfo, AutoImportsWithMetadata, HookInfo, ImageMeta, UpdateInfo } from './integrations'
import type { ComponentRelationship } from './module'
export interface ServerFunctions {
- getStorageMounts(): Promise
- getStorageKeys(base?: string): Promise
- getStorageItem(key: string): Promise
- setStorageItem(key: string, value: StorageValue): Promise
- removeStorageItem(key: string): Promise
+ // Static RPCs (can be provide on production build in the future)
getServerConfig(): NuxtOptions
getComponents(): Component[]
getComponentsRelationships(): Promise
@@ -21,7 +17,21 @@ export interface ServerFunctions {
getCustomTabs(): ModuleCustomTab[]
getServerHooks(): HookInfo[]
getServerLayouts(): NuxtLayout[]
- usePackageVersions(): UpdateInfo[]
+ getStaticAssets(): Promise
+ getPackageVersions(): UpdateInfo[]
+
+ // Storage
+ getStorageMounts(): Promise
+ getStorageKeys(base?: string): Promise
+ getStorageItem(key: string): Promise
+ setStorageItem(key: string, value: StorageValue): Promise
+ removeStorageItem(key: string): Promise
+
+ // Queries
+ getImageMeta(filepath: string): Promise
+ getTextAssetContent(filepath: string, limit?: number): Promise
+
+ // Actions
customTabAction(name: string, action: number): Promise
runWizard(name: T, ...args: WizardArgs): Promise
openInEditor(filepath: string): void
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b54cf2c95..0cbe9ce93 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -87,6 +87,7 @@ importers:
consola: ^2.15.3
esno: ^0.16.3
execa: ^7.0.0
+ fast-glob: ^3.2.12
flatted: ^3.2.7
floating-vue: 2.0.0-beta.20
fuse.js: ^6.6.2
@@ -94,6 +95,7 @@ importers:
global-dirs: ^3.0.1
h3: ^1.5.0
hookable: ^5.4.2
+ image-meta: ^0.1.1
is-installed-globally: ^0.4.0
json-editor-vue: ^0.10.5
launch-editor: ^2.6.0
@@ -101,6 +103,7 @@ importers:
nuxt: ^3.2.3
nuxt-vitest: ^0.6.6
ofetch: ^1.0.1
+ ohash: ^1.0.0
pacote: ^15.1.1
pathe: ^1.1.0
picocolors: ^1.0.0
@@ -131,8 +134,10 @@ importers:
birpc: 0.2.5
consola: 2.15.3
execa: 7.0.0
+ fast-glob: 3.2.12
h3: 1.5.0
hookable: 5.4.2
+ image-meta: 0.1.1
is-installed-globally: 0.4.0
launch-editor: 2.6.0
pacote: 15.1.1
@@ -168,6 +173,7 @@ importers:
nuxt: 3.2.3_ebmhceqxtb6o6vbzh6kcrsm5gi
nuxt-vitest: 0.6.6_yvegwsl5vqegbbjcry3pb53664
ofetch: 1.0.1
+ ohash: 1.0.0
picocolors: 1.0.0
semver: 7.3.8
shiki: 0.14.1
@@ -6367,6 +6373,11 @@ packages:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'}
+ /image-meta/0.1.1:
+ resolution: {integrity: sha512-+oXiHwOEPr1IE5zY0tcBLED/CYcre15J4nwL50x3o0jxWqEkyjrusiKP3YSU+tr9fvJp33ZcP5Gpj2295g3aEw==}
+ engines: {node: '>=10.18.0'}
+ dev: false
+
/import-fresh/3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}