Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change view module resolution #7805

Merged
merged 4 commits into from
Aug 25, 2022
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
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