Skip to content

Commit

Permalink
feat: improve typegen, automatic default include
Browse files Browse the repository at this point in the history
  • Loading branch information
natemoo-re committed Dec 22, 2023
1 parent cd9e502 commit 78fa55a
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 55 deletions.
17 changes: 0 additions & 17 deletions demo/astro.config.mjs

This file was deleted.

9 changes: 9 additions & 0 deletions demo/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'astro/config';
import icon from 'astro-icon';

// https://astro.build/config
export default defineConfig({
integrations: [
icon()
]
});
5 changes: 3 additions & 2 deletions packages/core/components/Icon.astro
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
// @ts-ignore virtual module
import icons, { config } from "virtual:astro-icon";
// @ts-ignore generated by typegen
import type { Icon } from "virtual:astro-icon";
import { getIconData, iconToSVG } from "@iconify/utils";
import type { HTMLAttributes } from "astro/types";
import { cache } from "./cache.js";
// @ts-expect-error - Types are generated by the integration
import type { Icon } from "astro-icon";
interface Props extends HTMLAttributes<"svg"> {
name: Icon;
Expand Down
13 changes: 7 additions & 6 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
],
"scripts": {
"build": "tsc",
"start": "tsc --watch"
"dev": "tsc --watch",
"start": "pnpm run dev"
},
"author": {
"name": "Nate Moore",
Expand Down Expand Up @@ -48,13 +49,13 @@
"dependencies": {
"@iconify/tools": "^3.0.1",
"@iconify/types": "^2.0.0",
"@iconify/utils": "^2.1.5"
"@iconify/utils": "^2.1.5",
"@skarab/detect-package-manager": "^1.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.0.6",
"@types/node": "^16.11.7",
"astro": "^2.5.0",
"@types/node": "^18.18.0",
"astro": "^4.0.0",
"typescript": "^5.0.4",
"vite": "^4.3.8"
"vite": "^5.0.0"
}
}
13 changes: 9 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import type { AstroIntegration } from "astro";
import type { IntegrationOptions } from "../typings/integration";
import { createPlugin } from "./vite-plugin-astro-icon.js";
import type { AstroIntegration } from 'astro';

export default function createIntegration(
opts: IntegrationOptions = {}
): AstroIntegration {
return {
name: "astro-icon",
hooks: {
async "astro:config:setup"({ updateConfig, command, config }) {
"astro:config:setup"({ updateConfig, config, logger }) {
const external = config.output === 'static' ? ['@iconify-json/*'] : undefined;
const { root, output } = config;
updateConfig({
vite: {
plugins: [await createPlugin(opts, { root: config.root })],
plugins: [createPlugin(opts, { root, output, logger })],
ssr: {
external
}
},
});
},
},
};
}
}
56 changes: 52 additions & 4 deletions packages/core/src/loaders/loadIconifyCollections.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
import { getIcons } from "@iconify/utils";
import { loadCollectionFromFS } from "@iconify/utils/lib/loader/fs";
import type {
AstroIconCollectionMap,
IconCollection,
IntegrationOptions,
} from "../../typings/integration";
import type { AutoInstall } from "../../typings/iconify";

import { readFile } from "node:fs/promises";
import { detectAgent } from '@skarab/detect-package-manager';
import { getIcons } from "@iconify/utils";
import { loadCollectionFromFS } from "@iconify/utils/lib/loader/fs";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { exec } from "node:child_process";

const execa = promisify(exec);

interface LoadOptions {
root: URL;
include?: IntegrationOptions["include"];
}

