From 667ee929d31e780c0f0ebc904dfae10bc3051d37 Mon Sep 17 00:00:00 2001 From: Vladislav Shkodin Date: Sun, 18 Apr 2021 15:02:24 +0300 Subject: [PATCH] refactor(core): migrate format helpers to ts (#5273) --- .../src/formats/{formats.js => formats.ts} | 28 +++- .../src/formats/frontmatter.js | 114 -------------- .../src/formats/frontmatter.ts | 146 ++++++++++++++++++ .../src/formats/{helpers.js => helpers.ts} | 8 +- .../src/formats/{json.js => json.ts} | 4 +- .../src/formats/{toml.js => toml.ts} | 12 +- .../src/formats/{yaml.js => yaml.ts} | 17 +- packages/netlify-cms-core/src/types/redux.ts | 6 +- .../src/types/tomlify-j0.4.d.ts | 13 ++ 9 files changed, 210 insertions(+), 138 deletions(-) rename packages/netlify-cms-core/src/formats/{formats.js => formats.ts} (71%) delete mode 100644 packages/netlify-cms-core/src/formats/frontmatter.js create mode 100644 packages/netlify-cms-core/src/formats/frontmatter.ts rename packages/netlify-cms-core/src/formats/{helpers.js => helpers.ts} (50%) rename packages/netlify-cms-core/src/formats/{json.js => json.ts} (65%) rename packages/netlify-cms-core/src/formats/{toml.js => toml.ts} (65%) rename packages/netlify-cms-core/src/formats/{yaml.js => yaml.ts} (69%) create mode 100644 packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts diff --git a/packages/netlify-cms-core/src/formats/formats.js b/packages/netlify-cms-core/src/formats/formats.ts similarity index 71% rename from packages/netlify-cms-core/src/formats/formats.js rename to packages/netlify-cms-core/src/formats/formats.ts index 00b4aba6..fdd0fbe1 100644 --- a/packages/netlify-cms-core/src/formats/formats.js +++ b/packages/netlify-cms-core/src/formats/formats.ts @@ -3,7 +3,15 @@ import { get } from 'lodash'; import yamlFormatter from './yaml'; import tomlFormatter from './toml'; import jsonFormatter from './json'; -import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter'; +import { + FrontmatterInfer, + frontmatterJSON, + frontmatterTOML, + frontmatterYAML, + Delimiter, +} from './frontmatter'; +import { Collection, EntryObject, Format } from '../types/redux'; +import { EntryValue } from '../valueObjects/Entry'; export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter']; @@ -28,7 +36,7 @@ export const extensionFormatters = { html: FrontmatterInfer, }; -function formatByName(name, customDelimiter) { +function formatByName(name: Format, customDelimiter?: Delimiter) { return { yml: yamlFormatter, yaml: yamlFormatter, @@ -41,11 +49,17 @@ function formatByName(name, customDelimiter) { }[name]; } -export function resolveFormat(collection, entry) { +function frontmatterDelimiterIsList( + frontmatterDelimiter?: Delimiter | List, +): frontmatterDelimiter is List { + return List.isList(frontmatterDelimiter); +} + +export function resolveFormat(collection: Collection, entry: EntryObject | EntryValue) { // Check for custom delimiter const frontmatter_delimiter = collection.get('frontmatter_delimiter'); - const customDelimiter = List.isList(frontmatter_delimiter) - ? frontmatter_delimiter.toArray() + const customDelimiter = frontmatterDelimiterIsList(frontmatter_delimiter) + ? (frontmatter_delimiter.toArray() as [string, string]) : frontmatter_delimiter; // If the format is specified in the collection, use that format. @@ -58,7 +72,9 @@ export function resolveFormat(collection, entry) { const filePath = entry && entry.path; if (filePath) { const fileExtension = filePath.split('.').pop(); - return get(extensionFormatters, fileExtension); + if (fileExtension) { + return get(extensionFormatters, fileExtension); + } } // If creating a new file, and an `extension` is specified in the diff --git a/packages/netlify-cms-core/src/formats/frontmatter.js b/packages/netlify-cms-core/src/formats/frontmatter.js deleted file mode 100644 index 539857bc..00000000 --- a/packages/netlify-cms-core/src/formats/frontmatter.js +++ /dev/null @@ -1,114 +0,0 @@ -import matter from 'gray-matter'; -import tomlFormatter from './toml'; -import yamlFormatter from './yaml'; -import jsonFormatter from './json'; - -const parsers = { - toml: { - parse: input => tomlFormatter.fromFile(input), - stringify: (metadata, { sortedKeys }) => tomlFormatter.toFile(metadata, sortedKeys), - }, - json: { - parse: input => { - let JSONinput = input.trim(); - // Fix JSON if leading and trailing brackets were trimmed. - if (JSONinput.substr(0, 1) !== '{') { - JSONinput = '{' + JSONinput + '}'; - } - return jsonFormatter.fromFile(JSONinput); - }, - stringify: (metadata, { sortedKeys }) => { - let JSONoutput = jsonFormatter.toFile(metadata, sortedKeys).trim(); - // Trim leading and trailing brackets. - if (JSONoutput.substr(0, 1) === '{' && JSONoutput.substr(-1) === '}') { - JSONoutput = JSONoutput.substring(1, JSONoutput.length - 1); - } - return JSONoutput; - }, - }, - yaml: { - parse: input => yamlFormatter.fromFile(input), - stringify: (metadata, { sortedKeys, comments }) => - yamlFormatter.toFile(metadata, sortedKeys, comments), - }, -}; - -function inferFrontmatterFormat(str) { - const firstLine = str.substr(0, str.indexOf('\n')).trim(); - if (firstLine.length > 3 && firstLine.substr(0, 3) === '---') { - // No need to infer, `gray-matter` will handle things like `---toml` for us. - return; - } - switch (firstLine) { - case '---': - return getFormatOpts('yaml'); - case '+++': - return getFormatOpts('toml'); - case '{': - return getFormatOpts('json'); - default: - console.warn('Unrecognized front-matter format.'); - } -} - -export function getFormatOpts(format) { - return { - yaml: { language: 'yaml', delimiters: '---' }, - toml: { language: 'toml', delimiters: '+++' }, - json: { language: 'json', delimiters: ['{', '}'] }, - }[format]; -} - -class FrontmatterFormatter { - constructor(format, customDelimiter) { - this.format = getFormatOpts(format); - this.customDelimiter = customDelimiter; - } - - fromFile(content) { - const format = this.format || inferFrontmatterFormat(content); - if (this.customDelimiter) this.format.delimiters = this.customDelimiter; - const result = matter(content, { engines: parsers, ...format }); - // in the absent of a body when serializing an entry we use an empty one - // when calling `toFile`, so we don't want to add it when parsing. - return { - ...result.data, - ...(result.content.trim() && { body: result.content }), - }; - } - - toFile(data, sortedKeys, comments = {}) { - const { body = '', ...meta } = data; - - // Stringify to YAML if the format was not set - const format = this.format || getFormatOpts('yaml'); - if (this.customDelimiter) this.format.delimiters = this.customDelimiter; - - // gray-matter always adds a line break at the end which trips our - // change detection logic - // https://github.com/jonschlinkert/gray-matter/issues/96 - const trimLastLineBreak = body.slice(-1) !== '\n' ? true : false; - // `sortedKeys` is not recognized by gray-matter, so it gets passed through to the parser - const file = matter.stringify(body, meta, { - engines: parsers, - sortedKeys, - comments, - ...format, - }); - return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file; - } -} - -export const FrontmatterInfer = new FrontmatterFormatter(); - -export function frontmatterYAML(customDelimiter) { - return new FrontmatterFormatter('yaml', customDelimiter); -} - -export function frontmatterTOML(customDelimiter) { - return new FrontmatterFormatter('toml', customDelimiter); -} - -export function frontmatterJSON(customDelimiter) { - return new FrontmatterFormatter('json', customDelimiter); -} diff --git a/packages/netlify-cms-core/src/formats/frontmatter.ts b/packages/netlify-cms-core/src/formats/frontmatter.ts new file mode 100644 index 00000000..5a733164 --- /dev/null +++ b/packages/netlify-cms-core/src/formats/frontmatter.ts @@ -0,0 +1,146 @@ +import matter from 'gray-matter'; +import tomlFormatter from './toml'; +import yamlFormatter from './yaml'; +import jsonFormatter from './json'; + +enum Language { + YAML = 'yaml', + TOML = 'toml', + JSON = 'json', +} + +export type Delimiter = string | [string, string]; +type Format = { language: Language; delimiters: Delimiter }; + +const parsers = { + toml: { + parse: (input: string) => tomlFormatter.fromFile(input), + stringify: (metadata: object, opts?: { sortedKeys?: string[] }) => { + const { sortedKeys } = opts || {}; + return tomlFormatter.toFile(metadata, sortedKeys); + }, + }, + json: { + parse: (input: string) => { + let JSONinput = input.trim(); + // Fix JSON if leading and trailing brackets were trimmed. + if (JSONinput.substr(0, 1) !== '{') { + JSONinput = '{' + JSONinput + '}'; + } + return jsonFormatter.fromFile(JSONinput); + }, + stringify: (metadata: object) => { + let JSONoutput = jsonFormatter.toFile(metadata).trim(); + // Trim leading and trailing brackets. + if (JSONoutput.substr(0, 1) === '{' && JSONoutput.substr(-1) === '}') { + JSONoutput = JSONoutput.substring(1, JSONoutput.length - 1); + } + return JSONoutput; + }, + }, + yaml: { + parse: (input: string) => yamlFormatter.fromFile(input), + stringify: ( + metadata: object, + opts?: { sortedKeys?: string[]; comments?: Record }, + ) => { + const { sortedKeys, comments } = opts || {}; + return yamlFormatter.toFile(metadata, sortedKeys, comments); + }, + }, +}; + +function inferFrontmatterFormat(str: string) { + const firstLine = str.substr(0, str.indexOf('\n')).trim(); + if (firstLine.length > 3 && firstLine.substr(0, 3) === '---') { + // No need to infer, `gray-matter` will handle things like `---toml` for us. + return; + } + switch (firstLine) { + case '---': + return getFormatOpts(Language.YAML); + case '+++': + return getFormatOpts(Language.TOML); + case '{': + return getFormatOpts(Language.JSON); + default: + console.warn('Unrecognized front-matter format.'); + } +} + +export function getFormatOpts(format?: Language, customDelimiter?: Delimiter) { + if (!format) { + return undefined; + } + + const formats: { [key in Language]: Format } = { + yaml: { language: Language.YAML, delimiters: '---' }, + toml: { language: Language.TOML, delimiters: '+++' }, + json: { language: Language.JSON, delimiters: ['{', '}'] }, + }; + + const { language, delimiters } = formats[format]; + + return { + language, + delimiters: customDelimiter || delimiters, + }; +} + +export class FrontmatterFormatter { + format?: Format; + + constructor(format?: Language, customDelimiter?: Delimiter) { + this.format = getFormatOpts(format, customDelimiter); + } + + fromFile(content: string) { + const format = this.format || inferFrontmatterFormat(content); + const result = matter(content, { engines: parsers, ...format }); + // in the absent of a body when serializing an entry we use an empty one + // when calling `toFile`, so we don't want to add it when parsing. + return { + ...result.data, + ...(result.content.trim() && { body: result.content }), + }; + } + + toFile( + data: { body?: string } & Record, + sortedKeys?: string[], + comments?: Record, + ) { + const { body = '', ...meta } = data; + + // Stringify to YAML if the format was not set + const format = this.format || getFormatOpts(Language.YAML); + + // gray-matter always adds a line break at the end which trips our + // change detection logic + // https://github.com/jonschlinkert/gray-matter/issues/96 + const trimLastLineBreak = body.slice(-1) !== '\n'; + const file = matter.stringify(body, meta, { + engines: parsers, + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore `sortedKeys` is not recognized by gray-matter, so it gets passed through to the parser + sortedKeys, + comments, + ...format, + }); + return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file; + } +} + +export const FrontmatterInfer = new FrontmatterFormatter(); + +export function frontmatterYAML(customDelimiter?: Delimiter) { + return new FrontmatterFormatter(Language.YAML, customDelimiter); +} + +export function frontmatterTOML(customDelimiter?: Delimiter) { + return new FrontmatterFormatter(Language.TOML, customDelimiter); +} + +export function frontmatterJSON(customDelimiter?: Delimiter) { + return new FrontmatterFormatter(Language.JSON, customDelimiter); +} diff --git a/packages/netlify-cms-core/src/formats/helpers.js b/packages/netlify-cms-core/src/formats/helpers.ts similarity index 50% rename from packages/netlify-cms-core/src/formats/helpers.js rename to packages/netlify-cms-core/src/formats/helpers.ts index 3b1620ad..e3330794 100644 --- a/packages/netlify-cms-core/src/formats/helpers.js +++ b/packages/netlify-cms-core/src/formats/helpers.ts @@ -1,5 +1,9 @@ -export function sortKeys(sortedKeys = [], selector = a => a) { - return (a, b) => { +export function sortKeys( + sortedKeys: string[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + selector: (a: Item) => string = (a: any) => a, +) { + return (a: Item, b: Item) => { const idxA = sortedKeys.indexOf(selector(a)); const idxB = sortedKeys.indexOf(selector(b)); if (idxA === -1 || idxB === -1) return 0; diff --git a/packages/netlify-cms-core/src/formats/json.js b/packages/netlify-cms-core/src/formats/json.ts similarity index 65% rename from packages/netlify-cms-core/src/formats/json.js rename to packages/netlify-cms-core/src/formats/json.ts index 2de169f7..518f85d4 100644 --- a/packages/netlify-cms-core/src/formats/json.js +++ b/packages/netlify-cms-core/src/formats/json.ts @@ -1,9 +1,9 @@ export default { - fromFile(content) { + fromFile(content: string) { return JSON.parse(content); }, - toFile(data) { + toFile(data: object) { return JSON.stringify(data, null, 2); }, }; diff --git a/packages/netlify-cms-core/src/formats/toml.js b/packages/netlify-cms-core/src/formats/toml.ts similarity index 65% rename from packages/netlify-cms-core/src/formats/toml.js rename to packages/netlify-cms-core/src/formats/toml.ts index 8b435837..027cfac8 100644 --- a/packages/netlify-cms-core/src/formats/toml.js +++ b/packages/netlify-cms-core/src/formats/toml.ts @@ -1,17 +1,19 @@ import toml from '@iarna/toml'; import tomlify from 'tomlify-j0.4'; import moment from 'moment'; -import AssetProxy from 'ValueObjects/AssetProxy'; +import AssetProxy from '../valueObjects/AssetProxy'; import { sortKeys } from './helpers'; -function outputReplacer(key, value) { +function outputReplacer(_key: string, value: unknown) { if (moment.isMoment(value)) { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore return value.format(value._f); } if (value instanceof AssetProxy) { return `${value.path}`; } - if (Number.isInteger(value)) { + if (typeof value === 'number' && Number.isInteger(value)) { // Return the string representation of integers so tomlify won't render with tenths (".0") return value.toString(); } @@ -20,11 +22,11 @@ function outputReplacer(key, value) { } export default { - fromFile(content) { + fromFile(content: string) { return toml.parse(content); }, - toFile(data, sortedKeys = []) { + toFile(data: object, sortedKeys: string[] = []) { return tomlify.toToml(data, { replace: outputReplacer, sort: sortKeys(sortedKeys) }); }, }; diff --git a/packages/netlify-cms-core/src/formats/yaml.js b/packages/netlify-cms-core/src/formats/yaml.ts similarity index 69% rename from packages/netlify-cms-core/src/formats/yaml.js rename to packages/netlify-cms-core/src/formats/yaml.ts index 7d592680..11e45b60 100644 --- a/packages/netlify-cms-core/src/formats/yaml.js +++ b/packages/netlify-cms-core/src/formats/yaml.ts @@ -1,7 +1,8 @@ import yaml from 'yaml'; import { sortKeys } from './helpers'; +import { YAMLMap, YAMLSeq, Pair, Node } from 'yaml/types'; -function addComments(items, comments, prefix = '') { +function addComments(items: Array, comments: Record, prefix = '') { items.forEach(item => { if (item.key !== undefined) { const itemKey = item.key.toString(); @@ -18,7 +19,7 @@ function addComments(items, comments, prefix = '') { } const timestampTag = { - identify: value => value instanceof Date, + identify: (value: unknown) => value instanceof Date, default: true, tag: '!timestamp', test: RegExp( @@ -29,20 +30,20 @@ const timestampTag = { 'Z' + // Z '$', ), - resolve: str => new Date(str), - stringify: value => value.toISOString(), -}; + resolve: (str: string) => new Date(str), + stringify: (value: Node) => (value as Date).toISOString(), +} as const; export default { - fromFile(content) { + fromFile(content: string) { if (content && content.trim().endsWith('---')) { content = content.trim().slice(0, -3); } return yaml.parse(content, { customTags: [timestampTag] }); }, - toFile(data, sortedKeys = [], comments = {}) { - const contents = yaml.createNode(data); + toFile(data: object, sortedKeys: string[] = [], comments: Record = {}) { + const contents = yaml.createNode(data) as YAMLMap | YAMLSeq; addComments(contents.items, comments); diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 6afba7b1..7e599307 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -9,6 +9,7 @@ import { Medias } from '../reducers/medias'; import { Deploys } from '../reducers/deploys'; import { Search } from '../reducers/search'; import { GlobalUI } from '../reducers/globalUI'; +import { formatExtensions } from '../formats/formats'; export type CmsBackendType = | 'azure' @@ -597,6 +598,8 @@ type i18n = StaticallyTypedRecord<{ default_locale: string; }>; +export type Format = keyof typeof formatExtensions; + type CollectionObject = { name: string; folder?: string; @@ -611,7 +614,8 @@ type CollectionObject = { filter?: FilterRule; type: 'file_based_collection' | 'folder_based_collection'; extension?: string; - format?: string; + format?: Format; + frontmatter_delimiter?: List | string | [string, string]; create?: boolean; delete?: boolean; identifier_field?: string; diff --git a/packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts b/packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts new file mode 100644 index 00000000..a7da4a6a --- /dev/null +++ b/packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts @@ -0,0 +1,13 @@ +declare module 'tomlify-j0.4' { + interface ToTomlOptions { + replace?(key: string, value: unknown): string | false; + sort?(a: string, b: string): number; + } + + interface Tomlify { + toToml(data: object, options?: ToTomlOptions): string; + } + + const tomlify: Tomlify; + export default tomlify; +}