Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
import type { OyuConfig } from './types';

// Config handlers will go here
export * from './types';

/**
* Type-assisted config builder
*
* @todo Verify that the config is valid with joi and throw errors if not
* @param config oyu instance options
* @returns oyu config
*/
const defineConfig = (config: OyuConfig): OyuConfig => config;

export default defineConfig;
12 changes: 12 additions & 0 deletions packages/config/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Transformer } from '@oyu/core';

/**
* Oyu's configuration interface.
*
* @todo This needs to be typed more strictly.
*/
export interface OyuConfig {
source: any;
transformer: Transformer;
content: [];
}
10 changes: 8 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@
"*.d.ts"
],
"dependencies": {
"graphql-compose": "^9.0.4"
"cache-manager": "^3.6.0",
"graphql-compose": "^9.0.4",
"graphql-compose-json": "^6.1.0",
"lru-cache": "^6.0.0"
},
"devDependencies": {
"@types/cache-manager": "^3.4.2",
"@types/lru-cache": "^5.1.1",
"@types/node": "^16.11.7",
"tsup": "^5.6.0",
"typescript": "^4.4.4"
"typescript": "^4.4.4",
"vfile": "^5.2.0"
}
}
27 changes: 27 additions & 0 deletions packages/core/src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import LRU from 'lru-cache';
import crypto from 'crypto';

const cache = new LRU(1000);

export function getCache() {
return cache;
}

export function addToCache(key: string, value: any) {
cache.set(key, value);
}

export function getFromCache(key: string) {
return cache.get(key);
}

export function createCacheKey(
attribute: string,
nodeType: string,
uid: string
) {
// This should include serialized node contents to ensure that we don't hit old cache entries
const key = `${nodeType}:${uid}:${attribute}`;

return crypto.createHash('md5').update(key).digest('hex');
}
30 changes: 30 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,32 @@
import { OyuJsonNode } from './types';

// core logic will go here
export * from './types';
export * from './interfaces/transformer';

// const summarizeTypeShape = (nodes: OyuJsonNode[]): Record<string, any> => {
// const schema = nodes.reduce((acc, node) => {
// Object.keys(arr);
// }, {});
// return;
// };

// import {schemaComposer} from 'graphql-compose';

// const AuthorTC = schemaComposer.createObjectTC({
// name: 'Author',
// fields: {
// id: 'Int!',
// firstName: 'String',
// lastName: 'String',
// posts: {
// type: () => [PostTC], // arrow function for `type` helps to solve hoisting problems and keep ability to list all fields
// args: {
// limit: { type: 'Int', defaultValue: 20 },
// skip: 'Int', // shortand to `{ type: 'Int' }`
// sort: `enum AuthorPostsSortEnum { ASC DESC }`, // type creation via SDL
// },
// resolve: () => { ... },
// }
// },
// });
28 changes: 28 additions & 0 deletions packages/core/src/interfaces/transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { VFile } from 'vfile';

/**
* Converts input to meaningful data.
* To be used as a helper layer on top of a source that is not directly usable.
* For example, a markdown file.
*/
export type Transformer = <Config>(config: Config) => {
/**
* Parse the given source file into data and an unnormalized representation of the content.
* @param input Node to transform
* @param config Options for the transformation
*/
parse?: (input: VFile) => EntryNode;
preknownSchemaFragments: () => Record<string, any>;
inspect: (input: EntryNode) => string;
};

/**
* A representation of the content of a file.
*/
export interface EntryNode {
internals: {
path: string;
[key: string]: any;
};
fields: Record<string, any>;
}
7 changes: 7 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,10 @@ export interface OyuJsonNode {
timeToRead: number;
content: string;
}

export interface OyuJsonTypeMap {
name: string;
fields: {
[key: string]: string | Record<string, any>;
};
}
1 change: 1 addition & 0 deletions packages/oyu/content/authors/me.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
name: Tony
id: '2a3e'
enjoys:
- cats
- tea
Expand Down
60 changes: 30 additions & 30 deletions packages/oyu/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import getNodesFromDirectory from '@oyu/source-filesystem';
import { nodeToJSON } from '@oyu/transformer-markdown';
// import github from 'remark-github';
// import getNodesFromDirectory from '@oyu/source-filesystem';
// import { nodeToJSON } from '@oyu/transformer-markdown';
// // import github from 'remark-github';

import type { TransformerConfig } from '@oyu/transformer-markdown';
import type { VFile } from 'vfile';
// import type { TransformerConfig } from '@oyu/transformer-markdown';
// import type { VFile } from 'vfile';

const nodes = await getNodesFromDirectory('content/books');
// const nodes = await getNodesFromDirectory('content/authors');

const defaultConfig: TransformerConfig = {
markdown: {
gfm: true,
externalLinks: true,
// remarkPlugins: [github],
},
};
// const defaultConfig: TransformerConfig = {
// markdown: {
// gfm: true,
// externalLinks: true,
// // remarkPlugins: [github],
// },
// };

