Skip to content

Commit fe2c93f

Browse files
authored
fix: discover collections installed by a layer (#519)
1 parent 00fa3ab commit fe2c93f

4 files changed

Lines changed: 61 additions & 13 deletions

File tree

src/bundle-server.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { relative } from 'node:path'
12
import { addTemplate } from '@nuxt/kit'
3+
import { resolveModule } from 'local-pkg'
24
import type { NuxtIconRuntimeOptions } from './types'
35
import { getResolvePaths } from './collections'
4-
import { getCollectionPath } from './core/collections'
6+
import { getCollectionPath, resolveCollectionFile } from './core/collections'
57
import type { NuxtIconModuleContext } from './context'
68

79
export function registerServerBundle(
@@ -49,13 +51,26 @@ export function registerServerBundle(
4951
return ` '${collection}': createRemoteCollection(${JSON.stringify(getRemoteEndpoint(collection))}),`
5052
}
5153

52-
const path = getCollectionPath(collection, getResolvePaths(nuxt))
54+
const resolvePaths = getResolvePaths(nuxt)
55+
const path = getCollectionPath(collection, resolvePaths)
5356

54-
// When in dev mode, we avoid bundling the icons to improve performance
55-
// Get rid of the require() when ESM JSON modules are widely supported
56-
return isBundling
57-
? ` '${collection}': () => import('${path}', { with: { type: 'json' } }).then(m => m.default),`
58-
: ` '${collection}': () => require('${path}'),`
57+
if (!isBundling) {
58+
// When in dev mode, we avoid bundling the icons to improve performance
59+
// Get rid of the require() when ESM JSON modules are widely supported
60+
return ` '${collection}': () => require('${path}'),`
61+
}
62+
63+
// A collection owned by a layer does not resolve from the app, so the bare specifier above
64+
// reaches the build unresolved and throws at runtime. Import the resolved file instead
65+
const file = resolveModule(path, { paths: [nuxt.options.rootDir] })
66+
? undefined
67+
: resolveCollectionFile(collection, resolvePaths)
68+
if (file) {
69+
const relPath = relative(nuxt.options.buildDir, file).replaceAll('\\', '/')
70+
return ` '${collection}': () => import('${relPath.startsWith('.') ? relPath : `./${relPath}`}').then(m => m.default),`
71+
}
72+
73+
return ` '${collection}': () => import('${path}', { with: { type: 'json' } }).then(m => m.default),`
5974
}
6075
else {
6176
const { prefix } = collection

src/collections.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
import { getLayerDirectories } from '@nuxt/kit'
12
import type { Nuxt } from '@nuxt/schema'
23

34
export function getResolvePaths(nuxt: Nuxt): string[] {
4-
return Array.from(new Set(
5-
[nuxt.options.rootDir, nuxt.options.workspaceDir].filter(Boolean),
6-
))
5+
const layerDirs = getLayerDirectories(nuxt).map(
6+
dir => dir.root,
7+
)
8+
9+
return Array.from(
10+
new Set(
11+
[nuxt.options.rootDir, nuxt.options.workspaceDir, ...layerDirs].filter(
12+
Boolean,
13+
),
14+
),
15+
)
716
}

src/core/collections.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { consola } from 'consola'
44
import { glob } from 'tinyglobby'
55
import type { IconifyIcon, IconifyJSON } from '@iconify/types'
66
import { parseSVGContent, convertParsedSVG } from '@iconify/utils/lib/svg/parse'
7-
import { isPackageExists } from 'local-pkg'
7+
import { isPackageExists, resolveModule } from 'local-pkg'
88
import { collectionNames } from '../collection-names'
99
import type { CustomCollection, ServerBundleOptions, RemoteCollection } from './types'
1010

@@ -33,6 +33,14 @@ export function getCollectionPath(collection: string, resolvePaths: string[]) {
3333
: `@iconify-json/${collection}/icons.json`
3434
}
3535

36+
/**
37+
* The file a collection's specifier points at, searched across every resolve path.
38+
* Returns `undefined` when the collection is not installed.
39+
*/
40+
export function resolveCollectionFile(collection: string, resolvePaths: string[]): string | undefined {
41+
return resolveModule(getCollectionPath(collection, resolvePaths), { paths: resolvePaths })
42+
}
43+
3644
// https://github.com/iconify/iconify/blob/2274c033b49c01a50dc89b490b89d803d19d95dc/packages/utils/src/icon/name.ts#L15-L18
3745
const validIconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
3846

test/client-bundle.test.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@ function installCollection(dir: string, prefix: string, icon: string) {
2727
}))
2828
}
2929

30-
function createContext(rootDir: string, workspaceDir: string, icons: string[], hookIcons: string[] = []) {
30+
function createContext(rootDir: string, workspaceDir: string, icons: string[], hookIcons: string[] = [], layerDirs: string[] = []) {
3131
const nuxt = {
32-
options: { rootDir, workspaceDir },
32+
options: {
33+
rootDir,
34+
workspaceDir,
35+
_layers: [{ cwd: rootDir, config: { rootDir } }, ...layerDirs.map(dir => ({ cwd: dir, config: { rootDir: dir } }))],
36+
},
3337
// Mimic a module contributing icons through the `icon:clientBundleIcons` hook
3438
callHook: async (_name: string, set: Set<string>) => {
3539
for (const icon of hookIcons)
@@ -48,11 +52,13 @@ let root: string
4852
const appDir = () => join(root, 'app')
4953
const wsDir = () => join(root, 'ws')
5054
const emptyDir = () => join(root, 'empty')
55+
const layerDir = () => join(root, 'layer')
5156

5257
beforeAll(() => {
5358
root = mkdtempSync(join(tmpdir(), 'nuxt-icon-client-bundle-'))
5459
installCollection(appDir(), 'nuxt-icon-test', 'foo')
5560
installCollection(wsDir(), 'nuxt-icon-test-ws', 'bar')
61+
installCollection(layerDir(), 'nuxt-icon-test-layer', 'baz')
5662
mkdirSync(emptyDir(), { recursive: true })
5763
})
5864

@@ -80,6 +86,16 @@ it('falls back to workspaceDir when the collection is not under rootDir', async
8086
expect(result.collections.find(c => c.prefix === 'nuxt-icon-test-ws')?.icons.bar).toBeTruthy()
8187
})
8288

89+
it('resolves a collection installed by a layer rather than the app', async () => {
90+
// The layer owns the dependency, so it resolves from neither `rootDir` nor `workspaceDir`.
91+
const context = createContext(emptyDir(), emptyDir(), ['nuxt-icon-test-layer:baz'], [], [layerDir()])
92+
const result = await context.loadClientBundleCollections()
93+
94+
expect(result.failed).toEqual([])
95+
expect(result.count).toBe(1)
96+
expect(result.collections.find(c => c.prefix === 'nuxt-icon-test-layer')?.icons.baz).toBeTruthy()
97+
})
98+
8399
it('does not hard-fail when a hook-contributed icon cannot be resolved', async () => {
84100
// A module adds an icon from a collection that is not installed. It must be
85101
// best-effort: omitted from the bundle (so it falls back to runtime loading)

0 commit comments

Comments
 (0)