jackyzha0 / quartz

🌱 a fast, batteries-included static-site generator that transforms Markdown content into fully functional websites
https://quartz.jzhao.xyz
MIT License
6.6k stars 2.44k forks source link

refactor: Expose Quartz type system #1062

Open OCDkirby opened 5 months ago

OCDkirby commented 5 months ago

This meta-issue is to request and track implementation of exposing the Quartz type system in a convenient way for developers. Specifically, plugin developers need access to types for a proper development and testing experience.

Specific goals relevant to community development:

Implementation suggestions:

Related:

jackyzha0 commented 5 months ago

we can re-export types and publish them, seems reasonable

OCDkirby commented 5 months ago

we can re-export types and publish them, seems reasonable

Any implementation guidance? I can bang out some commits on a draft PR that will enable me to start on the reworked plugin CLI if you've got a direction in mind

jackyzha0 commented 5 months ago

if theres a way we can export as @types/quartz or something that would be awesome, im think we can just have a flat file that re-exports everything that might be important is probably ok? and then a github ci workflow to publish it

OCDkirby commented 5 months ago

hosting as an npm package is a great idea. The syntax for their shorthand is @user/package, and just package works if it's unambiguous. @quartz is taken, could we do @quartz-md/types? I'd make that package have the Quartz repo as a URL dependency, then its index re-exports files from this repo. Will also set up a workflow.

EDIT: turns out we can publish to the @types org. See DefinitelyTyped. I don't know if this type of package's code quality meets the standard for that repo because it's just plain hacky, we'd probably need a full refactor to publish there so making our own org is likely the best choice.

OCDkirby commented 5 months ago

Working layout of the exports:

jackyzha0 commented 5 months ago

i believe i still have https://www.npmjs.com/package/@jackyzha0/quartz-lib if we want to use that as well

(and yes, i was referring to definitelytyped haha, I've seen plenty of hacky libraries there so i don't see why not :))

as a side note, i think exporting cli arg types might also be good? idk

OCDkirby commented 5 months ago

Sounds good. I've been making changes in https://github.com/OCDkirby/quartz-api and testing with @MaelImhof 's https://github.com/OCDkirby/quartz-pseudocode/tree/QuartzAPI, let me know how you want to proceed with moving stuff over. Should I clean it up and PR to jackyzha0/quartz-lib on gh?

Edit: now also see https://github.com/OCDkirby/quartz-create-plugin , once hosted on npm at create-plugin we'll be able to do npx @quartz-md/plugin myplugin (or replace with whatever author hosts the package) to get a template going

OCDkirby commented 5 months ago

@jackyzha0 looking at exporting the CLI types, but they're defined in JS as constants. Can I make a PR for some .d.ts files for so I can re-export them from the API, or do you have a different idea in mind?

jackyzha0 commented 5 months ago

ah forgot about that, yeah lets do that another time then, not super urgent

we can keep it there for now but lets move it when its time for productionizing

OCDkirby commented 5 months ago

I just realized, there's an interesting practical effect of the re-export route: A user's Quartz installation will depend on itself as a dependency of a dependency of a dependency if it uses community plugins (quartz depends on the plugin as a url, which depends on the api as an npm package, which depends on quartz as a url)

This just feels like bad design, especially if version mismatch occurs. Not sure if it's worth pursuing the re-export over extracting out the types into the quartz-lib package.

One potential hack to keep my current approach: If we can force NPM dependency resolution of A depending on B and C where C<-D<-B to respect how A depends on B when resolving D, we can avoid version mismatch by having Quartz depend on itself as a filepath dependency (a hardlink in practice, at least on Linux systems)

jackyzha0 commented 5 months ago

Could we add it as a soft peer dependency? should solve the cycle problem

OCDkirby commented 5 months ago

Perfect! PR incoming once I test that this works with a real plugin

MaelImhof commented 5 months ago

I'm not sure I understand how this becomes cyclic, don't we have three libraries?

