-
-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
531db17
commit 4690189
Showing
1 changed file
with
116 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,146 @@ | ||
import type { AstroConfig, AstroIntegrationLogger } from "astro"; | ||
import { mkdir, readFile, writeFile } from "node:fs/promises"; | ||
import type { Plugin } from "vite"; | ||
import type {AstroConfig, AstroIntegrationLogger} from 'astro'; | ||
import {mkdir, readFile, writeFile} from 'node:fs/promises'; | ||
import type {Plugin} from 'vite'; | ||
import type { | ||
AstroIconCollectionMap, | ||
IconCollection, | ||
IntegrationOptions, | ||
} from "../typings/integration"; | ||
import loadLocalCollection from "./loaders/loadLocalCollection.js"; | ||
import loadIconifyCollections from "./loaders/loadIconifyCollections.js"; | ||
import { createHash } from "node:crypto"; | ||
AstroIconCollectionMap, | ||
IconCollection, | ||
IntegrationOptions, | ||
} from '../typings/integration'; | ||
import loadLocalCollection from './loaders/loadLocalCollection.js'; | ||
import loadIconifyCollections from './loaders/loadIconifyCollections.js'; | ||
import {createHash} from 'node:crypto'; | ||
|
||
interface PluginContext extends Pick<AstroConfig, "root" | "output"> { | ||
logger: AstroIntegrationLogger; | ||
interface PluginContext extends Pick<AstroConfig, 'root' | 'output'> { | ||
logger: AstroIntegrationLogger; | ||
} | ||
|
||
export function createPlugin( | ||
{ include = {}, iconDir = "src/icons", svgoOptions }: IntegrationOptions, | ||
ctx: PluginContext, | ||
{include = {}, iconDir = 'src/icons', svgoOptions}: IntegrationOptions, | ||
ctx: PluginContext | ||
): Plugin { | ||
let collections: AstroIconCollectionMap | undefined; | ||
const { root } = ctx; | ||
const virtualModuleId = "virtual:astro-icon"; | ||
const resolvedVirtualModuleId = "\0" + virtualModuleId; | ||
let collections: AstroIconCollectionMap | undefined; | ||
const {root} = ctx; | ||
const virtualModuleId = 'virtual:astro-icon'; | ||
const resolvedVirtualModuleId = '\0' + virtualModuleId; | ||
|
||
return { | ||
name: "astro-icon", | ||
resolveId(id) { | ||
if (id === virtualModuleId) { | ||
return resolvedVirtualModuleId; | ||
} | ||
}, | ||
async load(id) { | ||
if (id === resolvedVirtualModuleId) { | ||
if (!collections) { | ||
collections = await loadIconifyCollections({ root, include }); | ||
} | ||
try { | ||
// Attempt to create local collection | ||
const local = await loadLocalCollection(iconDir, svgoOptions); | ||
collections["local"] = local; | ||
} catch (ex) { | ||
// Failed to load the local collection | ||
} | ||
logCollections(collections, { ...ctx, iconDir }); | ||
await generateIconTypeDefinitions(Object.values(collections), root); | ||
return { | ||
name: 'astro-icon', | ||
resolveId(id) { | ||
if (id === virtualModuleId) { | ||
return resolvedVirtualModuleId; | ||
} | ||
}, | ||
|
||
return `export default ${JSON.stringify( | ||
collections, | ||
)};\nexport const config = ${JSON.stringify({ include })}`; | ||
} | ||
}, | ||
}; | ||
async load(id) { | ||
if (id === resolvedVirtualModuleId) { | ||
try { | ||
if (!collections) { | ||
collections = await loadIconifyCollections({root, include}); | ||
} | ||
const local = await loadLocalCollection(iconDir, svgoOptions); | ||
collections['local'] = local; | ||
logCollections(collections, {...ctx, iconDir}); | ||
await generateIconTypeDefinitions(Object.values(collections), root); | ||
} catch (ex) { | ||
// Failed to load the local collection | ||
} | ||
return `export default ${JSON.stringify(collections)};\nexport const config = ${JSON.stringify({include})}`; | ||
} | ||
}, | ||
configureServer({watcher, moduleGraph}) { | ||
watcher.add(`${iconDir}/**/*.svg`); | ||
watcher.on('change', async () => { | ||
console.log(`Local icons changed, reloading`); | ||
try { | ||
if (!collections) { | ||
collections = await loadIconifyCollections({root, include}); | ||
} | ||
const local = await loadLocalCollection(iconDir, svgoOptions); | ||
collections['local'] = local; | ||
logCollections(collections, {...ctx, iconDir}); | ||
await generateIconTypeDefinitions(Object.values(collections), root); | ||
moduleGraph.invalidateAll(); | ||
} catch (ex) { | ||
// Failed to load the local collection | ||
} | ||
return `export default ${JSON.stringify(collections)};\nexport const config = ${JSON.stringify({include})}`; | ||
}); | ||
}, | ||
}; | ||
} | ||
|
||
function logCollections( | ||
collections: AstroIconCollectionMap, | ||
{ logger, iconDir }: PluginContext & { iconDir: string }, | ||
collections: AstroIconCollectionMap, | ||
{logger, iconDir}: PluginContext & {iconDir: string} | ||
) { | ||
if (Object.keys(collections).length === 0) { | ||
logger.warn("No icons detected!"); | ||
return; | ||
} | ||
const names: string[] = Object.keys(collections).filter((v) => v !== "local"); | ||
if (collections["local"]) { | ||
names.unshift(iconDir); | ||
} | ||
logger.info(`Loaded icons from ${names.join(", ")}`); | ||
if (Object.keys(collections).length === 0) { | ||
logger.warn('No icons detected!'); | ||
return; | ||
} | ||
const names: string[] = Object.keys(collections).filter((v) => v !== 'local'); | ||
if (collections['local']) { | ||
names.unshift(iconDir); | ||
} | ||
logger.info(`Loaded icons from ${names.join(', ')}`); | ||
} | ||
|
||
async function generateIconTypeDefinitions( | ||
collections: IconCollection[], | ||
rootDir: URL, | ||
defaultPack = "local", | ||
collections: IconCollection[], | ||
rootDir: URL, | ||
defaultPack = 'local' | ||
): Promise<void> { | ||
const typeFile = new URL("./.astro/icon.d.ts", rootDir); | ||
await ensureDir(new URL("./", typeFile)); | ||
const oldHash = await tryGetHash(typeFile); | ||
const currentHash = collectionsHash(collections); | ||
if (currentHash === oldHash) { | ||
return; | ||
} | ||
await writeFile( | ||
typeFile, | ||
`// Automatically generated by astro-icon | ||
const typeFile = new URL('./.astro/icon.d.ts', rootDir); | ||
await ensureDir(new URL('./', typeFile)); | ||
const oldHash = await tryGetHash(typeFile); | ||
const currentHash = collectionsHash(collections); | ||
if (currentHash === oldHash) { | ||
return; | ||
} | ||
await writeFile( | ||
typeFile, | ||
`// Automatically generated by astro-icon | ||
// ${currentHash} | ||
declare module 'virtual:astro-icon' { | ||
\texport type Icon = ${ | ||
collections.length > 0 | ||
? collections | ||
.map((collection) => | ||
Object.keys(collection.icons).map( | ||
(icon) => | ||
`\n\t\t| "${ | ||
collection.prefix === defaultPack | ||
? "" | ||
: `${collection.prefix}:` | ||
}${icon}"`, | ||
), | ||
) | ||
.flat(1) | ||
.join("") | ||
: "never" | ||
}; | ||
}`, | ||
); | ||
collections.length > 0 | ||
? collections | ||
.map((collection) => | ||
Object.keys(collection.icons).map( | ||
(icon) => | ||
`\n\t\t| "${ | ||
collection.prefix === defaultPack | ||
? '' | ||
: `${collection.prefix}:` | ||
}${icon}"` | ||
) | ||
) | ||
.flat(1) | ||
.join('') | ||
: 'never' | ||
}; | ||
}` | ||
); | ||
} | ||
|
||
function collectionsHash(collections: IconCollection[]): string { | ||
const hash = createHash("sha256"); | ||
for (const collection of collections) { | ||
hash.update(collection.prefix); | ||
hash.update(Object.keys(collection.icons).sort().join(",")); | ||
} | ||
return hash.digest("hex"); | ||
const hash = createHash('sha256'); | ||
for (const collection of collections) { | ||
hash.update(collection.prefix); | ||
hash.update(Object.keys(collection.icons).sort().join(',')); | ||
} | ||
return hash.digest('hex'); | ||
} | ||
|
||
async function tryGetHash(path: URL): Promise<string | void> { | ||
try { | ||
const text = await readFile(path, { encoding: "utf-8" }); | ||
return text.split("\n", 3)[1].replace("// ", ""); | ||
} catch {} | ||
try { | ||
const text = await readFile(path, {encoding: 'utf-8'}); | ||
return text.split('\n', 3)[1].replace('// ', ''); | ||
} catch {} | ||
} | ||
|
||
async function ensureDir(path: URL): Promise<void> { | ||
try { | ||
await mkdir(path, { recursive: true }); | ||
} catch {} | ||
try { | ||
await mkdir(path, {recursive: true}); | ||
} catch {} | ||
} |