...
 
Commits (7)
{
"plugins": [
["minify-replace", {
"replacements": [{
"identifierName": "__DEV__",
"replacement": {
"type": "booleanLiteral",
"value": true
}
}]
}],
["./scripts/babel/invariant", {
"strip": false,
}],
"relay",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties",
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "10.13.0",
},
},
],
"@babel/preset-flow",
"@babel/preset-react"
]
}
module.exports = function(api) {
api.cache(false);
return {
plugins: [
[
'minify-replace',
{
replacements: [
{
identifierName: '__DEV__',
replacement: {
type: 'booleanLiteral',
value: true,
},
},
],
},
],
[
'./scripts/babel/invariant',
{
strip: false,
},
],
'relay',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
presets: [
[
'@babel/preset-env',
{
targets: {
node: '10.13.0',
},
},
],
'@babel/preset-flow',
'@babel/preset-react',
],
};
};
......@@ -33,19 +33,22 @@ const babelOptions = {
},
],
'relay',
'transform-object-rest-spread',
'transform-class-properties',
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-nullish-coalescing-operator',
],
presets: [
[
'env',
'@babel/preset-env',
{
targets: {
node: '10.13.0',
},
},
],
'react',
'@babel/preset-flow',
'@babel/preset-react',
],
};
......
......@@ -146,6 +146,9 @@ type Post implements Node & Tagged & Versioned {
title: String
body: Markup!
"""Estimate of time necessary to read the post, in minutes"""
readTime: Int!
"""Succinct summary of the post content"""
description: String
......
......@@ -1220,6 +1220,22 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "readTime",
"description": "Estimate of time necessary to read the post, in minutes",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Succinct summary of the post content",
......
......@@ -11,19 +11,21 @@
* LICENSE file in the root directory of this source tree.
*
* @flow
* @providesModule RelayCompilerBin
* @format
*/
'use strict';
const CodegenRunner = require('relay-compiler/lib/CodegenRunner');
const ConsoleReporter = require('relay-compiler/lib/GraphQLConsoleReporter');
const RelayFileWriter = require('relay-compiler/lib/RelayFileWriter');
const RelayIRTransforms = require('relay-compiler/lib/RelayIRTransforms');
const RelayJSModuleParser = require('relay-compiler/lib/RelayJSModuleParser');
const WatchmanClient = require('relay-compiler/lib/GraphQLWatchmanClient');
const formatGeneratedModule = require('relay-compiler/lib/formatGeneratedModule');
const {
CodegenRunner,
ConsoleReporter,
DotGraphQLParser,
} = require('graphql-compiler');
const RelaySourceModuleParser = require('relay-compiler/lib/RelaySourceModuleParser');
const RelayFileWriter = require('relay-compiler').FileWriter;
const RelayIRTransforms = require('relay-compiler').IRTransforms;
const RelayLanguagePluginJavaScript = require('relay-compiler/lib/RelayLanguagePluginJavaScript');
const fs = require('fs');
const path = require('path');
......@@ -35,6 +37,7 @@ const {
} = require('graphql');
const {
commonTransforms,
codegenTransforms,
fragmentTransforms,
printTransforms,
......@@ -42,7 +45,12 @@ const {
schemaExtensions,
} = RelayIRTransforms;
import type {GetWriterOptions} from 'graphql-compiler';
import type {GraphQLSchema} from 'graphql';
import type {
PluginInitializer,
PluginInterface,
} from '../language/RelayLanguagePluginInterface';
function persistQuery(text: string): Promise<string> {
const match = text.match(/^\s*query\s+(\w+Query)\b/);
......@@ -59,18 +67,26 @@ function persistQuery(text: string): Promise<string> {
return Promise.resolve(name);
}
function buildWatchExpression() {
return [
'allof',
['type', 'f'],
['anyof', ['suffix', 'js']],
['not', ['match', '**/node_modules/**', 'wholename']],
['not', ['match', '**/__mocks__/**', 'wholename']],
['not', ['match', '**/__tests__/**', 'wholename']],
['not', ['match', '**/__generated__/**', 'wholename']],
];
function getFilepathsFromGlob(
baseDir,
options: {
extensions: Array<string>,
include: Array<string>,
exclude: Array<string>,
},
): Array<string> {
const {extensions, include, exclude} = options;
const patterns = include.map(inc => `${inc}/*.+(${extensions.join('|')})`);
const glob = require('fast-glob');
return glob.sync(patterns, {
cwd: baseDir,
ignore: exclude,
});
}
type LanguagePlugin = PluginInitializer | {default: PluginInitializer};
async function run() {
const schemaPath = path.resolve(process.cwd(), 'schema.graphql');
if (!fs.existsSync(schemaPath)) {
......@@ -80,24 +96,80 @@ async function run() {
if (!fs.existsSync(srcDir)) {
throw new Error(`source path does not exist: ${srcDir}.`);
}
const reporter = new ConsoleReporter({verbose: true});
const reporter = new ConsoleReporter({
verbose: true,
quiet: false,
});
const exclude = [
'**/node_modules/**',
'**/__mocks__/**',
'**/__tests__/**',
'**/__generated__/**',
];
const include = ['**'];
const schema = getSchema(schemaPath);
const languagePlugin = RelayLanguagePluginJavaScript();
const inputExtensions = languagePlugin.inputExtensions;
const outputExtension = languagePlugin.outputExtension;
const sourceParserName = inputExtensions.join('/');
const sourceWriterName = outputExtension;
const sourceModuleParser = RelaySourceModuleParser(
languagePlugin.findGraphQLTags,
);
const artifactDirectory = null;
const generatedDirectoryName = artifactDirectory || '__generated__';
const sourceSearchOptions = {
extensions: inputExtensions,
include,
exclude: ['**/*.graphql.*', ...exclude], // Do not include artifacts
};
const graphqlSearchOptions = {
extensions: ['graphql'],
include,
exclude: [path.relative(srcDir, schemaPath)].concat(exclude),
};
const parserConfigs = {
default: {
[sourceParserName]: {
baseDir: srcDir,
getFileFilter: sourceModuleParser.getFileFilter,
getParser: sourceModuleParser.getParser,
getSchema: () => schema,
watchmanExpression: null,
filepaths: getFilepathsFromGlob(srcDir, sourceSearchOptions),
},
graphql: {
baseDir: srcDir,
getFileFilter: RelayJSModuleParser.getFileFilter,
getParser: RelayJSModuleParser.getParser,
getSchema: () => getSchema(schemaPath),
watchmanExpression: buildWatchExpression(),
filepaths: null,
getParser: DotGraphQLParser.getParser,
getSchema: () => schema,
watchmanExpression: null,
filepaths: getFilepathsFromGlob(srcDir, graphqlSearchOptions),
},
};
const writerConfigs = {
default: {
getWriter: getRelayFileWriter(srcDir, persistQuery),
[sourceWriterName]: {
getWriter: getRelayFileWriter(
srcDir,
languagePlugin,
false,
artifactDirectory,
persistQuery,
),
isGeneratedFile: (filePath: string) =>
filePath.endsWith('.js') && filePath.includes('__generated__'),
parser: 'default',
filePath.endsWith('.graphql.' + outputExtension) &&
filePath.includes(generatedDirectoryName),
parser: sourceParserName,
baseParsers: ['graphql'],
},
};
const codegenRunner = new CodegenRunner({
......@@ -105,8 +177,11 @@ async function run() {
parserConfigs,
writerConfigs,
onlyValidate: false,
// TODO: allow passing in a flag or detect?
sourceControl: null,
});
const result = await codegenRunner.compileAll();
if (result === 'ERROR') {
process.exit(100);
}
......@@ -114,26 +189,46 @@ async function run() {
function getRelayFileWriter(
baseDir: string,
languagePlugin: PluginInterface,
noFutureProofEnums: boolean,
outputDir?: ?string,
persistQuery: (text: string) => Promise<string>,
) {
return (onlyValidate, schema, documents, baseDocuments) =>
return ({
onlyValidate,
schema,
documents,
baseDocuments,
sourceControl,
reporter,
}: GetWriterOptions) =>
new RelayFileWriter({
config: {
formatModule: formatGeneratedModule,
baseDir,
compilerTransforms: {
commonTransforms,
codegenTransforms,
fragmentTransforms,
printTransforms,
queryTransforms,
},
baseDir,
persistQuery,
customScalars: {},
formatModule: languagePlugin.formatModule,
inputFieldWhiteListForFlow: [],
schemaExtensions,
useHaste: false,
noFutureProofEnums,
extension: languagePlugin.outputExtension,
typeGenerator: languagePlugin.typeGenerator,
outputDir,
persistQuery,
},
onlyValidate,
schema,
baseDocuments,
documents,
reporter,
sourceControl,
});
}
......@@ -144,12 +239,12 @@ function getSchema(schemaPath: string): GraphQLSchema {
source = printSchema(buildClientSchema(JSON.parse(source).data));
}
source = `
directive @include(if: Boolean) on FRAGMENT | FIELD
directive @skip(if: Boolean) on FRAGMENT | FIELD
directive @include(if: Boolean) on FRAGMENT_SPREAD | FIELD
directive @skip(if: Boolean) on FRAGMENT_SPREAD | FIELD
${source}
`;
return buildASTSchema(parse(source));
return buildASTSchema(parse(source), {assumeValid: true});
} catch (error) {
throw new Error(
`
......
......@@ -9,6 +9,7 @@ const RESET = '\x1b[0m';
const YELLOW = '\x1b[33m';
const paths = [
'babel.config.js',
'gulpfile.babel.js',
'postcss.config.js',
'{scripts,src}/**/*.js',
......
......@@ -11,25 +11,14 @@ fragment ArticlesIndex on Root {
node {
id
...ArticlePreview
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on ArticleConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -11,25 +11,14 @@ fragment ArticlesIndex on Root {
node {
id
...ArticlePreview
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on ArticleConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -20,6 +20,7 @@ fragment Post on Post {
body {
html(baseHeadingLevel: $baseHeadingLevel)
}
readTime
...Tags
...When
}
......
......@@ -12,25 +12,14 @@ fragment PostsIndex on Root {
node {
id
...Post
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on PostConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......@@ -41,6 +30,7 @@ fragment Post on Post {
body {
html(baseHeadingLevel: $baseHeadingLevel)
}
readTime
...Tags
...When
}
......
......@@ -12,25 +12,14 @@ fragment PostsIndex on Root {
node {
id
...Post
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on PostConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......@@ -41,6 +30,7 @@ fragment Post on Post {
body {
html(baseHeadingLevel: $baseHeadingLevel)
}
readTime
...Tags
...When
}
......
......@@ -12,32 +12,17 @@ fragment Search on Root {
edges {
cursor
node {
__typename
...ContentPreview
... on Node {
id
}
__typename
}
}
pageInfo {
endCursor
hasNextPage
}
... on SearchResultConnection {
edges {
cursor
node {
__typename
... on Node {
id
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -12,32 +12,17 @@ fragment Search on Root {
edges {
cursor
node {
__typename
...ContentPreview
... on Node {
id
}
__typename
}
}
pageInfo {
endCursor
hasNextPage
}
... on SearchResultConnection {
edges {
cursor
node {
__typename
... on Node {
id
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -12,25 +12,14 @@ fragment SnippetsIndex on Root {
node {
id
...Snippet
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on SnippetConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -12,25 +12,14 @@ fragment SnippetsIndex on Root {
node {
id
...Snippet
__typename
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
... on SnippetConnection {
edges {
cursor
node {
__typename
id
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -19,32 +19,17 @@ fragment Tag on Tag {
edges {
cursor
node {
__typename
...ContentPreview
... on Node {
id
}
__typename
}
}
pageInfo {
endCursor
hasNextPage
}
... on TaggableConnection {
edges {
cursor
node {
__typename
... on Node {
id
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -22,32 +22,17 @@ fragment Tag on Tag {
edges {
cursor
node {
__typename
...ContentPreview
... on Node {
id
}
__typename
}
}
pageInfo {
endCursor
hasNextPage
}
... on TaggableConnection {
edges {
cursor
node {
__typename
... on Node {
id
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
......
......@@ -73,9 +73,9 @@ const api = {
readQuery(query, variables) {
const {
createOperationSelector,
getOperation,
getRequest,
} = environment.unstable_internal;
const operation = createOperationSelector(getOperation(query), variables);
const operation = createOperationSelector(getRequest(query), variables);
const snapshot = environment.lookup(
operation.fragment,
operation.variables,
......
.metadata {
color: #bbb;
font-style: italic;
margin-bottom: 1rem;
}
.metadata > *:after {
content: '\00b7'; /* middot */
margin: 0 1rem 0 1rem;
}
.metadata:last-child:after {
content: none;
}
.metadata .separator {
margin: 0 .75rem;
}
/**
* @flow
*/
import React from 'react';
import inBrowser from '../../common/inBrowser';
if (inBrowser) {
require('./Metadata.css');
}
export default function Metadata({children}) {
return (
<div className="metadata">
{children}
</div>
);
}
......@@ -7,6 +7,7 @@ import {createFragmentContainer, graphql} from 'react-relay';
import Link from './Link';
import TrustedPrerenderedMarkup from './TrustedPrerenderedMarkup';
import Tags from './Tags';
import Metadata from './Metadata';
import When from './When';
import type {Post as PostData} from './__generated__/Post.graphql';
......@@ -20,7 +21,10 @@ class Post extends React.Component<{data: PostData}> {
<h1>
<Link to={post.url}>{post.title}</Link>
</h1>
<When data={post} />
<Metadata>
<When data={post} />
<span>{post.readTime} minute read</span>
</Metadata>
<div>
<TrustedPrerenderedMarkup html={post.body.html} />
</div>
......@@ -40,6 +44,7 @@ export default createFragmentContainer(
body {
html(baseHeadingLevel: $baseHeadingLevel)
}
readTime
...Tags
...When
}
......
.when {
color: #bbb;
font-style: italic;
margin-bottom: 1rem;
}
.when:before {
background-image: url(../../../vendor/icons/calendar.svg);
background-size: cover;
......
......@@ -15,11 +15,11 @@ if (inBrowser) {
}
const WhenWrapper = ({children, url}) => (
<div className="when">
<span className="when">
<a className="when-link" href={url}>
{children}
</a>
</div>
</span>
);
class When extends React.Component<{data: WhenData}> {
......
......@@ -7,21 +7,31 @@
'use strict';
/*::
import type {ConcreteFragment} from 'relay-runtime';
import type { ConcreteFragment } from 'relay-runtime';
type Tags$ref = any;
type When$ref = any;
import type { FragmentReference } from "relay-runtime";
declare export opaque type Article$ref: FragmentReference;
export type Article = {|
+title: ?string;
+redirect: ?string;
+resolvedTitle: ?string;
+editURL: string;
+url: string;
+title: ?string,
+redirect: ?string,
+resolvedTitle: ?string,
+editURL: string,
+url: string,
+body: {|
+html: string;
|};
+html: string
|},
+$fragmentRefs: Tags$ref & When$ref,
+$refType: Article$ref,
|};
*/
const fragment /*: ConcreteFragment*/ = {
const node/*: ConcreteFragment*/ = {
"kind": "Fragment",
"name": "Article",
"type": "Article",
"metadata": null,
"argumentDefinitions": [
{
"kind": "RootArgument",
......@@ -29,56 +39,55 @@ const fragment /*: ConcreteFragment*/ = {
"type": "Int"
}
],
"kind": "Fragment",
"metadata": null,
"name": "Article",
"selections": [
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "title",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "redirect",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "resolvedTitle",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "editURL",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "url",
"args": null,
"storageKey": null
},
{
"kind": "LinkedField",
"alias": null,
"name": "body",
"storageKey": null,
"args": null,
"concreteType": "Markup",
"name": "body",
"plural": false,
"selections": [
{
"kind": "ScalarField",
"alias": null,
"name": "html",
"args": [
{
"kind": "Variable",
......@@ -87,11 +96,9 @@ const fragment /*: ConcreteFragment*/ = {
"type": "Int"
}
],
"name": "html",
"storageKey": null
}
],
"storageKey": null
]
},
{
"kind": "FragmentSpread",
......@@ -103,8 +110,8 @@ const fragment /*: ConcreteFragment*/ = {
"name": "When",
"args": null
}
],
"type": "Article"
]
};
module.exports = fragment;
// prettier-ignore
(node/*: any*/).hash = 'efe468bd4fb17dba34ac63638c11c782';
module.exports = node;
......@@ -7,40 +7,47 @@
'use strict';
/*::
import type {ConcreteFragment} from 'relay-runtime';
import type { ConcreteFragment } from 'relay-runtime';
type Tags$ref = any;
type When$ref = any;
import type { FragmentReference } from "relay-runtime";
declare export opaque type ArticlePreview$ref: FragmentReference;
export type ArticlePreview = {|
+description: ?string;
+title: ?string;
+url: string;
+description: ?string,
+title: ?string,
+url: string,
+$fragmentRefs: Tags$ref & When$ref,
+$refType: ArticlePreview$ref,
|};
*/
const fragment /*: ConcreteFragment*/ = {
"argumentDefinitions": [],
const node/*: ConcreteFragment*/ = {
"kind": "Fragment",
"metadata": null,
"name": "ArticlePreview",
"type": "Article",
"metadata": null,
"argumentDefinitions": [],
"selections": [
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "description",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "title",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "url",
"args": null,
"storageKey": null
},
{
......@@ -53,8 +60,8 @@ const fragment /*: ConcreteFragment*/ = {
"name": "When",
"args": null
}
],
"type": "Article"
]
};
module.exports = fragment;
// prettier-ignore
(node/*: any*/).hash = '5a336274060a204df8c9d87e903097a5';
module.exports = node;
......@@ -7,37 +7,32 @@
'use strict';
/*::
import type {ConcreteFragment} from 'relay-runtime';
import type { ConcreteFragment } from 'relay-runtime';
type ArticlePreview$ref = any;
import type { FragmentReference } from "relay-runtime";
declare export opaque type ArticlesIndex$ref: FragmentReference;
export type ArticlesIndex = {|
+articles: {|
+edges: ?$ReadOnlyArray<?{|
+node: ?{|
+id: string;
|};
|}>;
+id: string,
+$fragmentRefs: ArticlePreview$ref,
|}
|}>,
+pageInfo: {|
+endCursor: ?string;
+hasNextPage: boolean;
|};
|};
+endCursor: ?string,
+hasNextPage: boolean,
|},
|},
+$refType: ArticlesIndex$ref,
|};
*/
const fragment /*: ConcreteFragment*/ = {
"argumentDefinitions": [
{
"kind": "RootArgument",
"name": "count",
"type": "Int"
},
{
"kind": "RootArgument",
"name": "cursor",
"type": "String"
}
],
const node/*: ConcreteFragment*/ = {
"kind": "Fragment",
"name": "ArticlesIndex",
"type": "Root",
"metadata": {
"connection": [
{
......@@ -50,122 +45,105 @@ const fragment /*: ConcreteFragment*/ = {
}
]
},
"name": "ArticlesIndex",
"argumentDefinitions": [
{
"kind": "RootArgument",
"name": "count",
"type": "Int"
},
{
"kind": "RootArgument",
"name": "cursor",
"type": "String"
}
],
"selections": [
{
"kind": "LinkedField",
"alias": "articles",
"name": "__ArticlesIndex_articles_connection",
"storageKey": null,
"args": null,
"concreteType": "ArticleConnection",
"name": "__ArticlesIndex_articles_connection",
"plural": false,
"selections": [
{
"kind": "LinkedField",
"alias": null,
"name": "edges",
"storageKey": null,
"args": null,
"concreteType": "ArticleEdge",
"name": "edges",
"plural": true,
"selections": [
{
"kind": "LinkedField",
"alias": null,
"name": "node",
"storageKey": null,
"args": null,
"concreteType": "Article",
"name": "node",
"plural": false,
"selections": [
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "id",
"args": null,
"storageKey": null
},
{
"kind": "FragmentSpread",
"name": "ArticlePreview",
"args": null
},
{
"kind": "ScalarField",
"alias": null,
"name": "__typename",
"args": null,
"storageKey": null
}
],
]
},
{
"kind": "ScalarField",
"alias": null,
"name": "cursor",
"args": null,
"storageKey": null
}
],
"storageKey": null
]
},
{
"kind": "LinkedField",
"alias": null,
"name": "pageInfo",
"storageKey": null,
"args": null,
"concreteType": "PageInfo",
"name": "pageInfo",
"plural": false,
"selections": [
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "endCursor",
"args": null,
"storageKey": null
},
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "hasNextPage",
"storageKey": null
}
],
"storageKey": null
},
{
"kind": "InlineFragment",
"type": "ArticleConnection",
"selections": [
{
"kind": "LinkedField",
"alias": null,
"args": null,
"concreteType": "ArticleEdge",
"name": "edges",
"plural": true,
"selections": [
{
"kind": "ScalarField",
"alias": null,
"args": null,
"name": "cursor",
"storageKey": null
},
{
"kind": "LinkedField",
"alias": null,
"args": null,
"concreteType": "Article",
"name": "node",