export default async function loadIconifyCollections(
include: IntegrationOptions["include"] = {}
{ root, include = {} }: LoadOptions
): Promise<AstroIconCollectionMap> {
const installedCollections = await detectInstalledCollections(root);
// If icons are installed locally but not explicitly included, include the whole pack
for (let name of installedCollections) {
if (include[name] !== undefined) continue;
include[name] = ['*'];
}
const possibleCollections = await Promise.all(
Object.keys(include).map((collectionName) =>
installedCollections.map((collectionName) =>
loadCollection(collectionName).then(
(possibleCollection) => [collectionName, possibleCollection] as const
)
Expand Down Expand Up @@ -66,3 +85,32 @@ export async function loadCollection(

return loadCollectionFromFS(name, autoInstall);
}

async function detectInstalledCollections(root: URL) {
try {
const agent = await detectAgent(fileURLToPath(root));
let packages: string[] = []
if (!agent) {
const text = await readFile(new URL('./package.json', root), { encoding: 'utf8' });
const { dependencies = {}, devDependencies = {} } = JSON.parse(text);
packages.push(...Object.keys(dependencies));
packages.push(...Object.keys(devDependencies));
} else {
const { stdout: text } = await execa(`${agent.name} list --json`);
const data = JSON.parse(text);
if (Array.isArray(data)) {
for (const { dependencies = {}, devDependencies = {} } of data) {
packages.push(...Object.keys(dependencies));
packages.push(...Object.keys(devDependencies));
}
} else {
const { dependencies = {}, devDependencies = {} } = data;
packages.push(...Object.keys(dependencies));
packages.push(...Object.keys(devDependencies));
}
}
const collections = packages.filter(name => name.startsWith('@iconify-json/')).map(name => name.replace('@iconify-json/', ''));
return collections;
} catch {}
return []
}
78 changes: 59 additions & 19 deletions packages/core/src/vite-plugin-astro-icon.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import type { AstroConfig } from "astro";
import { mkdir, stat, writeFile } from "node:fs/promises";
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";

export async function createPlugin(
interface PluginContext extends Pick<AstroConfig, 'root' | 'output'> {
logger: AstroIntegrationLogger
}

let collections: AstroIconCollectionMap | undefined;
export function createPlugin(
{ include = {}, iconDir = "src/icons", svgoOptions }: IntegrationOptions,
{ root }: Pick<AstroConfig, "root">
): Promise<Plugin> {
ctx: PluginContext
): Plugin {
const { root } = ctx;
const virtualModuleId = "virtual:astro-icon";
const resolvedVirtualModuleId = "\0" + virtualModuleId;

// Load provided Iconify collections
const collections = await loadIconifyCollections(include);
await generateIconTypeDefinitions(Object.values(collections), root);

return {
name: "astro-icon",
resolveId(id) {
Expand All @@ -28,6 +32,10 @@ export async function createPlugin(
},
async load(id) {
if (id === resolvedVirtualModuleId) {
if (!collections) {
collections = await loadIconifyCollections({ root, include });
logCollections(collections, ctx);
}
try {
// Attempt to create local collection
const local = await loadLocalCollection(iconDir, svgoOptions);
Expand All @@ -46,16 +54,34 @@ export async function createPlugin(
};
}

function logCollections(collections: AstroIconCollectionMap, { logger, output }: PluginContext) {
if (Object.keys(collections).length === 0) {
logger.warn('No icons detected!');
return;
}
const names: string[] = Object.keys(collections);
logger.info(`Loaded icons from ${names.join(', ')}`)
}

async function generateIconTypeDefinitions(
collections: IconCollection[],
rootDir: URL,
defaultPack = "local"
): Promise<void> {
await ensureDir(new URL("./.astro", rootDir));
const typeFile = new URL("./.astro/icon.d.ts", rootDir);
const oldHash = await tryGetHash(typeFile)
const currentHash = collectionsHash(collections);
if (currentHash === oldHash) {
return;
}
await ensureDir(new URL("../", typeFile));
await writeFile(
new URL("./.astro/icon.d.ts", rootDir),
`declare module 'astro-icon' {
export type Icon = ${
typeFile,
`// Automatically generated by astro-icon
// ${currentHash}
declare module 'virtual:astro-icon' {
\texport type Icon = ${
collections.length > 0
? collections
.map((collection) =>
Expand All @@ -71,15 +97,29 @@ async function generateIconTypeDefinitions(
.flat(1)
.join("")
: "never"
};\n
}`
};
}`
);
}

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');
}

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 {}
}

async function ensureDir(path: URL): Promise<void> {
try {
await stat(path);
} catch (_) {
await mkdir(path);
}
await mkdir(path, { recursive: true })
} catch {}
}
12 changes: 9 additions & 3 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src", "typings"],
"include": [
"src",
"typings"
],
"compilerOptions": {
"allowJs": true,
"module": "ES2022",
Expand All @@ -21,7 +24,10 @@
"synchronousWatchDirectory": true,
// Finally, two additional settings for reducing the amount of possible
// files to track work from these directories
"excludeDirectories": ["**/node_modules", "_build"],
"excludeFiles": ["build/fileWhichChangesOften.ts"]
"excludeDirectories": [
"**/node_modules",
"_build"
],
"excludeFiles": []
}
}

0 comments on commit 78fa55a

Please sign in to comment.