diff --git a/.eslintrc.js b/.eslintrc.js index f030bfa6..77bb1f24 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,5 @@ module.exports = { env: { - commonjs: true, es6: true, node: true, 'jest/globals': true diff --git a/.gitignore b/.gitignore index 3ba17500..1d7923cd 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,8 @@ dist # IntelliJ .idea/ + +# CLI generated files +components.*.json +presets.*.json +storyblok-component-types.d.ts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e2839582 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "editor.formatOnSave": false, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + }, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} diff --git a/README.md b/README.md index 5ef42a6c..a76a0211 100644 --- a/README.md +++ b/README.md @@ -554,6 +554,92 @@ module.exports = function (block) { } ``` +## Typescript +It is possible to generate Typescript type definitions for your Storyblok components. The type definitions are based on the components' JSON Schema that can be retrieved with the [pull-components](#pull-components) command. + +### generate-typescript-typedefs + +Generate a file with the type definitions for the specified components' JSON Schemas. + +```sh +$ storyblok generate-typescript-typedefs + --sourceFilePaths + --destinationFilePath + --typeNamesPrefix + --typeNamesSuffix + --JSONSchemaToTSOptionsPath + --customFieldTypesParserPath +``` + +#### Options + +* `sourceFilePaths` (alias `source`) : Path(s) to the components JSON file(s) as comma separated values +* `destinationFilePath` (alias `target`) *optional* : Path to the Typescript file that will be generated (*default*: `storyblok-component-types.d.ts`) +* `typeNamesPrefix` (alias `titlePrefix`) *optional* : A prefix that will be prepended to all the names of the generated types +* `typeNamesSuffix` (alias `titleSuffix`) *optional* : A suffix that will be appended to all the names of the generated types (*default*: `Storyblok`) +* `JSONSchemaToTSOptionsPath` (alias `compilerOptions`) *optional* : Path to a JSON file with a list of options supported by `json-schema-to-typescript` +* `customFieldTypesParserPath` (alias `customTypeParser`) *optional* : Path to the parser file for Custom Field Types + +#### Examples + +```sh +# Generate typedefs for the components retrieved for the space `12345` via the `storyblok pull-components` command +$ storyblok generate-typescript-typedefs --sourceFilePaths ./components.12345.json + +# Generate typedefs for multiple components sources +$ storyblok generate-typescript-typedefs --sourceFilePaths ./fooComponent-12345.json,./barComponent-12345.json + +# Custom path for the typedefs file +$ storyblok generate-typescript-typedefs --sourceFilePaths ./components.12345.json --destinationFilePath ./types/my-custom-type-file.d.ts + +# Provide customized options for the JSON-schema-to-typescript lib +$ storyblok generate-typescript-typedefs --sourceFilePaths ./components.12345.json --JSONSchemaToTSOptionsPath ./PathToJSONFileWithCustomOptions.json + +# Provide a custom field types parser +$ storyblok generate-typescript-typedefs --sourceFilePaths ./components.12345.json --customFieldTypesParserPath ./customFieldTypesParser.js + +``` + +##### JSON Schema to Typescript options +This script uses the `json-schema-to-typescript` library under the hood. Values of the [JSON Schema to Typescript options](https://www.npmjs.com/package/json-schema-to-typescript#options) can be customized providing a JSON file to the `JSONSchemaToTSOptionsPath`. + +The default values used for the `storyblok generate-typescript-typedefs` command are the same defaults for the library except for two properties: +* `bannerComment` - The default value is `""` to remove noise from the generated Typedefs file +* `unknownAny` - The default value is `false` because it can help a smoother Typescript adoption on a JS project + +Example `JSONSchemaToTSOptions` JSON file to remove `additionalProperties` from the generated type definitions: + +```json +{ + "additionalProperties": false, +} +``` + +##### Custom Field Types parser +Storyblok [Custom Field Types](https://www.storyblok.com/docs/plugins/field-plugins/introduction) do not have inherent JSONSchema definitions. To overcome this issue, you can provide a path to a script exporting a parser function that should render a [JSONSchema Node](https://json-schema.org/learn/getting-started-step-by-step#define-properties) for each of your Custom Field Types. The parser function should be exported as a default export, like in the following example: +```js +export default function (key, obj) { + switch (obj.field_type) { + case 'my-custom-field-type-name': + return { + [key]: { + type: 'object', + properties: { + color: { type: 'string' } + }, + required: ['color'] + } + } + default: + return {} + } +} +``` + + + + + ## You're looking for a headstart? Check out our guides for client side apps (VueJS, Angular, React, ...), static site (Jekyll, NuxtJs, ...), dynamic site examples (Node, PHP, Python, Laravel, ...) on our [Getting Started](https://www.storyblok.com/getting-started) page. diff --git a/__mocks__/fs-extra.js b/__mocks__/fs-extra.js index d608bdd3..6a4db47d 100644 --- a/__mocks__/fs-extra.js +++ b/__mocks__/fs-extra.js @@ -1,4 +1,6 @@ -const fs = jest.genMockFromModule('fs-extra') +import { jest } from '@jest/globals' + +const fs = jest.createMockFromModule('fs-extra') let mockFiles = Object.create(null) @@ -19,15 +21,15 @@ const readFile = jest.fn((path) => { mockFiles = path return Promise.resolve(JSON.stringify([ { - "id": 0, - "full_slug": "another-post", - "content": { - "_uid": "5647c21f-8813-4f8a-ad38-b9f74e0e7c89", - "text": "Donec tortor mauris, mollis vel pretium vitae, lacinia nec sapien. Donec erat neque, ullamcorper tincidunt iaculis sit amet, pharetra bibendum ipsum. Nunc mattis risus ac ante consequat nec pulvinar neque molestie. Etiam interdum nunc at metus lacinia non varius erat dignissim. Integer elementum, felis id facilisis vulputate, ipsum tellus venenatis dui, at blandit nibh massa in dolor. Cras a ultricies sapien. Vivamus adipiscing feugiat pharetra.", - "image": "https://a.storyblok.com/f/51376/884x750/3bff01d851/international.svg", - "title": "test", - "category": "news", - "component": "Product" + id: 0, + full_slug: 'another-post', + content: { + _uid: '5647c21f-8813-4f8a-ad38-b9f74e0e7c89', + text: 'Donec tortor mauris, mollis vel pretium vitae, lacinia nec sapien. Donec erat neque, ullamcorper tincidunt iaculis sit amet, pharetra bibendum ipsum. Nunc mattis risus ac ante consequat nec pulvinar neque molestie. Etiam interdum nunc at metus lacinia non varius erat dignissim. Integer elementum, felis id facilisis vulputate, ipsum tellus venenatis dui, at blandit nibh massa in dolor. Cras a ultricies sapien. Vivamus adipiscing feugiat pharetra.', + image: 'https://a.storyblok.com/f/51376/884x750/3bff01d851/international.svg', + title: 'test', + category: 'news', + component: 'Product' } } ])) @@ -60,4 +62,4 @@ fs.__clearMockFiles = __clearMockFiles fs.__setMockFiles = __setMockFiles -module.exports = fs +export default fs diff --git a/build.config.ts b/build.config.ts new file mode 100644 index 00000000..514613de --- /dev/null +++ b/build.config.ts @@ -0,0 +1,23 @@ +import { defineBuildConfig } from "unbuild"; + +export default defineBuildConfig({ + declaration: true, + rollup: { + inlineDependencies: true, + resolve: { + exportConditions: ["production", "node"] as any, + }, + }, + entries: ["src/cli"], + externals: [ + "@nuxt/test-utils", + "fsevents", + "node:url", + "node:buffer", + "node:path", + "node:child_process", + "node:process", + "node:path", + "node:os", + ], +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..351f7ed4 --- /dev/null +++ b/jest.config.js @@ -0,0 +1 @@ +export default { transform: {} } diff --git a/package.json b/package.json index 06e7b0da..a88a6723 100644 --- a/package.json +++ b/package.json @@ -12,15 +12,21 @@ "node", "javascript" ], - "main": "src/cli.js", + "main": "./dist/cli.mjs", + "files": [ + "dist/**" + ], "bin": { - "storyblok": "src/cli.js" + "storyblok": "./dist/cli.mjs" }, + "type": "module", "scripts": { + "build": "unbuild", + "dev": "npm run build && ./dist/cli.mjs", "lint": "eslint src/", "lint:fix": "eslint src/ --fix", - "test:unit": "jest --silent", - "test:coverage": "jest --coverage" + "test:unit": "node --experimental-vm-modules ./node_modules/.bin/jest --silent", + "test:coverage": "node --experimental-vm-modules ./node_modules/.bin/jest --coverage" }, "author": "Dominik Angerer , Alexander Feiglstorfer ", "license": "MIT", @@ -36,6 +42,7 @@ "fs-extra": "^9.0.1", "git-clone": "^0.1.0", "inquirer": "^7.3.2", + "json-schema-to-typescript": "^13.1.2", "lodash": "^4.17.21", "netrc": "0.1.4", "on-change": "^2.0.1", @@ -43,7 +50,7 @@ "p-series": "^2.1.0", "path": "^0.12.7", "simple-uuid": "^0.0.1", - "storyblok-js-client": "^5.14.0", + "storyblok-js-client": "^6.7.1", "update-notifier": "^5.1.0", "xml-js": "^1.6.11" }, @@ -59,11 +66,16 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.1", - "jest": "^26.1.0" + "jest": "^29.7.0", + "typescript": "^5.3.3", + "unbuild": "^2.0.0" }, "release": { "branches": [ "master" ] + }, + "prettier": { + "printWidth": 120 } } diff --git a/src/cli.js b/src/cli.js index 0a64f77b..71a57990 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,20 +1,20 @@ #!/usr/bin/env node -const commander = require('commander') +import commander from 'commander' +import chalk from 'chalk' +import clear from 'clear' +import figlet from 'figlet' +import inquirer from 'inquirer' +import { ALL_REGIONS, EU_CODE, isRegion } from '@storyblok/region-helper' +import updateNotifier from 'update-notifier' +import fs from 'fs' +import tasks from './tasks' +import { getQuestions, lastStep, api, creds } from './utils' +import { SYNC_TYPES, COMMANDS } from './constants' + +const rawPkg = fs.readFileSync('./package.json') +const pkg = JSON.parse(rawPkg) const program = new commander.Command() - -const chalk = require('chalk') -const clear = require('clear') -const figlet = require('figlet') -const inquirer = require('inquirer') -const { ALL_REGIONS, EU_CODE, isRegion } = require('@storyblok/region-helper') - -const updateNotifier = require('update-notifier') -const pkg = require('../package.json') - -const tasks = require('./tasks') -const { getQuestions, lastStep, api, creds } = require('./utils') -const { SYNC_TYPES, COMMANDS } = require('./constants') const allRegionsText = ALL_REGIONS.join(', ') clear() @@ -523,6 +523,42 @@ program } }) +// Generate Typescript type definitions +program + .command(COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS) + // Providing backward-compatible flags with Storyblok Generate TS https://github.com/dohomi/storyblok-generate-ts + .requiredOption('--source, --sourceFilePaths ', 'Path(s) to the components JSON file(s) as comma separated values', (paths, _previous) => paths.split(',')) + .option('--target, --destinationFilePath ', 'Path to the Typescript file that will be generated (default: `storyblok-component-types.d.ts`)') + .option('--titlePrefix, --typeNamesPrefix ', 'A prefix that will be prepended to all the names of the generated types') + .option('--titleSuffix, --typeNamesSuffix ', 'A suffix that will be appended to all the names of the generated types (*default*: `Storyblok`)') + .option('--compilerOptions, --JSONSchemaToTSOptionsPath ', 'Path to a JSON file with a list of options supported by json-schema-to-typescript') + .option('--customTypeParser, --customFieldTypesParserPath ', 'Path to the parser file for Custom Field Types') + .action((options) => { + console.log(`${chalk.blue('-')} Executing ${COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS} task`) + + const { + sourceFilePaths, + destinationFilePath, + typeNamesPrefix, + typeNamesSuffix, + customFieldTypesParserPath, + JSONSchemaToTSOptionsPath + } = options + + try { + tasks.generateTypescriptTypedefs({ + sourceFilePaths, + destinationFilePath, + typeNamesPrefix, + typeNamesSuffix, + customFieldTypesParserPath, + JSONSchemaToTSOptionsPath + }) + } catch (e) { + errorHandler(e, COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS) + } + }) + program.parse(process.argv) if (program.rawArgs.length <= 2) { diff --git a/src/cli.ts b/src/cli.ts new file mode 100755 index 00000000..30bf2573 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,597 @@ +#!/usr/bin/env node +//@ts-nocheck +import commander from "commander"; +import chalk from "chalk"; +import clear from "clear"; +import figlet from "figlet"; +import inquirer from "inquirer"; +import { ALL_REGIONS, EU_CODE, isRegion } from "@storyblok/region-helper"; +import updateNotifier from "update-notifier"; +import fs from "fs"; +import tasks from "./tasks"; +import { getQuestions, lastStep, api, creds } from "./utils"; +import { SYNC_TYPES, COMMANDS } from "./constants"; +export * from "./types/index"; + +const rawPkg = fs.readFileSync("./package.json"); +const pkg = JSON.parse(rawPkg); +const program = new commander.Command(); +const allRegionsText = ALL_REGIONS.join(", "); + +clear(); +console.log(chalk.cyan(figlet.textSync("storyblok"))); +console.log(); +console.log(); +console.log("Hi, welcome to the Storyblok CLI"); +console.log(); + +// non-intrusive notify users if an update available +const notifyOptions = { + isGlobal: true, +}; + +updateNotifier({ pkg }).notify(notifyOptions); + +program.version(pkg.version); + +program.option("-s, --space [value]", "space ID"); + +// login +program + .command(COMMANDS.LOGIN) + .description("Login to the Storyblok cli") + .option("-t, --token ", "Token to login directly without questions, like for CI environments") + .option( + "-r, --region ", + `The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`, + EU_CODE + ) + .action(async (options) => { + const { token, region } = options; + + if (api.isAuthorized()) { + console.log( + chalk.green("✓") + + " The user has been already logged. If you want to change the logged user, you must logout and login again" + ); + return; + } + + if (!isRegion(region)) { + console.log( + chalk.red("X") + + `The provided region ${region} is not valid. Please use one of the following: ${allRegionsText}` + ); + return; + } + + try { + await api.processLogin(token, region); + process.exit(0); + } catch (e) { + console.log(chalk.red("X") + " An error occurred when logging the user: " + e.message); + process.exit(1); + } + }); + +// getUser +program + .command("user") + .description("Get the currently logged in user") + .action(async () => { + if (api.isAuthorized()) { + try { + const user = await api.getUser(); + console.log(chalk.green("✓") + ` Hi ${user.friendly_name}, you current logged in with: ${creds.get().email}`); + } catch (e) { + console.log(chalk.red("X") + ` Please check if your current region matches your user's region: ${e.message}.`); + } finally { + process.exit(0); + } + } + console.log(chalk.red("X") + " There is currently no user logged."); + }); + +// logout +program + .command(COMMANDS.LOGOUT) + .description("Logout from the Storyblok cli") + .action(async () => { + try { + await api.logout(); + console.log("Logged out successfully! Token has been removed from .netrc file."); + process.exit(0); + } catch (e) { + console.log(chalk.red("X") + " An error occurred when logging out the user: " + e.message); + process.exit(1); + } + }); + +// pull-languages +program + .command("pull-languages") + .description("Download your space's languages schema as json") + .action(async () => { + console.log(`${chalk.blue("-")} Executing pull-languages task`); + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(0); + } + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.pullLanguages(api, { space }); + } catch (e) { + console.log(chalk.red("X") + " An error occurred when executing the pull-languages task: " + e.message); + process.exit(1); + } + }); + +// pull-components +program + .command(COMMANDS.PULL_COMPONENTS) + .option("--sf, --separate-files [value]", "Argument to create a single file for each component") + .option("-p, --path ", "Path to save the component files") + .option("-f, --file-name ", "custom name to be used in file(s) name instead of space id") + .description("Download your space's components schema as json") + .action(async (options) => { + console.log(`${chalk.blue("-")} Executing pull-components task`); + const space = program.space; + const { separateFiles, path } = options; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(0); + } + + const fileName = options.fileName ? options.fileName : space; + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.pullComponents(api, { fileName, separateFiles, path }); + } catch (e) { + errorHandler(e, COMMANDS.PULL_COMPONENTS); + } + }); + +// push-components +program + .command(COMMANDS.PUSH_COMPONENTS + " ") + .option("-p, --presets-source ", "Path to presets file") + .description( + "Download your space's components schema as json. The source parameter can be a URL to your JSON file or a path to it" + ) + .action(async (source, options) => { + console.log(`${chalk.blue("-")} Executing push-components task`); + const space = program.space; + const presetsSource = options.presetsSource; + + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(0); + } + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.pushComponents(api, { source, presetsSource }); + } catch (e) { + errorHandler(e, COMMANDS.PUSH_COMPONENTS); + } + }); + +// delete-component +program + .command("delete-component ") + .description("Delete a single component on your space.") + .action(async (component) => { + console.log(`${chalk.blue("-")} Executing delete-component task`); + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(0); + } + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.deleteComponent(api, { comp: component }); + } catch (e) { + console.log(chalk.red("X") + " An error occurred when executing the delete-component task: " + e.message); + process.exit(1); + } + }); + +// delete-components +program + .command("delete-components ") + .description("Delete all components in your space that occur in your source file.") + .option("-r, --reverse", "Delete all components in your space that do not appear in your source.", false) + .option("--dryrun", "Does not perform any delete changes on your space.") + .action(async (source, options) => { + console.log(`${chalk.blue("-")} Executing delete-components task`); + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(0); + } + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.deleteComponents(api, { source, dryRun: !!options.dryrun, reversed: !!options.reverse }); + } catch (e) { + console.log(chalk.red("X") + " An error occurred when executing the delete-component task: " + e.message); + process.exit(1); + } + }); + +// scaffold +program + .command(COMMANDS.SCAFFOLD + " ") + .description("Scaffold component") + .action(async (name) => { + console.log(`${chalk.blue("-")} Scaffolding a component\n`); + + if (api.isAuthorized()) { + api.accessToken = creds.get().token || null; + } + + try { + await tasks.scaffold(api, name, program.space); + console.log(chalk.green("✓") + " Generated files: "); + console.log(chalk.green("✓") + " - views/components/_" + name + ".liquid"); + console.log(chalk.green("✓") + " - source/scss/components/below/_" + name + ".scss"); + process.exit(0); + } catch (e) { + console.log( + chalk.red("X") + " An error occurred when executing operations to create the component: " + e.message + ); + process.exit(1); + } + }); + +// select +program + .command(COMMANDS.SELECT) + .description("Usage to kickstart a boilerplate, fieldtype or theme") + .action(async () => { + console.log(`${chalk.blue("-")} Select a boilerplate, fieldtype or theme to initialize\n`); + + try { + const questions = getQuestions("select"); + const answers = await inquirer.prompt(questions); + + await lastStep(answers); + } catch (e) { + console.error(chalk.red("X") + " An error ocurred when execute the select command: " + e.message); + process.exit(1); + } + }); + +// sync +program + .command(COMMANDS.SYNC) + .description("Sync schemas, roles, folders and stories between spaces") + .requiredOption( + "--type ", + "Define what will be sync. Can be components, folders, stories, datasources or roles" + ) + .requiredOption("--source ", "Source space id") + .requiredOption("--target ", "Target space id") + .option("--components-groups ", "Synchronize components based on their group UUIDs separated by commas") + .action(async (options) => { + console.log(`${chalk.blue("-")} Sync data between spaces\n`); + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + const { type, target, source, componentsGroups } = options; + + const _componentsGroups = componentsGroups ? componentsGroups.split(",") : null; + + const token = creds.get().token || null; + + const _types = type.split(",") || []; + _types.forEach((_type) => { + if (!SYNC_TYPES.includes(_type)) { + throw new Error(`The type ${_type} is not valid`); + } + }); + + await tasks.sync(_types, { + api, + token, + target, + source, + _componentsGroups, + }); + + console.log("\n" + chalk.green("✓") + " Sync data between spaces successfully completed"); + } catch (e) { + errorHandler(e, COMMANDS.SYNC); + } + }); + +// quickstart +program + .command(COMMANDS.QUICKSTART) + .description("Start a project quickly") + .action(async () => { + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + const space = program.space; + const questions = getQuestions("quickstart", { space }, api); + const answers = await inquirer.prompt(questions); + await tasks.quickstart(api, answers, space); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred when execute quickstart operations: " + e.message); + process.exit(1); + } + }); + +program + .command(COMMANDS.GENERATE_MIGRATION) + .description("Generate a content migration file") + .requiredOption("-c, --component ", "Name of the component") + .requiredOption("-f, --field ", "Name of the component field") + .action(async (options) => { + const { field = "" } = options; + const { component = "" } = options; + + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(1); + } + + console.log(`${chalk.blue("-")} Creating the migration file in ./migrations/change_${component}_${field}.js\n`); + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.generateMigration(api, component, field); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred when generate the migration file: " + e.message); + process.exit(1); + } + }); + +program + .command(COMMANDS.RUN_MIGRATION) + .description("Run a migration file") + .requiredOption("-c, --component ", "Name of the component") + .requiredOption("-f, --field ", "Name of the component field") + .option("--dryrun", "Do not update the story content") + .option("--publish ", "Publish the content. It can be: all, published or published-with-changes") + .option("--publish-languages ", "Publish specific languages") + .action(async (options) => { + const field = options.field || ""; + const component = options.component || ""; + const isDryrun = !!options.dryrun; + const publish = options.publish || null; + const publishLanguages = options.publishLanguages || ""; + + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(1); + } + + const publishOptionsAvailable = ["all", "published", "published-with-changes"]; + if (publish && !publishOptionsAvailable.includes(publish)) { + console.log( + chalk.red("X") + " Please provide a correct publish option: all, published, or published-with-changes" + ); + process.exit(1); + } + + console.log(`${chalk.blue("-")} Processing the migration ./migrations/change_${component}_${field}.js\n`); + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + await tasks.runMigration(api, component, field, { isDryrun, publish, publishLanguages }); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred when run the migration file: " + e.message); + process.exit(1); + } + }); + +program + .command(COMMANDS.ROLLBACK_MIGRATION) + .description("Rollback-migration a migration file") + .requiredOption("-c, --component ", "Name of the component") + .requiredOption("-f, --field ", "Name of the component field") + .action(async (options) => { + const field = options.field || ""; + const component = options.component || ""; + const space = program.space; + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space YOUR_SPACE_ID."); + process.exit(1); + } + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(space); + + await tasks.rollbackMigration(api, field, component); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred when run rollback-migration: " + e.message); + process.exit(1); + } + }); + +// list spaces +program + .command(COMMANDS.SPACES) + .description("List all spaces of the logged account") + .action(async () => { + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + const { region } = creds.get(); + + await tasks.listSpaces(api, region); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred to listing spaces: " + e.message); + process.exit(1); + } + }); + +// import data +program + .command(COMMANDS.IMPORT) + .description("Import data from other systems and relational databases.") + .requiredOption("-f, --file ", "Name of the file") + .requiredOption("-t, --type ", "Type of the content") + .option("-fr, --folder ", "(Optional) This is a Id of folder in storyblok") + .option("-d, --delimiter ", 'If you are using a csv file, put the file delimiter, the default is ";"') + .action(async (options) => { + const space = program.space; + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + if (!space) { + console.log(chalk.red("X") + " Please provide the space as argument --space ."); + return; + } + + api.setSpaceId(space); + await tasks.importFiles(api, options); + + console.log(`${chalk.green("✓")} The import process was executed with success!`); + } catch (e) { + console.log(chalk.red("X") + " An error ocurred to import data : " + e.message); + process.exit(1); + } + }); + +// delete-datasources +program + .command(COMMANDS.DELETE_DATASOURCES) + .requiredOption("--space-id ", "Space id") + .option("--by-slug ", "Delete datasources by slug") + .option("--by-name ", "Delete datasources by name") + .action(async (options) => { + console.log(`${chalk.blue("-")} Executing ${COMMANDS.DELETE_DATASOURCES} task`); + + const { spaceId, bySlug, byName } = options; + + try { + if (!api.isAuthorized()) { + await api.processLogin(); + } + + api.setSpaceId(spaceId); + + await tasks.deleteDatasources(api, { byName, bySlug }); + } catch (e) { + errorHandler(e, COMMANDS.DELETE_DATASOURCES); + } + }); + +// Generate Typescript type definitions +program + .command(COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS) + // Providing backward-compatible flags with Storyblok Generate TS https://github.com/dohomi/storyblok-generate-ts + .requiredOption( + "--source, --sourceFilePaths ", + "Path(s) to the components JSON file(s) as comma separated values", + (paths, _previous) => paths.split(",") + ) + .option( + "--target, --destinationFilePath ", + "Path to the Typescript file that will be generated (default: `storyblok-component-types.d.ts`)" + ) + .option( + "--titlePrefix, --typeNamesPrefix ", + "A prefix that will be prepended to all the names of the generated types" + ) + .option( + "--titleSuffix, --typeNamesSuffix ", + "A suffix that will be appended to all the names of the generated types (*default*: `Storyblok`)" + ) + .option( + "--compilerOptions, --JSONSchemaToTSOptionsPath ", + "Path to a JSON file with a list of options supported by json-schema-to-typescript" + ) + .option("--customTypeParser, --customFieldTypesParserPath ", "Path to the parser file for Custom Field Types") + .action((options) => { + console.log(`${chalk.blue("-")} Executing ${COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS} task`); + + const { + sourceFilePaths, + destinationFilePath, + typeNamesPrefix, + typeNamesSuffix, + customFieldTypesParserPath, + JSONSchemaToTSOptionsPath, + } = options; + + try { + tasks.generateTypescriptTypedefs({ + sourceFilePaths, + destinationFilePath, + typeNamesPrefix, + typeNamesSuffix, + customFieldTypesParserPath, + JSONSchemaToTSOptionsPath, + }); + } catch (e) { + errorHandler(e, COMMANDS.GENERATE_TYPESCRIPT_TYPEDEFS); + } + }); + +program.parse(process.argv); + +if (program.rawArgs.length <= 2) { + program.help(); +} + +function errorHandler(e, command) { + if (/404/.test(e.message)) { + const allRegionsButDefault = ALL_REGIONS.filter((region) => region !== EU_CODE).join(" ,"); + console.log( + chalk.yellow("/!\\") + + ` If your space was not created under ${EU_CODE} region, you must provide the region (${allRegionsButDefault}) upon login.` + ); + } else { + console.log(chalk.red("X") + " An error occurred when executing the " + command + " task: " + e || e.message); + } + process.exit(1); +} diff --git a/src/constants.js b/src/constants.js index e73742bc..e6db717f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,6 +1,12 @@ -const SYNC_TYPES = ['folders', 'components', 'roles', 'stories', 'datasources'] +export const SYNC_TYPES = [ + 'folders', + 'components', + 'roles', + 'stories', + 'datasources' +] -const COMMANDS = { +export const COMMANDS = { GENERATE_MIGRATION: 'generate-migration', IMPORT: 'import', LOGIN: 'login', @@ -14,16 +20,11 @@ const COMMANDS = { SELECT: 'select', SPACES: 'spaces', SYNC: 'sync', - DELETE_DATASOURCES: 'delete-datasources' + DELETE_DATASOURCES: 'delete-datasources', + GENERATE_TYPESCRIPT_TYPEDEFS: 'generate-typescript-typedefs' } -const DEFAULT_AGENT = { +export const DEFAULT_AGENT = { SB_Agent: 'SB-CLI', SB_Agent_Version: process.env.npm_package_version || '3.0.0' } - -module.exports = { - SYNC_TYPES, - COMMANDS, - DEFAULT_AGENT -} diff --git a/src/tasks/delete-component.js b/src/tasks/delete-component.js index 98d2cd1e..94410abf 100644 --- a/src/tasks/delete-component.js +++ b/src/tasks/delete-component.js @@ -1,5 +1,5 @@ -const chalk = require('chalk') -const { getComponentsFromName } = require('./migrations/utils') +import chalk from 'chalk' +import { getComponentsFromName } from './migrations/utils' /** * @@ -29,4 +29,4 @@ const deleteComponent = async (api, { comp, dryrun = false }) => { return Promise.reject(new Error(e)) } } -module.exports = deleteComponent +export default deleteComponent diff --git a/src/tasks/delete-components.js b/src/tasks/delete-components.js index b8494a54..cda0f977 100644 --- a/src/tasks/delete-components.js +++ b/src/tasks/delete-components.js @@ -1,8 +1,9 @@ -const chalk = require('chalk') -const axios = require('axios') -const fs = require('fs') -const isEmpty = require('lodash/isEmpty') -const deleteComponent = require('./delete-component') +import chalk from 'chalk' +import axios from 'axios' +import fs from 'fs' +import lodash from 'lodash' +import deleteComponent from './delete-component' +const { isEmpty } = lodash const isUrl = source => source.indexOf('http') === 0 @@ -109,4 +110,4 @@ const deleteComponentAndSkip = async (api, c, dryrun) => { } } -module.exports = deleteComponents +export default deleteComponents diff --git a/src/tasks/delete-datasources.js b/src/tasks/delete-datasources.js index 4ff7c023..080f130d 100644 --- a/src/tasks/delete-datasources.js +++ b/src/tasks/delete-datasources.js @@ -1,4 +1,4 @@ -const chalk = require('chalk') +import chalk from 'chalk' /** * @method deleteDatasources @@ -38,4 +38,4 @@ const deleteDatasources = async (api, options) => { } } -module.exports = deleteDatasources +export default deleteDatasources diff --git a/src/tasks/generate-typescript-typedefs.ts b/src/tasks/generate-typescript-typedefs.ts new file mode 100644 index 00000000..d5413e77 --- /dev/null +++ b/src/tasks/generate-typescript-typedefs.ts @@ -0,0 +1,79 @@ +import chalk from "chalk"; +import fs from "fs"; +import type { GenerateTypescriptTypedefsCLIOptions, JSONSchemaToTSOptions } from "../types"; +import { GenerateTypesFromJSONSchemas } from "../utils/typescript/generateTypesFromJSONSchema"; +import type { JSONSchema } from "json-schema-to-typescript"; + +type GenerateTSTypedefs = (options: GenerateTypescriptTypedefsCLIOptions) => void; + +const generateTypescriptTypedefs: GenerateTSTypedefs = async ({ + sourceFilePaths, + destinationFilePath = "./storyblok-component-types.d.ts", + typeNamesPrefix, + typeNamesSuffix = "Storyblok", + customFieldTypesParserPath, + JSONSchemaToTSOptionsPath, +}) => { + /** + * Get JSON Schemas from files looking at all the paths provided + * @param paths An array of paths to read from + * @returns An array of components JSONSchemas + */ + const getJSONSchemasFromPaths = (paths: string[]): JSONSchema[] | null => { + try { + return paths.map((sourceFilePath) => JSON.parse(fs.readFileSync(sourceFilePath, "utf8"))); + } catch (e) { + console.error( + `${chalk.red("X")} + Could not load JSON files from the provided paths: ${paths}. Please check if those files exist.` + ); + return null; + } + }; + + /** + * Get user-provided options for json-schema-to-typescript https://www.npmjs.com/package/json-schema-to-typescript#options + * @param path Path to a JSON file with the options + * @returns A POJO with the options + */ + const getJSONSchemaToTSOptionsFromPath = (path: string): Record | null => { + try { + return JSON.parse(fs.readFileSync(path, "utf8")); + } catch (e) { + console.error( + `${chalk.red("X")} + Could not load options from the JSON file at ${path}. Please check if the file exists and if it's properly formatted.` + ); + return null; + } + }; + + const JSONSchemaToTSCustomOptions = + JSONSchemaToTSOptionsPath && getJSONSchemaToTSOptionsFromPath(JSONSchemaToTSOptionsPath); + + // Merge custom provided options to our defaults + const JSONSchemaToTSOptions: JSONSchemaToTSOptions = { + bannerComment: "", // Remove much noise from the Typedefs file + unknownAny: false, // Smoother transition from a non-TS codebase to a TS codebase + ...JSONSchemaToTSCustomOptions, + }; + + const componentsJSONSchemaArray = getJSONSchemasFromPaths(sourceFilePaths)?.flatMap( + (componentsJSONSchema) => componentsJSONSchema.components || componentsJSONSchema + ); + + if (componentsJSONSchemaArray && componentsJSONSchemaArray.length > 0) { + const generator = await GenerateTypesFromJSONSchemas.init(componentsJSONSchemaArray, { + sourceFilePaths, + destinationFilePath, + typeNamesPrefix, + typeNamesSuffix, + customFieldTypesParserPath, + JSONSchemaToTSCustomOptions: JSONSchemaToTSOptions, + }); + + return await generator.generateTSFile(); + } +}; + +export default generateTypescriptTypedefs; diff --git a/src/tasks/import/import.js b/src/tasks/import/import.js index e6227069..d32f359f 100644 --- a/src/tasks/import/import.js +++ b/src/tasks/import/import.js @@ -1,9 +1,5 @@ -const chalk = require('chalk') -const { - convertFile, - sendContent, - discoverExtension -} = require('./utils') +import chalk from 'chalk' +import { convertFile, sendContent, discoverExtension } from './utils' /** * @typedef {Object} ImportDataOptions @@ -37,4 +33,4 @@ const importFiles = async (api, options) => { return Promise.reject(e) } } -module.exports = importFiles +export default importFiles diff --git a/src/tasks/import/utils.js b/src/tasks/import/utils.js index 32d8e989..17d09275 100644 --- a/src/tasks/import/utils.js +++ b/src/tasks/import/utils.js @@ -1,17 +1,17 @@ -const csvReader = require('fast-csv') -const xmlConverter = require('xml-js') -const chalk = require('chalk') -const path = require('path') -const fs = require('fs') -const { isArray } = require('lodash') +import csvReader from 'fast-csv' +import xmlConverter from 'xml-js' +import chalk from 'chalk' +import path from 'path' +import fs from 'fs' +import lodash from 'lodash' +const { isArray } = lodash /** * @method discoverExtension * @param {String} fileName - Name of the file * @return {String} */ - -const discoverExtension = (fileName) => { +export const discoverExtension = (fileName) => { const extension = path.extname(fileName) if (extension !== '') { @@ -27,7 +27,7 @@ const discoverExtension = (fileName) => { * @param {Object} contents - Object with the content * @return {Promise} */ -const sendContent = async (api, contents) => { +export const sendContent = async (api, contents) => { for (const story of contents) { try { console.log( @@ -66,8 +66,7 @@ const removeJsonTextAttribute = (value, parentElement) => { * @param {String} delimiter - Csv file delimiter, default value is ';' * @return {Promise} */ - -const csvParser = (data, typeOfContent, folderID = 0, delimiter = ';') => { +export const csvParser = (data, typeOfContent, folderID = 0, delimiter = ';') => { return new Promise((resolve, reject) => { console.log() console.log(`${chalk.blue('-')} Reading CSV file... `) @@ -131,8 +130,7 @@ const xmlFactoryOfStories = (line, typeOfContent, folderID) => { * @param {Number} folderID - Storyblok folder id, default value is 0 * @return {Promise} */ - -const xmlParser = async (data, typeOfContent, folderID = 0) => { +export const xmlParser = async (data, typeOfContent, folderID = 0) => { return new Promise((resolve, reject) => { console.log() console.log(`${chalk.blue('-')} Reading XML file... `) @@ -166,8 +164,7 @@ const xmlParser = async (data, typeOfContent, folderID = 0) => { * @param {Number} folderID - Storyblok folder id, default value is 0 * @return {Promise} */ - -const jsonParser = async (data, typeOfContent, folderID = 0) => { +export const jsonParser = async (data, typeOfContent, folderID = 0) => { console.log() console.log(`${chalk.blue('-')} Reading JSON file... `) console.log() @@ -201,7 +198,7 @@ const jsonParser = async (data, typeOfContent, folderID = 0) => { * @param {string} extension file extension * @param {ConvertFileOptions} options options to parser functions */ -const convertFile = (file, extension, options) => { +export const convertFile = (file, extension, options) => { const { type, folder, @@ -223,12 +220,3 @@ const convertFile = (file, extension, options) => { return Promise.reject(new Error('This file extension is not supported. Please use .xml, .json or .csv')) } - -module.exports = { - csvParser, - xmlParser, - jsonParser, - sendContent, - convertFile, - discoverExtension -} diff --git a/src/tasks/index.js b/src/tasks/index.js index c6a2e277..25543f55 100644 --- a/src/tasks/index.js +++ b/src/tasks/index.js @@ -1,16 +1,33 @@ -module.exports = { - sync: require('./sync'), - scaffold: require('./scaffold'), - quickstart: require('./quickstart'), - pullComponents: require('./pull-components'), - pullLanguages: require('./pull-languages'), - pushComponents: require('./push-components'), - generateMigration: require('./migrations/generate'), - runMigration: require('./migrations/run'), - rollbackMigration: require('./migrations/rollback'), - listSpaces: require('./list-spaces'), - importFiles: require('./import/import'), - deleteComponent: require('./delete-component'), - deleteComponents: require('./delete-components'), - deleteDatasources: require('./delete-datasources') +import sync from './sync' +import scaffold from './scaffold' +import quickstart from './quickstart' +import pullComponents from './pull-components' +import pullLanguages from './pull-languages' +import pushComponents from './push-components' +import generateMigration from './migrations/generate' +import runMigration from './migrations/run' +import rollbackMigration from './migrations/rollback' +import listSpaces from './list-spaces' +import importFiles from './import/import' +import deleteComponent from './delete-component' +import deleteComponents from './delete-components' +import deleteDatasources from './delete-datasources' +import generateTypescriptTypedefs from './generate-typescript-typedefs' + +export default { + sync, + scaffold, + quickstart, + pullComponents, + pullLanguages, + pushComponents, + generateMigration, + runMigration, + rollbackMigration, + listSpaces, + importFiles, + deleteComponent, + deleteComponents, + deleteDatasources, + generateTypescriptTypedefs } diff --git a/src/tasks/list-spaces.js b/src/tasks/list-spaces.js index e76a0539..56f5d1b0 100644 --- a/src/tasks/list-spaces.js +++ b/src/tasks/list-spaces.js @@ -1,5 +1,6 @@ -const chalk = require('chalk') -const { ALL_REGIONS, getRegionName, CN_CODE } = require('@storyblok/region-helper') +import chalk from 'chalk' +import { ALL_REGIONS, getRegionName, CN_CODE } from '@storyblok/region-helper' + /** * @method listSpaces * @param api - Pass the api instance as a parameter @@ -60,4 +61,4 @@ const listSpaces = async (api, currentRegion) => { } } -module.exports = listSpaces +export default listSpaces diff --git a/src/tasks/migrations/generate.js b/src/tasks/migrations/generate.js index 1c780229..5cd36272 100644 --- a/src/tasks/migrations/generate.js +++ b/src/tasks/migrations/generate.js @@ -1,14 +1,6 @@ -const chalk = require('chalk') -const inquirer = require('inquirer') - -const { - getPathToFile, - checkFileExists, - getInquirerOptions, - createMigrationFile, - checkComponentExists, - getNameOfMigrationFile -} = require('./utils') +import chalk from 'chalk' +import inquirer from 'inquirer' +import { getPathToFile, checkFileExists, getInquirerOptions, createMigrationFile, checkComponentExists, getNameOfMigrationFile } from './utils' /** * @method generateMigration @@ -58,4 +50,4 @@ const generateMigration = async (api, component, field) => { } } -module.exports = generateMigration +export default generateMigration diff --git a/src/tasks/migrations/rollback.js b/src/tasks/migrations/rollback.js index a80810cf..9c0771fe 100644 --- a/src/tasks/migrations/rollback.js +++ b/src/tasks/migrations/rollback.js @@ -1,10 +1,7 @@ -const chalk = require('chalk') -const fs = require('fs-extra') +import chalk from 'chalk' +import fs from 'fs-extra' +import { checkExistenceFilesInRollBackDirectory, urlTofRollbackMigrationFile } from './utils' const MIGRATIONS_ROLLBACK_DIRECTORY = `${process.cwd()}/migrations/rollback` -const { - checkExistenceFilesInRollBackDirectory, - urlTofRollbackMigrationFile -} = require('./utils') /** * @method rollbackMigration @@ -67,4 +64,4 @@ const rollbackMigration = async (api, field, component) => { } } -module.exports = rollbackMigration +export default rollbackMigration diff --git a/src/tasks/migrations/run.js b/src/tasks/migrations/run.js index e3113a2b..f7a5237f 100644 --- a/src/tasks/migrations/run.js +++ b/src/tasks/migrations/run.js @@ -1,14 +1,9 @@ -const chalk = require('chalk') -const { isEmpty, cloneDeep, isEqual } = require('lodash') - -const { - getPathToFile, - checkFileExists, - processMigration, - getStoriesByComponent, - getNameOfMigrationFile, - createRollbackFile -} = require('./utils') +import chalk from 'chalk' +import lodash from 'lodash' +import { getPathToFile, checkFileExists, processMigration, getStoriesByComponent, getNameOfMigrationFile, createRollbackFile } from './utils' + +// Separate import because apparently `cloneDeep` is not exported as named export +const { isEmpty, cloneDeep, isEqual} = lodash /** * @method isStoryPublishedWithoutChanges @@ -64,7 +59,7 @@ const runMigration = async (api, component, field, options = {}) => { console.log( `${chalk.blue('-')} Getting the user defined migration function` ) - const migrationFn = require(pathToFile) + const migrationFn = (await import(pathToFile)).default; if (typeof migrationFn !== 'function') { throw new Error("The migration file doesn't export a function") @@ -156,4 +151,4 @@ const runMigration = async (api, component, field, options = {}) => { } } -module.exports = runMigration +export default runMigration diff --git a/src/tasks/migrations/utils.js b/src/tasks/migrations/utils.js index 3839f770..37328ef3 100644 --- a/src/tasks/migrations/utils.js +++ b/src/tasks/migrations/utils.js @@ -1,10 +1,11 @@ -const onChange = require('on-change') -const { isArray, isPlainObject, has, isEmpty, template, truncate } = require('lodash') -const fs = require('fs-extra') -const chalk = require('chalk') +import onChange from 'on-change' +import lodash from 'lodash' +import fs from 'fs-extra' +import chalk from 'chalk' +import { parseError } from '../../utils' +import migrationTemplate from '../templates/migration-file' +const { isArray, isPlainObject, has, isEmpty, template, truncate } = lodash -const { parseError } = require('../../utils') -const migrationTemplate = require('../templates/migration-file') const MIGRATIONS_DIRECTORY = `${process.cwd()}/migrations` const MIGRATIONS_ROLLBACK_DIRECTORY = `${process.cwd()}/migrations/rollback` @@ -21,7 +22,7 @@ const MIGRATIONS_ROLLBACK_DIRECTORY = `${process.cwd()}/migrations/rollback` * // ./migrations/change_teaser_subtitle.js * getPathToFile('change_teaser_subtitle.js', './migrations') */ -const getPathToFile = (fileName, migrationPath = null) => { +export const getPathToFile = (fileName, migrationPath = null) => { const pathTo = isEmpty(migrationPath) ? MIGRATIONS_DIRECTORY : migrationPath return `${pathTo}/${fileName}` @@ -36,7 +37,7 @@ const getPathToFile = (fileName, migrationPath = null) => { * @example * getNameOfMigrationFile('product', 'price') // change_product_price */ -const getNameOfMigrationFile = (component, field) => { +export const getNameOfMigrationFile = (component, field) => { return `change_${component}_${field}.js` } @@ -46,7 +47,7 @@ const getNameOfMigrationFile = (component, field) => { * @param {String} component name of component * @return {Promise} */ -const getStoriesByComponent = async (api, componentName) => { +export const getStoriesByComponent = async (api, componentName) => { try { const stories = await api.getStories({ contain_component: componentName @@ -67,7 +68,7 @@ const getStoriesByComponent = async (api, componentName) => { * @param {String} component name of component * @return {Promise} */ -const getComponentsFromName = async (api, componentName) => { +export const getComponentsFromName = async (api, componentName) => { try { const components = await api.getComponents() @@ -94,7 +95,7 @@ const getComponentsFromName = async (api, componentName) => { * @param {String} component name of component * @return {Promise} */ -const checkComponentExists = async (api, component) => { +export const checkComponentExists = async (api, component) => { try { const componentData = await getComponentsFromName(api, component) @@ -110,7 +111,7 @@ const checkComponentExists = async (api, component) => { * @param {String} filePath * @return {Promise} */ -const checkFileExists = async (filePath) => fs.pathExists(filePath) +export const checkFileExists = async (filePath) => fs.pathExists(filePath) /** * @method createMigrationFile @@ -118,7 +119,7 @@ const checkFileExists = async (filePath) => fs.pathExists(filePath) * @param {String} field name of the field * @return {Promise} */ -const createMigrationFile = (fileName, field) => { +export const createMigrationFile = (fileName, field) => { console.log(`${chalk.blue('-')} Creating the migration file in migrations folder`) // use lodash.template to replace the occurrences of fieldname @@ -137,7 +138,7 @@ const createMigrationFile = (fileName, field) => { * @param {String} type * @return {Array} */ -const getInquirerOptions = (type) => { +export const getInquirerOptions = (type) => { if (type === 'file-exists') { return [{ type: 'confirm', @@ -155,7 +156,7 @@ const getInquirerOptions = (type) => { * @param {unknown} value updated value * @param {unknown} oldValue previous value */ -const showMigrationChanges = (path, value, oldValue) => { +export const showMigrationChanges = (path, value, oldValue) => { // It was created a new field if (oldValue === undefined) { // truncate the string with more than 30 characters @@ -189,7 +190,7 @@ const showMigrationChanges = (path, value, oldValue) => { * @param {String} storyFullSlug the full slug of the containing story * @return {Promise} */ -const processMigration = async (content = {}, component = '', migrationFn, storyFullSlug) => { +export const processMigration = async (content = {}, component = '', migrationFn, storyFullSlug) => { // I'm processing the component that I want if (content.component === component) { const watchedContent = onChange( @@ -242,7 +243,7 @@ const processMigration = async (content = {}, component = '', migrationFn, story * @return {String} */ -const urlTofRollbackMigrationFile = (component, field) => { +export const urlTofRollbackMigrationFile = (component, field) => { return `${MIGRATIONS_ROLLBACK_DIRECTORY}/${getNameOfRollbackMigrationFile(component, field)}` } @@ -253,7 +254,7 @@ const urlTofRollbackMigrationFile = (component, field) => { * @return {String} */ -const getNameOfRollbackMigrationFile = (component, field) => { +export const getNameOfRollbackMigrationFile = (component, field) => { return `rollback_${component}_${field}.json` } @@ -263,7 +264,7 @@ const getNameOfRollbackMigrationFile = (component, field) => { * @return {Promise} */ -const createRollbackFile = async (stories, component, field) => { +export const createRollbackFile = async (stories, component, field) => { try { if (!fs.existsSync(MIGRATIONS_ROLLBACK_DIRECTORY)) { fs.mkdir(MIGRATIONS_ROLLBACK_DIRECTORY) @@ -299,7 +300,7 @@ const createRollbackFile = async (stories, component, field) => { * @return {Promisse} */ -const checkExistenceFilesInRollBackDirectory = (path, component, field) => { +export const checkExistenceFilesInRollBackDirectory = (path, component, field) => { if (!fs.existsSync(path)) { console.log(` ${chalk.red('X')} The path for which the rollback files should be contained does not exist` @@ -317,20 +318,3 @@ const checkExistenceFilesInRollBackDirectory = (path, component, field) => { }) return Promise.resolve(file) } - -module.exports = { - getPathToFile, - checkFileExists, - processMigration, - getInquirerOptions, - createMigrationFile, - checkComponentExists, - showMigrationChanges, - getStoriesByComponent, - getComponentsFromName, - getNameOfMigrationFile, - createRollbackFile, - checkExistenceFilesInRollBackDirectory, - urlTofRollbackMigrationFile, - getNameOfRollbackMigrationFile -} diff --git a/src/tasks/pull-components.js b/src/tasks/pull-components.js index 08153f1c..d0b97445 100644 --- a/src/tasks/pull-components.js +++ b/src/tasks/pull-components.js @@ -1,5 +1,5 @@ -const chalk = require('chalk') -const saveFileFactory = require('../utils/save-file-factory') +import chalk from 'chalk' +import saveFileFactory from '../utils/save-file-factory' /** * @method getNameFromComponentGroups @@ -81,4 +81,4 @@ const pullComponents = async (api, options) => { } } -module.exports = pullComponents +export default pullComponents diff --git a/src/tasks/pull-languages.js b/src/tasks/pull-languages.js index 7dead35a..c9622b5c 100644 --- a/src/tasks/pull-languages.js +++ b/src/tasks/pull-languages.js @@ -1,5 +1,5 @@ -const fs = require('fs') -const chalk = require('chalk') +import fs from 'fs' +import chalk from 'chalk' /** * @method pullLanguages @@ -36,4 +36,4 @@ const pullLanguages = async (api, options) => { } } -module.exports = pullLanguages +export default pullLanguages diff --git a/src/tasks/push-components.js b/src/tasks/push-components.js index 6011ca39..69875e9f 100644 --- a/src/tasks/push-components.js +++ b/src/tasks/push-components.js @@ -1,8 +1,9 @@ -const axios = require('axios') -const fs = require('fs') -const chalk = require('chalk') -const PresetsLib = require('../utils/presets-lib') -const isEmpty = require('lodash/isEmpty') +import axios from 'axios' +import fs from 'fs' +import chalk from 'chalk' +import PresetsLib from '../utils/presets-lib' +import lodash from 'lodash' +const { isEmpty } = lodash const isUrl = source => source.indexOf('http') === 0 @@ -62,7 +63,7 @@ const createContentList = (content, key) => { else return !isEmpty(content) ? [content] : [] } -module.exports = async (api, { source, presetsSource }) => { +const pushComponents = async (api, { source, presetsSource }) => { try { const rawComponents = await getDataFromPath(source) const components = createContentList(rawComponents, 'components') @@ -205,3 +206,5 @@ const push = async (api, components, presets = []) => { return Promise.reject(e.message) } } + +export default pushComponents diff --git a/src/tasks/quickstart.js b/src/tasks/quickstart.js index 82c968c7..9e8bfa69 100644 --- a/src/tasks/quickstart.js +++ b/src/tasks/quickstart.js @@ -1,6 +1,6 @@ -const open = require('open') -const chalk = require('chalk') -const lastStep = require('../utils/last-step') +import open from 'open' +import chalk from 'chalk' +import lastStep from '../utils/last-step' const hasSpaceId = spaceId => typeof spaceId !== 'undefined' @@ -88,4 +88,4 @@ const quickstart = async (api, answers, spaceId) => { } } -module.exports = quickstart +export default quickstart diff --git a/src/tasks/scaffold.js b/src/tasks/scaffold.js index 50682e27..99710a13 100644 --- a/src/tasks/scaffold.js +++ b/src/tasks/scaffold.js @@ -1,6 +1,6 @@ -const fs = require('fs') +import fs from 'fs' -module.exports = function (api, name, space) { +export default async function (api, name, space) { if (space) { if (!api.accessToken) { return Promise.reject(new Error('The user is not logged')) @@ -17,7 +17,7 @@ module.exports = function (api, name, space) { var liquid = './views/components/_' + name + '.liquid' console.log('Writing template file to ' + liquid) - fs.writeFileSync(liquid, require('./templates/teaser')) + fs.writeFileSync(liquid, await import('./templates/teaser')) var scss = './source/scss/components/below/_' + name + '.scss' console.log('Writing scss file to ' + scss) diff --git a/src/tasks/sync-commands/component-groups.js b/src/tasks/sync-commands/component-groups.js index 4fcb1506..acd7b56b 100644 --- a/src/tasks/sync-commands/component-groups.js +++ b/src/tasks/sync-commands/component-groups.js @@ -1,6 +1,6 @@ -const chalk = require('chalk') -const { findByProperty } = require('../../utils') -const api = require('../../utils/api') +import chalk from 'chalk' +import { findByProperty } from '../../utils' +import api from '../../utils/api' class SyncComponentGroups { /** @@ -137,4 +137,4 @@ class SyncComponentGroups { } } -module.exports = SyncComponentGroups +export default SyncComponentGroups diff --git a/src/tasks/sync-commands/components.js b/src/tasks/sync-commands/components.js index 34f77580..5c9beed0 100644 --- a/src/tasks/sync-commands/components.js +++ b/src/tasks/sync-commands/components.js @@ -1,9 +1,10 @@ -const chalk = require('chalk') -const { find } = require('lodash') -const SyncComponentGroups = require('./component-groups') -const { findByProperty } = require('../../utils') -const PresetsLib = require('../../utils/presets-lib') -const api = require('../../utils/api') +import chalk from 'chalk' +import lodash from 'lodash' +import SyncComponentGroups from './component-groups' +import { findByProperty } from '../../utils' +import PresetsLib from '../../utils/presets-lib' +import api from '../../utils/api' +const { find } = lodash class SyncComponents { /** @@ -260,4 +261,4 @@ class SyncComponents { } } -module.exports = SyncComponents +export default SyncComponents diff --git a/src/tasks/sync-commands/datasources.js b/src/tasks/sync-commands/datasources.js index 71d021a7..2321ddd7 100644 --- a/src/tasks/sync-commands/datasources.js +++ b/src/tasks/sync-commands/datasources.js @@ -1,6 +1,6 @@ -const chalk = require('chalk') -const UUID = require('simple-uuid') -const api = require('../../utils/api') +import chalk from 'chalk' +import UUID from 'simple-uuid' +import api from '../../utils/api' class SyncDatasources { /** @@ -302,4 +302,4 @@ class SyncDatasources { } } -module.exports = SyncDatasources +export default SyncDatasources diff --git a/src/tasks/sync.js b/src/tasks/sync.js index d0b5f334..3ec6b97f 100644 --- a/src/tasks/sync.js +++ b/src/tasks/sync.js @@ -1,8 +1,8 @@ -const pSeries = require('p-series') -const chalk = require('chalk') -const SyncComponents = require('./sync-commands/components') -const SyncDatasources = require('./sync-commands/datasources') -const { capitalize } = require('../utils') +import pSeries from 'p-series' +import chalk from 'chalk' +import SyncComponents from './sync-commands/components' +import SyncDatasources from './sync-commands/datasources' +import { capitalize } from '../utils' const SyncSpaces = { targetComponents: [], @@ -230,7 +230,7 @@ const SyncSpaces = { oauthToken: this.oauthToken, componentsGroups: this.componentsGroups }) - + try { await syncComponentsInstance.sync() } catch (e) { @@ -285,4 +285,4 @@ const sync = (types, options) => { return pSeries(tasks) } -module.exports = sync +export default sync diff --git a/src/tasks/templates/migration-file.js b/src/tasks/templates/migration-file.js index 80c6bab1..44b2cdf0 100644 --- a/src/tasks/templates/migration-file.js +++ b/src/tasks/templates/migration-file.js @@ -1,4 +1,4 @@ -module.exports = `module.exports = function (block) { +export default `export default function (block) { // Example to change a string to boolean // block.{{ fieldname }} = !!(block.{{ fieldname }}) diff --git a/src/tasks/templates/teaser.js b/src/tasks/templates/teaser.js index 78889cad..10621f31 100644 --- a/src/tasks/templates/teaser.js +++ b/src/tasks/templates/teaser.js @@ -1,4 +1,4 @@ -module.exports = `
+export default `