/**
* Convert an iterable set of content to an array of objects
*
* @param nodes array of content files
* @returns array of JSON-transformed content files
*/
async function convertNodesToJSON(
nodes: Promise<VFile>[]
): Promise<Record<any, any>> {
let data = [];
for (let node of nodes) {
data.push(await nodeToJSON(await node, defaultConfig));
}
return data;
}
const result = await convertNodesToJSON(nodes);
console.log(result);
// /**
// * Convert an iterable set of content to an array of objects
// *
// * @param nodes array of content files
// * @returns array of JSON-transformed content files
// */
// async function convertNodesToJSON(
// nodes: Promise<VFile>[]
// ): Promise<Record<any, any>> {
// let data = [];
// for (let node of nodes) {
// data.push(await nodeToJSON(await node, defaultConfig));
// }
// return data;
// }
// const result = await convertNodesToJSON(nodes);
// console.log(result);
49 changes: 39 additions & 10 deletions packages/source-filesystem/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import type { VFile } from 'vfile';
* Get filenames from a directory of files that match
* the expected file extensions.
*
* @param dir The directory to read from
* @param path The directory to read from
* @param extensions File extensions to parse
* @returns A list of content nodes' filenames
*/
export async function getValidNodesFilenames(
dir: string,
async function getValidNodesFilenames(
path: string,
extensions?: string[]
): Promise<string[]> {
// If no extensions are provided, use the default ones
const validExtensions = extensions ?? ['.md', '.mdx', '.markdown'];

const files = await fs.readdir(join(process.cwd(), dir));
const files = await fs.readdir(join(process.cwd(), path));
return files.filter((f) =>
validExtensions.includes(extname(f).toLowerCase())
);
Expand All @@ -29,15 +29,44 @@ export async function getValidNodesFilenames(
/**
* Get nodes (files) from the directory
*
* @param dir The directory to read from
* @param path The directory to read from
* @returns An array of content nodes
*/
export default async function getNodesFromDirectory(
dir: string
): Promise<Promise<VFile>[]> {
const slugs: string[] = await getValidNodesFilenames(dir);
async function getNodesFromDirectory(path: string): Promise<Promise<VFile>[]> {
const slugs: string[] = await getValidNodesFilenames(path);

return slugs.map(
async (slug: string): Promise<VFile> => await read(join(dir, slug))
async (slug: string): Promise<VFile> => await read(join(path, slug))
);
}

/**
* Returns all nodes from the directory
*
* @param paths array of directories to read from
* @returns
*/
function getAllNodes(
allContentTypes: Record<string, any>[]
): Record<string, []> {
return Object.fromEntries(
allContentTypes.map((contentType) => [
contentType.typeName,
getNodesFromDirectory(contentType.path),
])
);
}

/**
* Source filesystem plugin for fetching flat-file content nodes from directories on disk.
*
* @param config content types config
* @returns A function that returns functions which fetch lists of nodes
*/
const source = (config?: Record<string, any>) => ({
fetchByType: (path: string) => getNodesFromDirectory(path),
fetch: (allContentTypes: Record<string, any>[]) =>
getAllNodes(allContentTypes),
});

export default source;
2 changes: 2 additions & 0 deletions packages/transformer-markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"dependencies": {
"@sindresorhus/slugify": "^2.1.0",
"graphql": "16.0.1",
"gray-matter": "^4.0.3",
"lodash-es": "^4.17.21",
"rehype-raw": "^6.1.0",
Expand All @@ -59,6 +60,7 @@
"devDependencies": {
"@oyu/core": "workspace:^0.0.2",
"@types/node": "^16.11.7",
"@types/sanitize-html": "^2.5.0",
"tsup": "^5.6.0",
"typescript": "^4.4.4",
"vfile": "^5.2.0"
Expand Down
75 changes: 75 additions & 0 deletions packages/transformer-markdown/src/graphql/schema-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
createExcerpt,
estimateTimeToRead,
transformContentToHTML,
} from '../processors';

import { GraphQLInt, GraphQLString } from 'graphql';
import sanitizeHtml from 'sanitize-html';
import { MarkdownTransformerConfig } from '../types';

/**
* A GraphQL field for the time to read the content, if it exists.
*/
export const timeToRead = {
type: GraphQLInt,
args: {
speed: {
type: GraphQLInt,
description: 'The reading speed in words per minute',
defaultValue: 230,
},
},
resolve: (node: any, args: { speed: number }) =>
estimateTimeToRead(node.fields.content, args.speed),
};

/**
* A GraphQL field for an excerpt of the content, if it exists.
*/
export const excerpt = {
type: GraphQLString,
args: {
length: {
type: GraphQLInt,
description: 'The length of the excerpt in words',
defaultValue: 200,
},
},
resolve: async (
node: any,
args: { length: number },
config: MarkdownTransformerConfig
) => {
if (!node.fields.content.html) {
node.fields.content.html = await transformContentToHTML(
node.fields.content.raw,
config.markdown ?? {}
);
}

const plaintext = node.fields.content
? sanitizeHtml(node.fields.content.html, {
allowedAttributes: {},
allowedTags: [],
}).replace(/\r?\n|\r/g, ' ')
: '';
return createExcerpt(plaintext, args.length);
},
};

/**
* A GraphQL field for the content as HTML, if it exists.
*/
export const html = {
type: GraphQLString,
resolve: async (node: any, config: MarkdownTransformerConfig) => {
if (!node.fields.content.html) {
node.fields.content.html = await transformContentToHTML(
node.fields.content.raw,
config.markdown ?? {}
);
}
return node.fields.content.html;
},
};
Loading