Skip to content

Commit

Permalink
Change view module resolution (#7805)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 14, 2022
1 parent 9369e25 commit ff3aedf
Show file tree
Hide file tree
Showing 36 changed files with 83 additions and 126 deletions.
5 changes: 5 additions & 0 deletions .changeset/views-do-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': major
---

Changes field `.views` module resolution, from a path, to a module path that is resolved from where `keystone start` is run
10 changes: 5 additions & 5 deletions docs/pages/docs/apis/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Options:
See the [Hooks API](./hooks) for full details on the available hook options.
- `label`: The label displayed for this field in the Admin UI. Defaults to a human readable version of the field name.
- `ui`: Controls how the field is displayed in the Admin UI.
- `views`: A [resolved](https://nodejs.org/api/modules.html#modules_require_resolve_request_options) path to a module containing code to replace or extend the default Admin UI components for this field. See the [Custom Field Views](../guides/custom-field-views) guide for details on how to use this option.
- `views`: A module path that is resolved from where `keystone start` is run, resolving to a module containing code to replace or extend the Admin UI components for this field. See the [Custom Field Views](../guides/custom-field-views) guide for details on how to use this option.
- `createView.fieldMode` (default: `'edit'`): Controls the create view page of the Admin UI.
Can be one of `['edit', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['edit', 'hidden']`.
Defaults to the list's `ui.createView.defaultFieldMode` config if defined.
Expand Down Expand Up @@ -109,7 +109,7 @@ export default config({
hooks: { /* ... */ },
label: '...',
ui: {
views: require.resolve('path/to/viewsModule.tsx'),
views: './path/to/viewsModule',
createView: {
fieldMode: ({ session, context }) => 'edit',
},
Expand Down Expand Up @@ -1025,12 +1025,12 @@ export default config({
## Related resources

{% related-content %}
{% well
{% well
heading="Schema API Reference"
href="/docs/apis/schema" %}
The API to configure your options used with the `list()` function.
The API to configure your options used with the `list()` function.
{% /well %}
{% well
{% well
heading="GraphQL API Reference"
href="/docs/apis/graphql" %}
A complete CRUD (create, read, update, delete) GraphQL API derived from the list and field names you configure in your system.
Expand Down
8 changes: 5 additions & 3 deletions docs/pages/docs/guides/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const myInt =
orderBy: { arg: graphql.arg({ type: orderDirectionEnum }) },
},
output: graphql.field({ type: graphql.Int }),
views: require.resolve('./view.tsx'),
views: './view',
});
```

Expand Down Expand Up @@ -115,8 +115,10 @@ output: graphql.field({

The frontend portion of a field must be in a seperate file that the backend implementation points to with the `views` option.

The `views` option is resolved as though it is an import from some file in the project directory.

```
views: require.resolve('./view.tsx'),
views: './view',
```

#### Controller
Expand Down Expand Up @@ -217,7 +219,7 @@ export const CardValue: CardValueComponent = ({ item, field }) => {
## Related resources

{% related-content %}
{% well
{% well
heading="Example Project: Custom Fields"
href="https://github.com/keystonejs/keystone/tree/main/examples/custom-field"
target="_blank" %}
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/docs/guides/document-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export default config({
fields: {
fieldName: document({
ui: {
views: require.resolve('./component-blocks')
views: './component-blocks'
},
componentBlocks,
}),
Expand Down
3 changes: 0 additions & 3 deletions examples/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,6 @@ export const lists: Lists = {
Post: list({
fields: {
title: text({ access: {} }),
// TODO: expand this out into a proper example project
// Enable this line to test custom field views
// test: text({ ui: { views: require.resolve('./admin/fieldViews/Test.tsx') } }),
status: select({
options: [
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field-view/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ In this project we add a new JSON field to the `Task` list:
```typescript
relatedLinks: json({
ui: {
views: require.resolve('./fields/related-links/components.tsx'),
views: './fields/related-links/components',
createView: { fieldMode: 'edit' },
listView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'edit' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field-view/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const lists = {
// We've added a json field which implements custom views in the Admin UI
relatedLinks: json({
ui: {
views: require.resolve('./fields/related-links/components.tsx'),
views: './fields/related-links/components',
createView: { fieldMode: 'edit' },
listView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'edit' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field/1-text-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function text<ListTypeInfo extends BaseListTypeInfo>({
return value;
},
}),
views: require.resolve('./views.tsx'),
views: './1-text-field/views',
getAdminMeta() {
return {};
},
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field/2-stars-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const stars =
return value;
},
}),
views: require.resolve('./views.tsx'),
views: './2-stars-field/views',
getAdminMeta() {
return { maxStars };
},
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field/3-pair-field/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export function pair<ListTypeInfo extends BaseListTypeInfo>({
return resolveOutput(value);
},
}),
views: require.resolve('./views.tsx'),
views: './3-pair-field/views',
getAdminMeta() {
return {};
},
Expand Down
3 changes: 1 addition & 2 deletions packages/cloudinary/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import path from 'path';
import {
CommonFieldConfig,
BaseListTypeInfo,
Expand Down Expand Up @@ -172,7 +171,7 @@ export const cloudinaryImage =
};
},
}),
views: path.join(path.dirname(__dirname), 'views'),
views: '@keystone-6/cloudinary/views',
},
{
map: config.db?.map,
Expand Down
25 changes: 24 additions & 1 deletion packages/core/src/admin-ui/system/createAdminMeta.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'path';
import { GraphQLString, isInputObjectType } from 'graphql';
import { KeystoneConfig, AdminMetaRootVal, QueryMode } from '../../types';
import { humanize } from '../../lib/utils';
Expand Down Expand Up @@ -128,11 +129,19 @@ export function createAdminMeta(
`The ui.searchFields option on the ${key} list includes '${fieldKey}' but that field doesn't have a contains filter that accepts a GraphQL String`
);
}
assertValidView(
field.views,
`The \`views\` on the implementation of the field type at lists.${key}.fields.${fieldKey}`
);
adminMetaRoot.listsByKey[key].fields.push({
label: field.label ?? humanize(fieldKey),
description: field.ui?.description ?? null,
viewsIndex: getViewId(field.views),
customViewsIndex: field.ui?.views === undefined ? null : getViewId(field.ui.views),
customViewsIndex:
field.ui?.views === undefined
? null
: (assertValidView(field.views, `lists.${key}.fields.${fieldKey}.ui.views`),
getViewId(field.ui.views)),
fieldMeta: null,
path: fieldKey,
listKey: key,
Expand All @@ -158,3 +167,17 @@ export function createAdminMeta(

return adminMetaRoot;
}

function assertValidView(view: string, location: string) {
if (view.includes('\\')) {
throw new Error(
`${location} contains a backslash, which is invalid. You need to use a module path that is resolved from where 'keystone start' is run (see https://github.com/keystonejs/keystone/pull/7805)`
);
}

if (path.isAbsolute(view)) {
throw new Error(
`${location} is an absolute path, which is invalid. You need to use a module path that is resolved from where 'keystone start' is run (see https://github.com/keystonejs/keystone/pull/7805)`
);
}
}
9 changes: 1 addition & 8 deletions packages/core/src/admin-ui/system/generateAdminUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,7 @@ export const generateAdminUI = async (

// Write out the built-in admin UI files. Don't overwrite any user-defined pages.
const configFileExists = getDoesAdminConfigExist();
let adminFiles = writeAdminFiles(
config,
graphQLSchema,
adminMeta,
configFileExists,
projectAdminPath,
isLiveReload
);
let adminFiles = writeAdminFiles(config, graphQLSchema, adminMeta, configFileExists);

// Add files to pages/ which point to any files which exist in admin/pages
const adminConfigDir = Path.join(process.cwd(), 'admin');
Expand Down
48 changes: 14 additions & 34 deletions packages/core/src/admin-ui/templates/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Path from 'path';
import hashString from '@emotion/hash';
import {
executeSync,
Expand All @@ -12,16 +11,14 @@ import {
} from 'graphql';
import { AdminMetaRootVal } from '../../types';
import { staticAdminMetaQuery, StaticAdminMetaQuery } from '../admin-meta-graphql';
import { serializePathForImport } from '../utils/serializePathForImport';

type AppTemplateOptions = { configFileExists: boolean; projectAdminPath: string };
type AppTemplateOptions = { configFileExists: boolean };

export const appTemplate = (
adminMetaRootVal: AdminMetaRootVal,
graphQLSchema: GraphQLSchema,
{ configFileExists, projectAdminPath }: AppTemplateOptions,
apiPath: string,
isLiveReload: boolean
{ configFileExists }: AppTemplateOptions,
apiPath: string
) => {
const result = executeSync({
document: staticAdminMetaQuery,
Expand All @@ -34,34 +31,17 @@ export const appTemplate = (
const { adminMeta } = result.data!.keystone;
const adminMetaQueryResultHash = hashString(JSON.stringify(adminMeta));

const allViews = adminMetaRootVal.views.map(views => {
// webpack/next for some reason _sometimes_ adds a query parameter to the return of require.resolve
// because it does it _sometimes_, we have to remove it so that during live reloading
// we're not constantly doing builds because the query param is there and then it's not and then it is and so on
views = views.replace(/\?[A-Za-z0-9]+$/, '');
// webpack/next adds (api)/ to the return of require.resolve
views = views.replace(/^\(api\)\//, '');
// during a live reload, we'll have paths from a webpack compilation which will make the paths
// that __dirname/__filename/require.resolve return relative to the webpack's "context" option
// which for Next, it's set to the directory of the Next project which is projectAdminPath here.
// so to get absolute paths, we need to resolve them relative to the projectAdminPath
// generally though, relative paths are problematic because
// we don't know where to resolve them from so we disallow them
// we're assuming that the relative paths we get
// of course, this isn't necessarily true but it's kinda the best we can do
// this means that if someone writes a relative path as a view during live reloading
// they'll get a more confusing error than they would get at startup
if (isLiveReload) {
views = Path.resolve(projectAdminPath, views);
} else if (!Path.isAbsolute(views)) {
throw new Error(
`Field views must be absolute paths, but ${JSON.stringify(
views
)} was provided. Use path.join(__dirname, './relative/path') or require.resolve('./relative/path') to get an absolute path.`
);
}
const viewPath = Path.relative(Path.join(projectAdminPath, 'pages'), views);
return serializePathForImport(viewPath);
const allViews = adminMetaRootVal.views.map(viewRelativeToProject => {
const isRelativeToFile =
viewRelativeToProject.startsWith('./') || viewRelativeToProject.startsWith('../');
const viewRelativeToAppFile = isRelativeToFile
? '../../../' + viewRelativeToProject
: viewRelativeToProject;

// we're not using serializePathForImport here because we want the thing you write for a view
// to be exactly what you would put in an import in the project directory.
// we're still using JSON.stringify to escape anything that might need to be though
return JSON.stringify(viewRelativeToAppFile);
});
// -- TEMPLATE START
return `import { getApp } from '@keystone-6/core/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App';
Expand Down
9 changes: 3 additions & 6 deletions packages/core/src/admin-ui/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ export const writeAdminFiles = (
config: KeystoneConfig,
graphQLSchema: GraphQLSchema,
adminMeta: AdminMetaRootVal,
configFileExists: boolean,
projectAdminPath: string,
isLiveReload: boolean
configFileExists: boolean
): AdminFileToWrite[] => {
if (
config.experimental?.enableNextJsGraphqlApiEndpoint &&
Expand All @@ -44,9 +42,8 @@ export const writeAdminFiles = (
src: appTemplate(
adminMeta,
graphQLSchema,
{ configFileExists, projectAdminPath },
config.graphql?.path || '/api/graphql',
isLiveReload
{ configFileExists },
config.graphql?.path || '/api/graphql'
),
outputPath: 'pages/_app.js',
},
Expand Down
5 changes: 0 additions & 5 deletions packages/core/src/fields/resolve-view.ts

This file was deleted.

3 changes: 1 addition & 2 deletions packages/core/src/fields/types/bigInt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
} from '../../non-null-graphql';
import { resolveView } from '../../resolve-view';

export type BigIntFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
CommonFieldConfig<ListTypeInfo> & {
Expand Down Expand Up @@ -173,7 +172,7 @@ export const bigInt =
output: graphql.field({
type: config.graphql?.read?.isNonNull ? graphql.nonNull(graphql.BigInt) : graphql.BigInt,
}),
views: resolveView('bigInt/views'),
views: '@keystone-6/core/fields/types/bigInt/views',
getAdminMeta() {
return {
validation: {
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/fields/types/calendarDay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
assertReadIsNonNullAllowed,
getResolvedIsNullable,
} from '../../non-null-graphql';
import { resolveView } from '../../resolve-view';
import { CalendarDayFieldMeta } from './views';

export type CalendarDayFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
Expand Down Expand Up @@ -143,7 +142,7 @@ export const calendarDay =
return value;
},
}),
views: resolveView('calendarDay/views'),
views: '@keystone-6/core/fields/types/calendarDay/views',
getAdminMeta(): CalendarDayFieldMeta {
return {
defaultValue: defaultValue ?? null,
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/fields/types/checkbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '../../../types';
import { graphql } from '../../..';
import { assertCreateIsNonNullAllowed, assertReadIsNonNullAllowed } from '../../non-null-graphql';
import { resolveView } from '../../resolve-view';

export type CheckboxFieldConfig<ListTypeInfo extends BaseListTypeInfo> =
CommonFieldConfig<ListTypeInfo> & {
Expand Down Expand Up @@ -72,7 +71,7 @@ export const checkbox =
output: graphql.field({
type: config.graphql?.read?.isNonNull ? graphql.nonNull(graphql.Boolean) : graphql.Boolean,
}),
views: resolveView('checkbox/views'),
views: '@keystone-6/core/fields/types/checkbox/views',
getAdminMeta: () => ({ defaultValue }),
});
};
3 changes: 1 addition & 2 deletions packages/core/src/fields/types/decimal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
FieldData,
} from '../../../types';
import { graphql } from '../../..';
import { resolveView } from '../../resolve-view';
import {
assertCreateIsNonNullAllowed,
assertReadIsNonNullAllowed,
Expand Down Expand Up @@ -183,7 +182,7 @@ export const decimal =
return val;
},
}),
views: resolveView('decimal/views'),
views: '@keystone-6/core/fields/types/decimal/views',
getAdminMeta: (): import('./views').DecimalFieldMeta => ({
defaultValue: defaultValue ?? null,
precision,
Expand Down
3 changes: 1 addition & 2 deletions packages/core/src/fields/types/file/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
FileMetadata,
} from '../../../types';
import { graphql } from '../../..';
import { resolveView } from '../../resolve-view';

export type FileFieldConfig<ListTypeInfo extends BaseListTypeInfo> = {
storage: string;
Expand Down Expand Up @@ -115,6 +114,6 @@ export const file =
return { filename, filesize, storage: config.storage };
},
}),
views: resolveView('file/views'),
views: '@keystone-6/core/fields/types/file/views',
});
};
Loading

0 comments on commit ff3aedf

Please sign in to comment.