├── .prettierrc ├── packages ├── mordred │ ├── webpack.js │ ├── src │ │ ├── node.ts │ │ ├── gql.ts │ │ ├── plugin.ts │ │ ├── templates.ts │ │ ├── webpack.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── next.js │ ├── templates │ │ ├── graphql.d.ts │ │ └── graphql.ejs │ └── package.json ├── mordred-source-filesystem │ ├── tsconfig.json │ ├── package.json │ └── src │ │ ├── resolvers.ts │ │ ├── to-node.ts │ │ └── index.ts ├── mordred-transformer-markdown │ ├── tsconfig.json │ ├── package.json │ ├── src │ │ ├── markdown-plugin-headings.ts │ │ ├── resolvers.ts │ │ └── index.ts │ └── README.md └── mordred-source-github-issues │ ├── tsconfig.json │ ├── src │ ├── resolvers.ts │ ├── api.ts │ └── index.ts │ └── package.json ├── example ├── content │ ├── goodbye.md │ ├── another-post.md │ └── hello-world.md ├── pages │ ├── about.js │ ├── api │ │ └── graphql.js │ ├── page │ │ └── [page].js │ └── index.js ├── next.config.js └── mordred.config.js ├── .gitignore ├── lerna.json ├── ship.config.js ├── .github └── workflows │ ├── shipjs-trigger.yml │ └── shipjs-manual-prepare.yml ├── package.json ├── CHANGELOG.md ├── LICENSE ├── tsconfig.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | "@egoist/prettier-config" 2 | -------------------------------------------------------------------------------- /packages/mordred/webpack.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/webpack') 2 | -------------------------------------------------------------------------------- /example/content/goodbye.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: wow!! 3 | --- 4 | 5 | pretty awesome! -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .next 4 | .DS_Store 5 | *.log 6 | example/mordred -------------------------------------------------------------------------------- /example/content/another-post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: This is working 3 | --- 4 | 5 | __seriously__!! good!~~ -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "0.2.1", 4 | "npmClient": "yarn", 5 | "useWorkspaces": true 6 | } 7 | -------------------------------------------------------------------------------- /example/pages/about.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | export default () => <> 4 | 5 | Home 6 | 7 | -------------------------------------------------------------------------------- /packages/mordred/src/node.ts: -------------------------------------------------------------------------------- 1 | export interface Node { 2 | id: string 3 | type: string 4 | createdAt: Date 5 | updatedAt: Date 6 | content: string 7 | } -------------------------------------------------------------------------------- /example/content/hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | date: 2020-02-02 4 | --- 5 | 6 | This is my first post! 7 | 8 | ## hello 9 | 10 | ## there 11 | 12 | ### cool -------------------------------------------------------------------------------- /packages/mordred-source-filesystem/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } -------------------------------------------------------------------------------- /packages/mordred-source-github-issues/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/mordred/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" /* Redirect output structure to the directory. */ 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /ship.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | monorepo: { 3 | mainVersionFile: "lerna.json", 4 | packagesToBump: ["packages/*"], 5 | packagesToPublish: ["packages/*"], 6 | }, 7 | buildCommand: () => null, 8 | }; 9 | -------------------------------------------------------------------------------- /example/pages/api/graphql.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import graphqlHTTP from 'express-graphql' 3 | import { schema } from '../../mordred/graphql' 4 | 5 | export default express().use(graphqlHTTP({ 6 | graphiql: true, 7 | schema 8 | })) -------------------------------------------------------------------------------- /packages/mordred/src/gql.ts: -------------------------------------------------------------------------------- 1 | export function gql(literals: TemplateStringsArray, ...variables: any[]) { 2 | return literals 3 | .map((l, i) => { 4 | const variable = variables[i] 5 | return `${l}${variable ? variable : ''}` 6 | }) 7 | .join('') 8 | } 9 | -------------------------------------------------------------------------------- /example/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack(config) { 3 | const { MordredWebpackPlugin } = require('mordred/webpack') 4 | 5 | const mordredPlugin = new MordredWebpackPlugin() 6 | config.plugins.push(mordredPlugin) 7 | 8 | return config 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /example/mordred.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | resolve: 'mordred-source-filesystem', 5 | options: { 6 | path: __dirname + '/content', 7 | }, 8 | }, 9 | { 10 | resolve: 'mordred-transformer-markdown', 11 | options: {}, 12 | }, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /packages/mordred-source-github-issues/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { GithubIssueNodeType } from './api' 2 | 3 | export default ({ nodes }: { nodes: any[] }) => { 4 | return { 5 | Query: { 6 | allGithubIssue() { 7 | const result = nodes.filter((node) => node.type === GithubIssueNodeType) 8 | return { 9 | nodes: result, 10 | } 11 | }, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/mordred-source-filesystem/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "name": "mordred-source-filesystem", 4 | "files": [ 5 | "dist" 6 | ], 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "tsc", 11 | "watch": "tsc --watch", 12 | "prepublishOnly": "yarn build" 13 | }, 14 | "peerDependencies": { 15 | "mordred": "^0.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/mordred-source-filesystem/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { FILE_NODE_TYPE } from './to-node' 2 | 3 | export default ({ nodes }: { nodes: any[] }) => { 4 | return { 5 | Query: { 6 | allFile(parent: any, args: any) { 7 | const result = nodes.filter((node) => node.type === FILE_NODE_TYPE) 8 | return { 9 | nodes: result, 10 | } 11 | }, 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/mordred/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Mordred } from '.' 2 | 3 | export type Plugin = { 4 | name: string 5 | 6 | onInit?: () => void | Promise 7 | 8 | getSchema?: (typeDefs: string[]) => string 9 | 10 | getResolvers?: () => string 11 | 12 | createNodes?: () => any[] | Promise 13 | } 14 | 15 | export type PluginFactory = ( 16 | context: Mordred, 17 | options: TOptions, 18 | ) => Plugin 19 | -------------------------------------------------------------------------------- /packages/mordred/src/templates.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { compile } from 'ejs' 3 | import { readFileSync } from 'fs-extra' 4 | 5 | const template = readFileSync( 6 | join(__dirname, '../templates/graphql.ejs'), 7 | 'utf8' 8 | ) 9 | 10 | export const graphqlTemplate = compile(template) 11 | 12 | export const graphqlDefinitionTemplate = readFileSync( 13 | join(__dirname, '../templates/graphql.d.ts'), 14 | 'utf8' 15 | ) 16 | -------------------------------------------------------------------------------- /packages/mordred-source-github-issues/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "name": "mordred-source-github-issues", 4 | "files": [ 5 | "dist" 6 | ], 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "tsc", 11 | "watch": "tsc --watch", 12 | "prepublishOnly": "yarn build" 13 | }, 14 | "dependencies": { 15 | "axios": "0.20.0" 16 | }, 17 | "peerDependencies": { 18 | "mordred": "^0.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/mordred/next.js: -------------------------------------------------------------------------------- 1 | export const withMordred = (config = {}) => { 2 | const webpack = config.webpack 3 | 4 | config.webpack = (webpackConfig, options) => { 5 | const { MordredWebpackPlugin } = require('./webpack') 6 | 7 | const mordredPlugin = new MordredWebpackPlugin() 8 | webpackConfig.plugins.push(mordredPlugin) 9 | 10 | if (webpack) { 11 | webpackConfig = webpack(webpackConfig, options) 12 | } 13 | 14 | return webpackConfig 15 | } 16 | 17 | return config 18 | } 19 | -------------------------------------------------------------------------------- /packages/mordred/templates/graphql.d.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionResult, GraphQLSchema } from 'graphql' 2 | import { Thunder } from './zeus' 3 | 4 | export const client: ReturnType 5 | 6 | export declare type QueryOptions = { 7 | variables?: { 8 | [k: string]: any 9 | } 10 | } 11 | 12 | export declare const schema: GraphQLSchema 13 | 14 | export declare const executeQuery: ( 15 | query: string, 16 | options?: QueryOptions, 17 | ) => ExecutionResult 18 | 19 | export declare function gql( 20 | literals: TemplateStringsArray, 21 | ...variables: any[] 22 | ): string 23 | -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "name": "mordred-transformer-markdown", 4 | "files": [ 5 | "dist" 6 | ], 7 | "main": "dist/index.js", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "tsc", 11 | "watch": "tsc --watch", 12 | "prepublishOnly": "yarn build" 13 | }, 14 | "devDependencies": { 15 | "@types/markdown-it": "^10.0.0" 16 | }, 17 | "dependencies": { 18 | "gray-matter": "^4.0.2", 19 | "markdown-it": "^10.0.0" 20 | }, 21 | "peerDependencies": { 22 | "mordred": "^0.2.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/src/markdown-plugin-headings.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it' 2 | 3 | export function markdownPluginHeadings(md: MarkdownIt) { 4 | const heading_open = md.renderer.rules.heading_open 5 | md.renderer.rules.heading_open = (tokens, idx, options, env, renderer) => { 6 | const token = tokens[idx] 7 | const nextToken = tokens[idx + 1] 8 | env.headings = env.headings || [] 9 | env.headings.push({ 10 | text: nextToken.content, 11 | depth: Number(token.tag.slice(1)), 12 | }) 13 | return renderer.renderToken(tokens, idx, options) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/pages/page/[page].js: -------------------------------------------------------------------------------- 1 | import { client } from '../../mordred/graphql' 2 | 3 | export { getStaticProps, default } from '../' 4 | 5 | export const getStaticPaths = async () => { 6 | const { allMarkdown } = await client.query({ 7 | allMarkdown: [ 8 | { 9 | limit: 1, 10 | }, 11 | { 12 | pageInfo: { 13 | pageCount: true, 14 | }, 15 | }, 16 | ], 17 | }) 18 | const paths = new Array(allMarkdown.pageInfo.pageCount) 19 | .fill(null) 20 | .map((_, i) => { 21 | return { 22 | params: { 23 | page: String(i + 2), 24 | }, 25 | } 26 | }) 27 | return { 28 | fallback: false, 29 | paths, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/shipjs-trigger.yml: -------------------------------------------------------------------------------- 1 | name: Ship js trigger 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | ref: master 14 | - uses: actions/setup-node@v1 15 | with: 16 | registry-url: "https://registry.npmjs.org" 17 | - run: | 18 | if [ -f "yarn.lock" ]; then 19 | yarn install 20 | else 21 | npm install 22 | fi 23 | - run: npm run release:trigger 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 27 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }} 28 | -------------------------------------------------------------------------------- /packages/mordred-source-github-issues/src/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const GithubIssueNodeType = 'GithubIssue' 4 | 5 | export const fetchIssues = async ({ 6 | token, 7 | user, 8 | repo, 9 | }: { 10 | token?: string 11 | user: string 12 | repo: string 13 | }) => { 14 | const { data } = await axios.get( 15 | `https://api.github.com/repos/${user}/${repo}/issues?per_page=100`, 16 | { 17 | headers: { 18 | Authorization: token ? `Bearer ${token}` : null, 19 | }, 20 | }, 21 | ) 22 | return data.map((item: any) => ({ 23 | id: item.id, 24 | type: GithubIssueNodeType, 25 | mime: 'text/markdown', 26 | title: item.title, 27 | content: item.body, 28 | createdAt: item.created_at, 29 | updatedAt: item.updated_at, 30 | comments: item.comments, 31 | labels: item.labels, 32 | })) 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "mordred", 4 | "version": "1.0.0", 5 | "repository": "git@github.com:egoist/mordred-test.git", 6 | "author": "EGOIST <0x142857@gmail.com>", 7 | "license": "MIT", 8 | "scripts": { 9 | "toc": "doctoc README.md", 10 | "release:prepare": "shipjs prepare", 11 | "release:trigger": "shipjs trigger" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "lint-staged" 16 | } 17 | }, 18 | "lint-staged": { 19 | "README.md": [ 20 | "yarn doctoc" 21 | ] 22 | }, 23 | "workspaces": [ 24 | "packages/*" 25 | ], 26 | "devDependencies": { 27 | "@egoist/prettier-config": "^0.1.0", 28 | "doctoc": "^1.4.0", 29 | "husky": "^4.2.5", 30 | "lerna": "^3.20.2", 31 | "lint-staged": "^10.1.7", 32 | "prettier": "^2.1.1", 33 | "shipjs": "0.18.2", 34 | "typescript": "^3.8.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [0.2.1](https://github.com/egoist/mordred-test/compare/v0.2.0...v0.2.1) (2020-09-02) 2 | 3 | ### Features 4 | 5 | - add mordred-source-github-issues ([959132d](https://github.com/egoist/mordred-test/commit/959132d3b4d5395c43f5a64ce67ddd2f131923f0)) 6 | 7 | # [0.2.0](https://github.com/egoist/mordred-test/compare/v0.1.1...v0.2.0) (2020-09-01) 8 | 9 | ### Features 10 | 11 | - generate js client, closes [#9](https://github.com/egoist/mordred-test/issues/9) ([f55f294](https://github.com/egoist/mordred-test/commit/f55f294cde4637bd32dc17291ce96f9aea925201)) 12 | 13 | # [0.1.1](https://github.com/egoist/mordred/compare/v0.1.0...v0.1.1) (2020-04-25) 14 | 15 | ### Bug Fixes 16 | 17 | - (mordred-transformer-markdown) use double underscores to reference nested prop ([d5169f7](https://github.com/egoist/mordred/commit/d5169f7cc49153f71d94f1e8a0d02afcef17f199)) 18 | 19 | ### Features 20 | 21 | - (plugin) allow to write resolvers in standalone file ([7b53ad1](https://github.com/egoist/mordred/commit/7b53ad151639bc37d3cbfa2a7fd56bb4e2fa682a)) 22 | 23 | # 0.1.0 (2020-04-24) 24 | 25 | First release. 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EGOIST (Kevin Titor) <0x142857@gmail.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/mordred-source-filesystem/src/to-node.ts: -------------------------------------------------------------------------------- 1 | import { join, extname } from 'path' 2 | import hashsum from 'hash-sum' 3 | import { stat, readFile } from 'fs-extra' 4 | import { getType as GetType } from 'mime' 5 | 6 | export const getFileNodeId = (filename: string) => { 7 | return hashsum(`file::${filename}`) 8 | } 9 | 10 | export type FileNode = { 11 | id: string 12 | type: typeof FILE_NODE_TYPE 13 | mime: string | null 14 | createdAt: Date 15 | updatedAt: Date 16 | content: string 17 | absolutePath: string 18 | relativePath: string 19 | slug: string 20 | } 21 | 22 | export async function fileToNode(filename: string, cwd: string, getType: typeof GetType): Promise { 23 | const absolutePath = join(cwd, filename) 24 | const content = await readFile(absolutePath, 'utf8') 25 | const { ctime, mtime } = await stat(absolutePath) 26 | return { 27 | id: getFileNodeId(filename), 28 | type: FILE_NODE_TYPE, 29 | mime: getType(extname(filename)), 30 | createdAt: ctime, 31 | updatedAt: mtime, 32 | content, 33 | relativePath: filename, 34 | absolutePath, 35 | slug: filename.replace(/\.[a-zA-Z0-9]+$/, '') 36 | } 37 | } 38 | 39 | export const FILE_NODE_TYPE = 'File' -------------------------------------------------------------------------------- /packages/mordred-source-github-issues/src/index.ts: -------------------------------------------------------------------------------- 1 | import { relative, join } from 'path' 2 | import { PluginFactory } from 'mordred' 3 | import { fetchIssues } from './api' 4 | 5 | const plugin: PluginFactory<{ token?: string; user: string; repo: string }> = ( 6 | ctx, 7 | { token, user, repo }, 8 | ) => { 9 | const gql = ctx.gql 10 | return { 11 | name: 'source-github-issues', 12 | 13 | getSchema() { 14 | return gql` 15 | type GithubIssueNode { 16 | id: ID! 17 | mime: String! 18 | title: String 19 | content: String! 20 | createdAt: String! 21 | updatedAt: String! 22 | comments: Int 23 | labels: [GithubIssueNodeLabel!] 24 | } 25 | 26 | type GithubIssueNodeLabel { 27 | id: ID! 28 | name: String! 29 | color: String! 30 | description: String 31 | } 32 | 33 | type GithubIssueConnection { 34 | nodes: [GithubIssueNode!]! 35 | } 36 | 37 | extend type Query { 38 | allGithubIssue: GithubIssueConnection 39 | } 40 | ` 41 | }, 42 | 43 | createNodes() { 44 | return fetchIssues({ token, user, repo }) 45 | }, 46 | 47 | getResolvers: () => relative(ctx.outDir, join(__dirname, 'resolvers')), 48 | } 49 | } 50 | 51 | export default plugin 52 | -------------------------------------------------------------------------------- /packages/mordred/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.1", 3 | "name": "mordred", 4 | "files": [ 5 | "dist", 6 | "/webpack.js", 7 | "/templates", 8 | "/next.js" 9 | ], 10 | "main": "dist/index.js", 11 | "types": "dist/index.d.ts", 12 | "license": "MIT", 13 | "scripts": { 14 | "build": "tsc", 15 | "watch": "tsc --watch", 16 | "example": "next example", 17 | "prepublishOnly": "yarn build" 18 | }, 19 | "devDependencies": { 20 | "@types/ejs": "^3.0.2", 21 | "@types/express": "^4.17.6", 22 | "@types/fs-extra": "^8.1.0", 23 | "@types/graphql-type-json": "^0.3.2", 24 | "@types/hash-sum": "^1.0.0", 25 | "@types/mime": "^2.0.1", 26 | "@types/node": "^13.13.2", 27 | "@types/serialize-javascript": "^1.5.0", 28 | "@types/webpack": "^4.41.12", 29 | "express": "^4.17.1", 30 | "express-graphql": "^0.9.0", 31 | "next": "9.5.2", 32 | "react": "^16.13.1", 33 | "react-dom": "^16.13.1" 34 | }, 35 | "dependencies": { 36 | "@graphql-tools/merge": "^6.2.0", 37 | "chokidar": "^3.3.1", 38 | "ejs": "^3.1.2", 39 | "fast-glob": "^3.2.2", 40 | "fs-extra": "^9.0.0", 41 | "graphql": "^14.0.0", 42 | "graphql-tools": "^5.0.0", 43 | "graphql-type-json": "^0.3.1", 44 | "graphql-zeus": "^2.7.5", 45 | "hash-sum": "^2.0.0", 46 | "joycon": "^2.2.5", 47 | "mime": "^2.4.4", 48 | "serialize-javascript": "^3.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/mordred/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import { Compiler } from 'webpack' 2 | import JoyCon from 'joycon' 3 | import { Mordred } from './' 4 | 5 | type PluginConfigObject = { 6 | resolve: string 7 | options?: any 8 | } 9 | 10 | type MordredConfigPlugins = PluginConfigObject[] 11 | 12 | export type MordredConfig = { 13 | plugins?: MordredConfigPlugins 14 | } 15 | 16 | let initialized = false 17 | 18 | export class MordredWebpackPlugin { 19 | loadConfig(cwd: string): MordredConfig { 20 | const joycon = new JoyCon() 21 | const { path, data } = joycon.loadSync(['mordred.config.js'], cwd) 22 | if (!path) { 23 | throw new Error(`Cannot find mordred.config.js in your project`) 24 | } 25 | return data || {} 26 | } 27 | 28 | apply(compiler: Compiler) { 29 | if (initialized) { 30 | return 31 | } 32 | 33 | initialized = true 34 | 35 | const webpackContext = compiler.context || process.cwd() 36 | 37 | const config = this.loadConfig(webpackContext) 38 | 39 | const mordred = new Mordred(config, { 40 | cwd: webpackContext, 41 | }) 42 | 43 | let started = false 44 | compiler.hooks.watchRun.tapPromise('mordred', async () => { 45 | if (started) { 46 | return 47 | } 48 | started = true 49 | await mordred.init() 50 | }) 51 | compiler.hooks.run.tapPromise('mordred', async () => { 52 | await mordred.init() 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/mordred/templates/graphql.ejs: -------------------------------------------------------------------------------- 1 | /*********************************************** 2 | *------------ Generated by Mordred ------------* 3 | *------ YOU SHOULD NOT EDIT THIS FILE !!! -----* 4 | ***********************************************/ 5 | 6 | import { graphql } from 'graphql' 7 | import { makeExecutableSchema } from 'graphql-tools' 8 | import { GraphQLJSON } from 'graphql-type-json' 9 | import { Thunder } from './zeus' 10 | import nodes from './nodes' 11 | 12 | export const typeDefs = `<%= typeDefs %>` 13 | 14 | export const resolvers = [{ 15 | JSON: GraphQLJSON 16 | }] 17 | 18 | const resolverArgs = { 19 | nodes 20 | } 21 | 22 | <% plugins.forEach(function (plugin, index) { %> 23 | 24 | <% if (plugin.getResolvers) { %> 25 | import getResolvers_<%= index %> from '<%= plugin.getResolvers().replace(/\\/g, '') %>' 26 | resolvers.push(getResolvers_<%= index %>(resolverArgs)) 27 | <% } %> 28 | 29 | 30 | <% }) %> 31 | 32 | export const schema = makeExecutableSchema({ 33 | typeDefs, 34 | resolvers 35 | }) 36 | 37 | export const executeQuery = (query, { variables } = {}) => { 38 | return graphql(schema, query, {}, {}, variables) 39 | } 40 | 41 | export function gql(literals, ...variables) { 42 | return literals 43 | .map((l, i) => { 44 | const variable = variables[i] 45 | return `${l}${variable ? variable : ''}` 46 | }) 47 | .join('') 48 | } 49 | 50 | export const client = Thunder(async (query, variables) => { 51 | const { errors, data } = await executeQuery(query, { variables }) 52 | if (errors && errors.length > 0) { 53 | throw errors[0] 54 | } 55 | return data 56 | }) -------------------------------------------------------------------------------- /example/pages/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { client } from '../mordred/graphql' 3 | 4 | export const getStaticProps = async ({ params = {} }) => { 5 | const limit = 1 6 | const page = Number(params.page || 1) 7 | const skip = (page - 1) * limit 8 | const { allMarkdown } = await client.query({ 9 | allMarkdown: [ 10 | { 11 | skip, 12 | limit, 13 | }, 14 | { 15 | nodes: { 16 | id: true, 17 | html: true, 18 | createdAt: true, 19 | frontmatter: { 20 | title: true, 21 | }, 22 | }, 23 | pageInfo: { 24 | hasNextPage: true, 25 | hasPrevPage: true, 26 | }, 27 | }, 28 | ], 29 | }) 30 | 31 | return { 32 | props: { 33 | page, 34 | allMarkdown, 35 | }, 36 | } 37 | } 38 | 39 | export default ({ allMarkdown, page }) => { 40 | return ( 41 | <> 42 | 43 | About 44 | 45 |
    46 | {allMarkdown.nodes.map((post) => { 47 | return ( 48 |
  • 49 |

    {post.frontmatter.title}

    50 |
    51 |
  • 52 | ) 53 | })} 54 |
55 |
56 | {allMarkdown.pageInfo.hasPrevPage && ( 57 | 58 | Prev Page 59 | 60 | )}{' '} 61 | {allMarkdown.pageInfo.hasNextPage && ( 62 | 63 | Next Page 64 | 65 | )} 66 |
67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/src/resolvers.ts: -------------------------------------------------------------------------------- 1 | const getValue = (obj: any, path: string) => { 2 | if (path.startsWith('frontmatter__')) { 3 | return obj.frontmatter[path.replace('frontmatter__', '')] 4 | } 5 | return obj[path] 6 | } 7 | 8 | export default ({ nodes }: { nodes: any[] }) => { 9 | return { 10 | Query: { 11 | markdownBySlug(_: any, args: any) { 12 | const node = nodes.find( 13 | (node) => node.type === 'Markdown' && node.slug === args.slug, 14 | ) 15 | return node 16 | }, 17 | 18 | allMarkdown(_: any, args: any) { 19 | const orderBy = args.orderBy || 'createdAt' 20 | const order = args.order || 'DESC' 21 | const skip = args.skip || 0 22 | 23 | const markdownNodes = nodes 24 | .filter((node) => { 25 | return node.type === 'Markdown' 26 | }) 27 | .sort((a, b) => { 28 | const aValue = getValue(a, orderBy) 29 | const bValue = getValue(b, orderBy) 30 | if (order === 'ASC') { 31 | return aValue > bValue ? 1 : -1 32 | } 33 | return aValue > bValue ? -1 : 1 34 | }) 35 | const endIndex = args.limit ? skip + args.limit : markdownNodes.length 36 | const result = markdownNodes.slice(skip, endIndex) 37 | const pageCount = args.limit 38 | ? Math.ceil(markdownNodes.length / args.limit) 39 | : 1 40 | const hasNextPage = endIndex < markdownNodes.length 41 | const hasPrevPage = skip > 0 42 | return { 43 | nodes: result, 44 | pageInfo: { 45 | hasPrevPage, 46 | hasNextPage, 47 | pageCount, 48 | }, 49 | } 50 | }, 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/shipjs-manual-prepare.yml: -------------------------------------------------------------------------------- 1 | name: Ship js Manual Prepare 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | manual_prepare: 7 | if: | 8 | github.event_name == 'issue_comment' && 9 | (github.event.comment.author_association == 'member' || github.event.comment.author_association == 'owner') && 10 | startsWith(github.event.comment.body, '@shipjs prepare') 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | ref: master 17 | - uses: actions/setup-node@v1 18 | - run: | 19 | if [ -f "yarn.lock" ]; then 20 | yarn install 21 | else 22 | npm install 23 | fi 24 | - run: | 25 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 26 | git config --global user.name "github-actions[bot]" 27 | - run: npm run release:prepare -- --yes --no-browse 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | SLACK_INCOMING_HOOK: ${{ secrets.SLACK_INCOMING_HOOK }} 31 | 32 | create_done_comment: 33 | if: success() 34 | needs: manual_prepare 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/github@master 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | args: comment "@${{ github.actor }} `shipjs prepare` done" 42 | 43 | create_fail_comment: 44 | if: cancelled() || failure() 45 | needs: manual_prepare 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/github@master 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | args: comment "@${{ github.actor }} `shipjs prepare` fail" 53 | -------------------------------------------------------------------------------- /packages/mordred-source-filesystem/src/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve, join, relative } from 'path' 2 | import glob from 'fast-glob' 3 | import { fileToNode } from './to-node' 4 | import { PluginFactory } from 'mordred' 5 | 6 | const plugin: PluginFactory = ( 7 | ctx, 8 | { path = 'content', include = '**/*.md' }, 9 | ) => { 10 | const gql = ctx.gql 11 | const contentDir = resolve(ctx.cwd, path) 12 | const contentGlobs = [...(Array.isArray(include) ? include : [include])] 13 | 14 | return { 15 | name: 'source-filesystem', 16 | 17 | getSchema() { 18 | return gql` 19 | type FileNode { 20 | id: ID! 21 | type: String! 22 | mime: String 23 | createdAt: String! 24 | updatedAt: String! 25 | content: String! 26 | relativePath: String 27 | absolutePath: String 28 | slug: String 29 | } 30 | 31 | type FileConnection { 32 | nodes: [FileNode!]! 33 | } 34 | 35 | extend type Query { 36 | allFile: FileConnection! 37 | } 38 | ` 39 | }, 40 | 41 | getResolvers: () => relative(ctx.outDir, join(__dirname, 'resolvers')), 42 | 43 | async createNodes() { 44 | const files = await glob(contentGlobs, { 45 | cwd: contentDir, 46 | }) 47 | 48 | const nodes = await Promise.all( 49 | files.map((filename) => { 50 | return fileToNode(filename, contentDir, ctx.mime.getType) 51 | }), 52 | ) 53 | 54 | return nodes 55 | }, 56 | 57 | async onInit() { 58 | if (process.env.NODE_ENV === 'development') { 59 | const { watch } = await import('chokidar') 60 | watch(contentGlobs, { 61 | cwd: contentDir, 62 | ignoreInitial: true, 63 | }).on('all', async () => { 64 | await ctx.createNodes() 65 | await ctx.writeAll() 66 | }) 67 | } 68 | }, 69 | } 70 | } 71 | 72 | export default plugin 73 | -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/README.md: -------------------------------------------------------------------------------- 1 | # mordred-transformer-markdown 2 | 3 | Parse markdown files using [markdown-it](https://github.com/markdown-it/markdown-it). 4 | 5 | ## Table of Contents 6 | 7 | 8 | 9 | 10 | 11 | - [Install](#install) 12 | - [How to use](#how-to-use) 13 | - [How to query](#how-to-query) 14 | - [Query Markdown Headings](#query-markdown-headings) 15 | - [Pagination](#pagination) 16 | - [License](#license) 17 | 18 | 19 | 20 | ## Install 21 | 22 | ```bash 23 | yarn add mordred-transformer-markdown 24 | ``` 25 | 26 | ## How to use 27 | 28 | In your `mordred.config.js`: 29 | 30 | ```js 31 | module.exports = { 32 | plugins: [ 33 | // Typically after `mordred-source-filesystem` 34 | { 35 | resolve: 'mordred-transformer-markdown', 36 | options: { 37 | // All markdown-it options are supported 38 | } 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | ## How to query 45 | 46 | A simple query: 47 | 48 | ```graphql 49 | { 50 | allMarkdown { 51 | node { 52 | html 53 | frontmatter { 54 | # Assumes you're using title in your frontmatter. 55 | title 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ### Query Markdown Headings 63 | 64 | ```graphql 65 | { 66 | allMarkdown { 67 | nodes { 68 | headings { 69 | depth 70 | text 71 | } 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | ### Pagination 78 | 79 | By default markdown nodes are ordered by `createdAt` in `DESC`, but you can also order them by any frontmatter key: 80 | 81 | ```graphql 82 | { 83 | allMarkdown(orderBy: frontmatter__title, skip: 5, limit: 5) { 84 | nodes { 85 | slug 86 | } 87 | pageInfo { 88 | hasNextPage 89 | hasPrevPage 90 | pageCount 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | ## License 97 | 98 | MIT © [EGOIST (Kevin Titor)](https://github.com/sponsor/egoist) 99 | -------------------------------------------------------------------------------- /packages/mordred/src/index.ts: -------------------------------------------------------------------------------- 1 | import { join, relative } from 'path' 2 | import { outputFile } from 'fs-extra' 3 | import serialize from 'serialize-javascript' 4 | import mime from 'mime' 5 | import { mergeTypeDefs } from '@graphql-tools/merge' 6 | import { print } from 'graphql' 7 | import { Parser, TreeToTS } from 'graphql-zeus' 8 | import { graphqlTemplate, graphqlDefinitionTemplate } from './templates' 9 | import { Plugin } from './plugin' 10 | import { gql } from './gql' 11 | 12 | export { PluginFactory } from './plugin' 13 | 14 | type PluginConfigObject = { 15 | resolve: string 16 | options?: any 17 | } 18 | 19 | type MordredConfigPlugins = PluginConfigObject[] 20 | 21 | export type MordredConfig = { 22 | plugins?: MordredConfigPlugins 23 | } 24 | export class Mordred { 25 | config: MordredConfig 26 | cwd: string 27 | plugins: Plugin[] 28 | outDir: string 29 | 30 | gql = gql 31 | mime = mime 32 | 33 | nodes: Map 34 | 35 | constructor(config: MordredConfig, { cwd }: { cwd: string }) { 36 | this.config = config 37 | this.cwd = cwd 38 | this.nodes = new Map() 39 | 40 | this.outDir = join(cwd, 'mordred') 41 | 42 | this.plugins = (this.config.plugins || []).map(({ resolve, options }) => { 43 | const pluginDefaultExport = require(resolve).default || require(resolve) 44 | const plugin: Plugin = pluginDefaultExport(this, options) 45 | return plugin 46 | }) 47 | } 48 | 49 | get graphqlClientPath() { 50 | return join(this.outDir, 'graphql.js') 51 | } 52 | 53 | async writeGraphQL() { 54 | try { 55 | const outContent = graphqlTemplate({ 56 | typeDefs: this.typeDefs, 57 | plugins: this.plugins, 58 | }) 59 | 60 | await outputFile(this.graphqlClientPath, outContent, 'utf8') 61 | await outputFile( 62 | join(this.outDir, 'graphql.d.ts'), 63 | graphqlDefinitionTemplate, 64 | 'utf8', 65 | ) 66 | } catch (err) { 67 | console.error(err) 68 | throw err 69 | } 70 | } 71 | 72 | async writeNodes() { 73 | const outPath = join(this.outDir, 'nodes.json') 74 | const outContent = `${serialize([...this.nodes.values()], { 75 | isJSON: true, 76 | })}` 77 | await outputFile(outPath, outContent, 'utf8') 78 | } 79 | 80 | get typeDefs() { 81 | const types: string[] = this.plugins 82 | .filter((plugin) => plugin.getSchema) 83 | .reduce( 84 | (result, plugin) => { 85 | result.push(plugin.getSchema ? plugin.getSchema(result) : '') 86 | return result 87 | }, 88 | [ 89 | ` 90 | scalar JSON 91 | 92 | type Query { 93 | hello: String 94 | } 95 | `, 96 | ], 97 | ) 98 | 99 | const typeDefs = print(mergeTypeDefs(types)) 100 | return typeDefs 101 | } 102 | 103 | async writeZeus() { 104 | const tree = Parser.parse(this.typeDefs) 105 | const jsDefinition = TreeToTS.javascript(tree) 106 | await Promise.all([ 107 | outputFile( 108 | join(this.outDir, 'zeus.d.ts'), 109 | jsDefinition.definitions, 110 | 'utf8', 111 | ), 112 | outputFile(join(this.outDir, 'zeus.js'), jsDefinition.javascript, 'utf8'), 113 | ]) 114 | } 115 | 116 | async writeAll() { 117 | console.log( 118 | `Updating GraphQL client at ${relative( 119 | process.cwd(), 120 | this.graphqlClientPath, 121 | )}..`, 122 | ) 123 | await Promise.all([ 124 | this.writeNodes(), 125 | this.writeZeus(), 126 | this.writeGraphQL(), 127 | ]) 128 | } 129 | 130 | async init() { 131 | for (const plugin of this.plugins) { 132 | if (plugin.onInit) { 133 | await plugin.onInit() 134 | } 135 | } 136 | 137 | await this.createNodes() 138 | await this.writeAll() 139 | } 140 | 141 | async createNodes() { 142 | this.nodes.clear() 143 | for (const plugin of this.plugins) { 144 | if (plugin.createNodes) { 145 | const nodes = await plugin.createNodes() 146 | for (const node of nodes) { 147 | this.nodes.set(node.id, node) 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /packages/mordred-transformer-markdown/src/index.ts: -------------------------------------------------------------------------------- 1 | import { join, relative } from 'path' 2 | import { PluginFactory } from 'mordred' 3 | import grayMatter from 'gray-matter' 4 | import { markdownPluginHeadings } from './markdown-plugin-headings' 5 | 6 | const plugin: PluginFactory<{ 7 | inputNodeTypes?: string[] 8 | [k: string]: any 9 | }> = (ctx, options) => { 10 | const frontmatterKeys: Set = new Set() 11 | const inputNodeTypes: Set = new Set() 12 | const gql = ctx.gql 13 | 14 | return { 15 | name: 'transformer-markdown', 16 | 17 | getSchema(typeDefs) { 18 | const types = typeDefs.join('\n') 19 | const extraNodeFields: Array = [] 20 | 21 | for (const type of inputNodeTypes) { 22 | const re = new RegExp(`type\\s+${type}\\s+{([^}]+)}`) 23 | const m = re.exec(types) 24 | if (m) { 25 | extraNodeFields.push(m[1]) 26 | } else { 27 | extraNodeFields.push(null) 28 | } 29 | } 30 | 31 | const MarkdownFrontMatter = 32 | frontmatterKeys.size === 0 33 | ? '' 34 | : `type MarkdownFrontMatter { 35 | ${[...frontmatterKeys] 36 | .map((key) => { 37 | return `${key}: JSON` 38 | }) 39 | .join('\n')} 40 | }` 41 | 42 | return gql` 43 | ${MarkdownFrontMatter} 44 | 45 | enum MarkdownNodeOrderBy { 46 | createdAt 47 | updatedAt 48 | ${[...frontmatterKeys].map((key) => { 49 | return 'frontmatter__' + key 50 | })} 51 | } 52 | 53 | enum MarkdownNodeOrder { 54 | ASC 55 | DESC 56 | } 57 | 58 | type MarkdownNodeHeading { 59 | depth: Int! 60 | text: String! 61 | } 62 | 63 | type MarkdownNode { 64 | html: String! 65 | headings: [MarkdownNodeHeading!]! 66 | frontmatter: ${ 67 | frontmatterKeys.size === 0 ? 'JSON' : 'MarkdownFrontMatter' 68 | } 69 | } 70 | 71 | ${extraNodeFields 72 | .map((fields) => { 73 | return ` 74 | extend type MarkdownNode { 75 | ${fields} 76 | }` 77 | }) 78 | .join('\n')} 79 | 80 | 81 | type MarkdownPageInfo { 82 | hasPrevPage: Boolean! 83 | hasNextPage: Boolean! 84 | pageCount: Int! 85 | } 86 | 87 | type MarkdownConnection { 88 | nodes: [MarkdownNode] 89 | pageInfo: MarkdownPageInfo! 90 | } 91 | 92 | extend type Query { 93 | markdownBySlug(slug: String!): MarkdownNode 94 | 95 | allMarkdown(orderBy: MarkdownNodeOrderBy, order: MarkdownNodeOrder, limit: Int, skip: Int): MarkdownConnection 96 | } 97 | ` 98 | }, 99 | 100 | getResolvers: () => relative(ctx.outDir, join(__dirname, 'resolvers')), 101 | 102 | createNodes() { 103 | frontmatterKeys.clear() 104 | inputNodeTypes.clear() 105 | 106 | const nodes = [...ctx.nodes.values()] 107 | .filter((node) => { 108 | return node.mime === 'text/markdown' 109 | }) 110 | .map((node) => { 111 | const { data, content } = grayMatter(node.content) 112 | for (const key of Object.keys(data)) { 113 | frontmatterKeys.add(key) 114 | } 115 | inputNodeTypes.add(`${node.type}Node`) 116 | const Markdown = require('markdown-it') 117 | const md = new Markdown({ 118 | html: options.html !== false, 119 | breaks: options.breaks, 120 | linkify: options.linkify, 121 | typographer: options.typographer, 122 | highlight: options.highlight, 123 | quotes: options.quotes, 124 | langPrefix: options.langPrefix, 125 | }) 126 | md.use(markdownPluginHeadings) 127 | const env = { headings: [] } 128 | const html = md.render(content, env) 129 | return { 130 | ...node, 131 | id: `Markdown::${node.id}`, 132 | type: 'Markdown', 133 | content, 134 | html, 135 | headings: env.headings, 136 | frontmatter: data, 137 | } 138 | }) 139 | 140 | return nodes 141 | }, 142 | } 143 | } 144 | 145 | export default plugin 146 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 18 | // "removeComments": true, /* Do not emit comments to output. */ 19 | // "noEmit": true, /* Do not emit outputs. */ 20 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 21 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 22 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 23 | 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | 40 | /* Module Resolution Options */ 41 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 42 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 43 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 44 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 45 | // "typeRoots": [], /* List of folders to include type definitions from. */ 46 | // "types": [], /* Type declaration files to be included in compilation. */ 47 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 48 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 49 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 50 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | 62 | /* Advanced Options */ 63 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤺 Mordred 2 | 3 | [![npm version](https://flat.badgen.net/npm/v/mordred?scale=1.5)](https://npm.im/mordred) [![community](https://flat.badgen.net/badge/icon/discord?icon=discord&label=community&scale=1.5)](https://chat.egoist.sh) 4 | 5 | **Source data from anywhere, for Next.js, Nuxt.js, Eleventy and many more.** 6 | 7 | ## Features 8 | 9 | ✅ Inspired by [Gatsby](https://gatsbyjs.org), you can query any data (Markdown, API, database, CMS) with GraphQL
10 | ✅ Automatically generate JavaScript client for better dev experience
11 | ✅ Framework agnostic, works with any framework that has SSG support
12 | ✅ Tons of plugins for popular headless CMS (not yet, we need your contribution!) 13 | 14 | ## Table of Contents 15 | 16 | 17 | 18 | 19 | 20 | - [Install](#install) 21 | - [Usage with Next.js](#usage-with-nextjs) 22 | - [Configuration](#configuration) 23 | - [Using Data](#using-data) 24 | - [Execute Raw Query](#execute-raw-query) 25 | - [Module Alias](#module-alias) 26 | - [Exploring Data with GraphiQL](#exploring-data-with-graphiql) 27 | - [Plugin List](#plugin-list) 28 | - [License](#license) 29 | 30 | 31 | 32 | ## Install 33 | 34 | ```bash 35 | yarn add mordred 36 | ``` 37 | 38 | ## Usage with Next.js 39 | 40 | ### Configuration 41 | 42 | In `next.config.js`: 43 | 44 | ```js 45 | const { withMordred } = require('mordred/next') 46 | 47 | module.exports = withMordred({ 48 | // Extra Next.js config.. 49 | }) 50 | ``` 51 | 52 | Then create a `mordred.config.js` in the same directory and use some plugins: 53 | 54 | ```js 55 | module.exports = { 56 | plugins: [ 57 | // Load markdown files from file system 58 | { 59 | resolve: 'mordred-source-filesystem', 60 | options: { 61 | // This is where you'll be creating Markdown files 62 | path: __dirname + '/content', 63 | }, 64 | }, 65 | // Transform files to markdown nodes 66 | { 67 | resolve: 'mordred-transformer-markdown', 68 | }, 69 | ], 70 | } 71 | ``` 72 | 73 | You also need to install these plugins: 74 | 75 | ```bash 76 | yarn add mordred-source-filesystem mordred-transformer-markdown 77 | ``` 78 | 79 | ### Using Data 80 | 81 | Create a Markdown file in `content` folder (in your project root), like `content/my-first-posts.md`: 82 | 83 | ```markdown 84 | --- 85 | title: My First Post 86 | date: 2020-04-24 87 | --- 88 | 89 | This is my **first** post! 90 | ``` 91 | 92 | When you run `next` or `next build`, Mordred will generate a GraphQL client at `mordred/graphql.js`, then you can use the generated client to query data. 93 | 94 | **You should add this folder to `.gitignore`:** 95 | 96 | ``` 97 | mordred/ 98 | ``` 99 | 100 | Now in any page, query data in `getStaticProps`: 101 | 102 | ```js 103 | import { client } from '../mordred/graphql' 104 | 105 | export const getStaticProps = async () => { 106 | const { allMarkdown } = await client.query({ 107 | allMarkdown: [ 108 | { 109 | limit: 20 110 | }, 111 | { 112 | nodes: { 113 | id: true, 114 | slug: true, 115 | createdAt: true, 116 | updatedAt: true, 117 | html: true, 118 | frontmatter { 119 | title: true 120 | } 121 | } 122 | } 123 | ] 124 | }) 125 | return { 126 | props: { 127 | allMarkdown 128 | }, 129 | } 130 | } 131 | 132 | export default ({ allMarkdown }) => { 133 | return ( 134 |
    135 | {allMarkdown.nodes.map((post) => { 136 | return ( 137 |
  • 138 | {post.title} 139 |
  • 140 | ) 141 | })} 142 |
143 | ) 144 | } 145 | ``` 146 | 147 | The `client.query` syntax is very similar to GraphQL SDL except that it also provides type hints as you write, we use [graphql-zeus](https://github.com/graphql-editor/graphql-zeus) to generate the client code. 148 | 149 | ### Execute Raw Query 150 | 151 | If you prefer GraphQL SDL over the JavaScript client, you can execute raw query too: 152 | 153 | ```js 154 | import { executeQuery, gql } from './path/to/mordred/graphql' 155 | 156 | const { data, errors } = await executeQuery( 157 | gql` 158 | query($limit: Int!) { 159 | allMarkdown(limit: $limit) { 160 | id 161 | } 162 | } 163 | `, 164 | { 165 | limit: 20, 166 | }, 167 | ) 168 | ``` 169 | 170 | Note that we use the `gql` tag here only for syntax highlighting in supported editors like VS Code, it's completely optional. 171 | 172 | ### Module Alias 173 | 174 | When your project has a deep nested folder structure, you might run into _import hell_: 175 | 176 | ```js 177 | import { client } from '../../mordred/graphql' 178 | ``` 179 | 180 | To simplify the import path, you can use `paths` option in `tsconfig.json`: 181 | 182 | ```json 183 | { 184 | "compilerOptions": { 185 | "baseUrl": ".", 186 | "paths": { 187 | "mordred-graphql": ["./mordred/graphql"] 188 | } 189 | } 190 | } 191 | ``` 192 | 193 | Now you can import form `mordred-graphql` instead. 194 | 195 | Note that Next.js supports `paths` by default, but if you're using other tools which don't support this, you might find [alias-hq](https://github.com/davestewart/alias-hq) helpful. 196 | 197 | ### Exploring Data with GraphiQL 198 | 199 | You can create an API at `/api/graphql` to explore data via GraphiQL: 200 | 201 | ```js 202 | import express from 'express' 203 | import graphqlHTTP from 'express-graphql' 204 | import { schema } from '../../mordred/graphql' 205 | 206 | const app = express() 207 | 208 | app.use( 209 | graphqlHTTP({ 210 | schema, 211 | graphiql: true, 212 | }), 213 | ) 214 | 215 | export default app 216 | ``` 217 | 218 | ## Plugin List 219 | 220 | - [mordred-source-filesystem](/packages/mordred-source-filesystem) 221 | - [mordred-transformer-markdown](/packages/mordred-transformer-markdown) 222 | 223 | ## License 224 | 225 | MIT © [EGOIST (Kevin Titor)](https://github.com/sponsors/egoist) 226 | --------------------------------------------------------------------------------