graph TD;
    Q[Quartz itself]
    T[Shared types]
    P[Plugins]
    Q --> T
    Q --> P
    P --> T
OCDkirby commented 5 months ago

The types aren't shared, the way we decided to implement is just re-exporting. Nothing about base quartz changes (yet)

graph TD;
    Q[Quartz itself]
    T[Types re-exported]
    P[Plugins]
    Q --> P
    P --> T
    T --> Q

Anyway, quasi-peer deps and a self-dependency by Quartz solves the problem by making it look like

graph TD;
    Q[Quartz itself]
    Q2[Link to Quartz]
    T[Types re-exported]
    P[Plugins]
    Q ~~~ Q2
    Q --> P
    Q --> Q2
    T --> Q2
    P --> T
iceprosurface commented 2 months ago

I believe the two methods mentioned above are not commonly used practices in the Node community. The currently popular approach generally involves using workspaces in yarn, npm, or pnpm to build and install sub-packages. In other words, you can create a new npm package, store the types in the corresponding package, and then directly install this dependency in the top-level package.

-- package.json 
-- workspace
   -- package.json @quartz/types

I'll create a pull request later to demonstrate an example.

iceprosurface commented 2 months ago

Here is a example:

https://github.com/iceprosurface/quartz/tree/refactor/type/packages/types

We can use tsup(Of course, you can also use tsc directly) to collect types and publish them to npm.

For example:

Input:

export * from './../../../quartz/util/path'

Output:

import { Element as Element$1 } from 'hast';

declare const clone: <T>(input: T) => T;
declare const QUARTZ = "quartz";
type SlugLike<T> = string & {
    __brand: T;
};
/** Cannot be relative and must have a file extension. */
type FilePath = SlugLike<"filepath">;
declare function isFilePath(s: string): s is FilePath;
/** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */
type FullSlug = SlugLike<"full">;
declare function isFullSlug(s: string): s is FullSlug;
/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
type SimpleSlug = SlugLike<"simple">;
declare function isSimpleSlug(s: string): s is SimpleSlug;
/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
type RelativeURL = SlugLike<"relative">;
declare function isRelativeURL(s: string): s is RelativeURL;
declare function getFullSlug(window: Window): FullSlug;
declare function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug;
declare function simplifySlug(fp: FullSlug): SimpleSlug;
declare function transformInternalLink(link: string): RelativeURL;
declare function normalizeRelativeURLs(el: Element | Document, destination: string | URL): void;
declare function normalizeHastElement(rawEl: Element$1, curBase: FullSlug, newBase: FullSlug): Element$1;
declare function pathToRoot(slug: FullSlug): RelativeURL;
declare function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL;
declare function splitAnchor(link: string): [string, string];
declare function slugTag(tag: string): string;
declare function joinSegments(...args: string[]): string;
declare function getAllSegmentPrefixes(tags: string): string[];
interface TransformOptions {
    strategy: "absolute" | "relative" | "shortest";
    allSlugs: FullSlug[];
}
declare function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL;
declare function endsWith(s: string, suffix: string): boolean;
declare function stripSlashes(s: string, onlyStripPrefix?: boolean): string;

export { type FilePath, type FullSlug, QUARTZ, type RelativeURL, type SimpleSlug, type TransformOptions, clone, endsWith, getAllSegmentPrefixes, getFullSlug, isFilePath, isFullSlug, isRelativeURL, isSimpleSlug, joinSegments, normalizeHastElement, normalizeRelativeURLs, pathToRoot, resolveRelative, simplifySlug, slugTag, slugifyFilePath, splitAnchor, stripSlashes, transformInternalLink, transformLink };
iceprosurface commented 2 months ago

If the type system of Quartz is simple enough and doesn't have deep dependencies, then the exported types generated by tsup will be straightforward like the ones above. I believe this method is simple and effective. Moreover, if we plan to implement some built-in plugins in the future, using workspaces will definitely be a better solution.