diff --git a/lib/builder.ts b/lib/builder.ts new file mode 100644 index 0000000..134cbf0 --- /dev/null +++ b/lib/builder.ts @@ -0,0 +1,5 @@ +/** + * @file Builder + */ + +class Builder {} diff --git a/lib/cli.ts b/lib/cli.ts new file mode 100755 index 0000000..caea898 --- /dev/null +++ b/lib/cli.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +/** + * @file CLI entry point + */ diff --git a/lib/config.ts b/lib/config.ts new file mode 100644 index 0000000..4245b22 --- /dev/null +++ b/lib/config.ts @@ -0,0 +1,81 @@ +/** + * @file Load config from user's project + */ + +import { access } from 'node:fs/promises' +import { join } from 'node:path' +import { build } from 'esbuild' + +import { name } from '../package.json' +import { addLoader } from './loaders' + +import type { Config } from './types' + +const isRootPath = (path: string): boolean => path === '/' || path.endsWith(':\\') + +const search = async (files: string[], cwd: string = process.cwd(), depth: number = 3): Promise => { + for (const file of files) { + const filepath = join(cwd, file) + try { + await access(filepath) + return filepath + } catch { + continue + } + } + if (depth > 0 && !isRootPath(cwd)) { + return await search(files, join(cwd, '..'), depth - 1) + } +} + +const loadConfig = async (filename: string): Promise => { + if (!/\.(js|mjs|cjs|ts|mts|cts)$/.test(filename)) { + const ext = filename.split('.').pop() + throw new Error(`not supported config file with '${ext}' extension`) + } + const result = await build({ + entryPoints: [filename], + bundle: true, + write: false, + format: 'esm', + target: 'node18', + platform: 'node', + sourcemap: 'inline' + }) + const { text } = result.outputFiles[0] + const mod = await import(`data:text/javascript;base64,${Buffer.from(text).toString('base64')}`) + return mod.default ?? mod +} + +type Options = { + root?: string + filename?: string + verbose?: boolean +} + +export const resolveConfig = async (options: Options = {}): Promise => { + // prettier-ignore + const files = [ + name + '.config.js', + name + '.config.mjs', + name + '.config.cjs', + name + '.config.ts', + name + '.config.mts', + name + '.config.cts' + ] + + options.filename != null && files.unshift(options.filename) + + const filename = await search(files) + if (filename == null) throw new Error(`config file not found`) + + options.verbose && console.log(`using config '${filename}'`) + + const config: Config = await loadConfig(filename) + + config.loaders != null && config.loaders.forEach(addLoader) + + return config +} + +resolveConfig().then(console.log).catch(console.error) diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..5163765 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,5 @@ +/** + * @file module entry point + */ + +export {} from './config' diff --git a/lib/loaders/index.ts b/lib/loaders/index.ts new file mode 100644 index 0000000..b733332 --- /dev/null +++ b/lib/loaders/index.ts @@ -0,0 +1,20 @@ +import json from './json' +import markdown from './markdown' +import yaml from './yaml' + +import type { Loader } from '../types' + +const loaders = [json, yaml, markdown] + +export const addLoader = (loader: Loader): void => { + loaders.unshift(loader) +} + +export const removeLoader = (name: string): void => { + const index = loaders.findIndex(loader => loader.name === name) + index !== -1 && loaders.splice(index, 1) +} + +export const resolveLoader = (filename: string): Loader | undefined => { + return loaders.find(loader => loader.test.test(filename)) +} diff --git a/lib/loaders/json.ts b/lib/loaders/json.ts new file mode 100644 index 0000000..3dc6630 --- /dev/null +++ b/lib/loaders/json.ts @@ -0,0 +1,13 @@ +/** + * @file json file loader + */ + +import { defineLoader } from '../types' + +export default defineLoader({ + name: 'json', + test: /\.json$/, + load: async vfile => { + vfile.data.result = JSON.parse(vfile.toString()) + } +}) diff --git a/lib/loaders/markdown.ts b/lib/loaders/markdown.ts new file mode 100644 index 0000000..43c77ae --- /dev/null +++ b/lib/loaders/markdown.ts @@ -0,0 +1,15 @@ +/** + * @file markdown file loader + */ + +import { defineLoader } from '../types' + +export default defineLoader({ + name: 'markdown', + test: /\.(md|mdx)$/, + load: async vfile => { + vfile.data.result = { + content: vfile.toString() + } + } +}) diff --git a/lib/loaders/yaml.ts b/lib/loaders/yaml.ts new file mode 100644 index 0000000..ab1398f --- /dev/null +++ b/lib/loaders/yaml.ts @@ -0,0 +1,15 @@ +/** + * @file yaml file loader + */ + +import yaml from 'yaml' + +import { defineLoader } from '../types' + +export default defineLoader({ + name: 'yaml', + test: /\.(yaml|yml)$/, + load: async vfile => { + vfile.data.result = yaml.parse(vfile.toString()) + } +}) diff --git a/lib/plugins/remove-comments.ts b/lib/plugins/remove-comments.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..1f1ba1e --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,107 @@ +/** + * @file shared types + */ + +import type { VFile } from 'vfile' +import type { ZodType } from 'zod' + +/** + * Single date + */ +export type Entry = Record + +/** + * List of data + */ +export type Entries = Entry[] + +/** + * Data from document + */ +export type Collection = Entry | Entries + +/** + * All data, key is collection name + */ +export type Collections = Record + +declare module 'vfile' { + interface DataMap { + result: Collection + } +} + +/** + * File loader + */ +export interface Loader { + name: string + test: RegExp + load: (vfile: VFile, config: Config) => Promise +} + +// PluggableList + +/** + * Schema + */ +interface Schema { + /** + * Schema name + */ + name: string + /** + * Schema pattern, glob pattern, based on `root` + */ + pattern: string + /** + * Whether the schema is single + * @default false + */ + single?: boolean + /** + * Schema fields + */ + fields: ZodType +} + +interface MarkdownOptions { + remarkPlugins?: any[] + rehypePlugins?: any[] +} + +/** + * User config + */ +export interface Config = Record> { + /** + * The root directory of the contents + * @default 'content' + */ + root: string + /** + * The output directory of the data + * @default '.velite' + */ + output: string + /** + * The content schemas + */ + schemas: Schemas + /** + * File loaders + * @default [yaml, markdown, json] + */ + loaders?: Loader[] + markdown?: MarkdownOptions + /** + * Success callback, you can do anything you want with the collections, such as modify them, or write them to files + */ + callback: (collections: { + [name in keyof Schemas]: Schemas[name]['single'] extends true ? Schemas[name]['fields']['_output'] : Array + }) => void | Promise +} + +// export for user config type inference +export const defineLoader = (loader: Loader) => loader +export const defineConfig = >(config: Config) => config diff --git a/package.json b/package.json index 7582fbc..431db03 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "micromark-extension-gfm": "latest", "ora": "latest", "sharp": "latest", + "tsx": "latest", "unist-util-visit": "latest", "vfile": "latest", "vfile-reporter": "latest", diff --git a/readme.md b/readme.md index 8b2d8fa..43cdb2b 100644 --- a/readme.md +++ b/readme.md @@ -10,8 +10,23 @@ [![Dependency Status][dependency-img]][dependency-url] [![Code Style][style-img]][style-url] + +## Structure + +```text +cli ---------------------- cli entry +lib ---------------------- core library + ├── core ---------------- core functions + ├── plugins ------------- plugins + ├── schemas ------------- schemas + ├── utils --------------- utils + └── index.js ------------ core entry +``` + + ## TODOs +- [ ] loaders & plugins - [ ] nextjs plugin ## Installation diff --git a/velite.config.js b/velite.config.js new file mode 100644 index 0000000..ed8fdc2 --- /dev/null +++ b/velite.config.js @@ -0,0 +1,8 @@ +import { join } from 'node:path' +import z from 'zod' + +console.log(join('a', 'b')) + +export default { + foo: z.string() +}