diff --git a/.babelrc b/.babelrc index dd178d38..545d63b2 100644 --- a/.babelrc +++ b/.babelrc @@ -6,6 +6,6 @@ "@babel/plugin-proposal-class-properties" ], "presets": [ - "@babel/preset-env" + ["@babel/preset-env", { "modules": false }] ] } diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 92% rename from .eslintrc.js rename to .eslintrc.cjs index 5215b46d..3489aa3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { 'node': true, 'browser': true }, - 'parser': 'babel-eslint', + 'parser': '@babel/eslint-parser', 'parserOptions': { 'ecmaVersion': 7, 'sourceType': 'module' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c129b659..45aa7761 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,19 +9,28 @@ jobs: build: strategy: matrix: - node-version: ['14', '15'] + node-version: ['lts/*', 'current'] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: yarn install - run: yarn build - run: yarn test - run: yarn lint - - run: yarn test-gen + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + - run: yarn test-gen && yarn test-app - run: yarn test-gen-env + - run: yarn test-gen-openapi3 - run: yarn check + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 1eff5700..a04ba6b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /lib/ /node_modules/ -/tmp +/playwright/.cache/ +/playwright-report/ +/test-results/ +/tmp/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..5a182ef1 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn lint-staged diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..163ab089 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +export default { + runner: "jest-light-runner", +}; diff --git a/package.json b/package.json index 0a7db16d..6f2b17ba 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "src", "templates" ], - "main": "lib/index", + "type": "module", + "exports": "./lib/index.js", "repository": "api-platform/client-generator", "homepage": "https://github.com/api-platform/client-generator", "bugs": "https://github.com/api-platform/client-generator/issues", @@ -18,35 +19,40 @@ "devDependencies": { "@babel/cli": "^7.0.0", "@babel/core": "^7.0.0", + "@babel/eslint-parser": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", "@babel/plugin-transform-flow-strip-types": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/preset-env": "^7.6.0", - "babel-eslint": "^10.0.0", - "babel-jest": "^26.0.0", - "eslint": "^7.17.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-import": "^2.14.0", - "eslint-plugin-prettier": "^3.3.0", - "husky": "^4.3.6", - "jest": "^26.6.3", - "lint-staged": "^10.5.3", + "@playwright-testing-library/test": "4.3.0-beta.1", + "@playwright/test": "^1.25.0", + "babel-jest": "^28.1.0", + "eslint": "^8.22.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.2.0", + "husky": "^8.0.0", + "jest": "^28.1.0", + "jest-light-runner": "^0.3.0", + "lint-staged": "^13.0.0", + "start-server-and-test": "^1.14.0", "tmp": "^0.2.1" }, "dependencies": { - "@api-platform/api-doc-parser": "^0.12.0", + "@api-platform/api-doc-parser": "^0.15.0", "@babel/runtime": "^7.0.0", - "chalk": "^4.1.0", - "commander": "^8.2.0", + "chalk": "^5.0.0", + "commander": "^9.4.0", "handlebars": "^4.0.12", "handlebars-helpers": "^0.10.0", "isomorphic-fetch": "^3.0.0", "mkdirp": "^1.0.4", - "prettier": "^2.2.1", + "prettier": "^2.7.0", "sprintf-js": "^1.1.1" }, "scripts": { + "prepare": "husky install", "test": "jest src", "lint": "eslint src", "fix": "eslint --fix src", @@ -55,16 +61,12 @@ "watch": "babel --watch src -d lib --ignore '*.test.js'", "test-gen": "rm -rf ./tmp && yarn build && ./lib/index.js https://demo.api-platform.com ./tmp/react -g react && ./lib/index.js https://demo.api-platform.com ./tmp/react-native -g react-native && ./lib/index.js https://demo.api-platform.com ./tmp/next -g next && ./lib/index.js https://demo.api-platform.com ./tmp/vue -g vue", "test-gen-custom": "rm -rf ./tmp && yarn build && babel src/generators/ReactGenerator.js src/generators/BaseGenerator.js -d ./tmp/gens && cp -r ./templates/react ./templates/react-common ./templates/entrypoint.js ./tmp/gens && ./lib/index.js https://demo.api-platform.com ./tmp/react-custom -g \"$(pwd)/tmp/gens/ReactGenerator.js\" -t ./tmp/gens", - "test-gen-swagger": "rm -rf ./tmp && yarn build && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react -f swagger && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react-native -g react-native -f swagger && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/vue -g vue -f swagger", - "test-gen-env": "rm -rf ./tmp && yarn build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js" - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } + "test-gen-openapi3": "rm -rf ./tmp && yarn build && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react -f openapi3 && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/react-native -g react-native -f openapi3 && ./lib/index.js https://demo.api-platform.com/docs.json ./tmp/vue -g vue -f openapi3", + "test-gen-env": "rm -rf ./tmp && yarn build && API_PLATFORM_CLIENT_GENERATOR_ENTRYPOINT=https://demo.api-platform.com API_PLATFORM_CLIENT_GENERATOR_OUTPUT=./tmp ./lib/index.js", + "test-app": "rm -rf ./tmp/app && mkdir -p ./tmp/app && yarn create next-app --typescript ./tmp/app/next && yarn --cwd ./tmp/app/next add lodash.get lodash.has isomorphic-unfetch formik react-query && yarn --cwd ./tmp/app/next add -D @types/lodash && cp -R ./tmp/next/* ./tmp/app/next && rm ./tmp/app/next/pages/index.tsx && rm -rf ./tmp/app/next/pages/api && yarn --cwd ./tmp/app/next build && start-server-and-test 'yarn --cwd ./tmp/app/next start' http://127.0.0.1:3000/books 'yarn playwright test'" }, "lint-staged": { - "src/**/*.js": "npm run lint" + "src/**/*.js": "yarn lint --fix" }, "bin": { "generate-api-platform-client": "./lib/index.js" diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..57859e6b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,107 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +export default config; diff --git a/src/generators.js b/src/generators.js index c1b40185..91247c1d 100644 --- a/src/generators.js +++ b/src/generators.js @@ -1,12 +1,12 @@ import fs from "fs"; -import NextGenerator from "./generators/NextGenerator"; -import NuxtGenerator from "./generators/NuxtGenerator"; -import ReactGenerator from "./generators/ReactGenerator"; -import ReactNativeGenerator from "./generators/ReactNativeGenerator"; -import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator"; -import VueGenerator from "./generators/VueGenerator"; -import VuetifyGenerator from "./generators/VuetifyGenerator"; -import QuasarGenerator from "./generators/QuasarGenerator"; +import NextGenerator from "./generators/NextGenerator.js"; +import NuxtGenerator from "./generators/NuxtGenerator.js"; +import ReactGenerator from "./generators/ReactGenerator.js"; +import ReactNativeGenerator from "./generators/ReactNativeGenerator.js"; +import TypescriptInterfaceGenerator from "./generators/TypescriptInterfaceGenerator.js"; +import VueGenerator from "./generators/VueGenerator.js"; +import VuetifyGenerator from "./generators/VuetifyGenerator.js"; +import QuasarGenerator from "./generators/QuasarGenerator.js"; function wrap(cl) { return ({ hydraPrefix, templateDirectory }) => diff --git a/src/generators/NextGenerator.js b/src/generators/NextGenerator.js index d0893521..ec9865f1 100644 --- a/src/generators/NextGenerator.js +++ b/src/generators/NextGenerator.js @@ -1,7 +1,7 @@ import chalk from "chalk"; import handlebars from "handlebars"; -import hbh_comparison from "handlebars-helpers/lib/comparison"; -import BaseGenerator from "./BaseGenerator"; +import hbh_comparison from "handlebars-helpers/lib/comparison.js"; +import BaseGenerator from "./BaseGenerator.js"; export default class NextGenerator extends BaseGenerator { constructor(params) { @@ -10,6 +10,7 @@ export default class NextGenerator extends BaseGenerator { this.routeAddedtoServer = false; this.registerTemplates(`next/`, [ // components + "components/common/Layout.tsx", "components/common/Pagination.tsx", "components/common/ReferenceLinks.tsx", "components/foo/List.tsx", @@ -26,6 +27,7 @@ export default class NextGenerator extends BaseGenerator { "pages/foos/[id]/edit.tsx", "pages/foos/index.tsx", "pages/foos/create.tsx", + "pages/_app.tsx", // utils "utils/dataAccess.ts", @@ -97,6 +99,7 @@ export default class NextGenerator extends BaseGenerator { // copy with regular name [ // components + "components/common/Layout.tsx", "components/common/Pagination.tsx", "components/common/ReferenceLinks.tsx", @@ -104,6 +107,9 @@ export default class NextGenerator extends BaseGenerator { "types/collection.ts", "types/item.ts", + // pages + "pages/_app.tsx", + // utils "utils/dataAccess.ts", "utils/mercure.ts", diff --git a/src/generators/NextGenerator.test.js b/src/generators/NextGenerator.test.js index d5cb08f3..686780d3 100644 --- a/src/generators/NextGenerator.test.js +++ b/src/generators/NextGenerator.test.js @@ -1,11 +1,15 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import NextGenerator from "./NextGenerator"; +import NextGenerator from "./NextGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); const generator = new NextGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); afterEach(() => { @@ -43,6 +47,7 @@ describe("generate", () => { "/components/abc/List.tsx", "/components/abc/Show.tsx", "/components/abc/Form.tsx", + "/components/common/Layout.tsx", "/components/common/ReferenceLinks.tsx", "/components/common/Pagination.tsx", "/types/Abc.ts", @@ -52,6 +57,7 @@ describe("generate", () => { "/pages/abcs/[id]/edit.tsx", "/pages/abcs/index.tsx", "/pages/abcs/create.tsx", + "/pages/_app.tsx", "/utils/dataAccess.ts", "/utils/mercure.ts", ].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true)); diff --git a/src/generators/NuxtGenerator.js b/src/generators/NuxtGenerator.js index 647cabb9..33cbb330 100644 --- a/src/generators/NuxtGenerator.js +++ b/src/generators/NuxtGenerator.js @@ -1,5 +1,5 @@ import chalk from "chalk"; -import BaseVueGenerator from "./VueBaseGenerator"; +import BaseVueGenerator from "./VueBaseGenerator.js"; export default class NuxtGenerator extends BaseVueGenerator { constructor(params) { @@ -106,9 +106,9 @@ export default class NuxtGenerator extends BaseVueGenerator { this.createEntrypoint(api.entrypoint, `${dir}/config/entrypoint.js`); - for (let dir of [`${dir}/components/${lc}`, `${dir}/pages/${lc}s`]) { + [`${dir}/components/${lc}`, `${dir}/pages/${lc}s`].forEach((dir) => { this.createDir(dir); - } + }); this.createFile("services/api.js", `${dir}/services/api.js`, {}, false); diff --git a/src/generators/NuxtGenerator.test.js b/src/generators/NuxtGenerator.test.js index 371d4453..f918e073 100644 --- a/src/generators/NuxtGenerator.test.js +++ b/src/generators/NuxtGenerator.test.js @@ -1,11 +1,15 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import NuxtGenerator from "./NuxtGenerator"; +import NuxtGenerator from "./NuxtGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); const generator = new NuxtGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); afterEach(() => { diff --git a/src/generators/QuasarGenerator.js b/src/generators/QuasarGenerator.js index 4dbc8e8e..6bf7797c 100644 --- a/src/generators/QuasarGenerator.js +++ b/src/generators/QuasarGenerator.js @@ -1,9 +1,9 @@ import chalk from "chalk"; -import BaseGenerator from "./BaseGenerator"; import handlebars from "handlebars"; -import hbh_comparison from "handlebars-helpers/lib/comparison"; -import hbh_array from "handlebars-helpers/lib/array"; -import hbh_string from "handlebars-helpers/lib/string"; +import hbh_comparison from "handlebars-helpers/lib/comparison.js"; +import hbh_array from "handlebars-helpers/lib/array.js"; +import hbh_string from "handlebars-helpers/lib/string.js"; +import BaseGenerator from "./BaseGenerator.js"; export default class extends BaseGenerator { constructor(params) { @@ -403,7 +403,7 @@ export const store = new Vuex.Store({ // Create directories // These directories may already exist - for (let dir of [ + [ `${dir}/config`, `${dir}/error`, `${dir}/router`, @@ -420,11 +420,9 @@ export const store = new Vuex.Store({ `${dir}/common/store/list`, `${dir}/common/store/show`, `${dir}/common/store/update`, - ]) { - this.createDir(dir, false); - } + ].forEach((dir) => this.createDir(dir, false)); - for (let dir of [ + [ `${dir}/store/modules/${lc}`, `${dir}/store/modules/${lc}/create`, `${dir}/store/modules/${lc}/delete`, @@ -433,11 +431,9 @@ export const store = new Vuex.Store({ `${dir}/store/modules/${lc}/update`, `${dir}/components/${lc}`, - ]) { - this.createDir(dir); - } + ].forEach((dir) => this.createDir(dir)); - for (let common of [ + [ "common/components/index.js", "common/components/ActionCell.vue", "common/components/Breadcrumb.vue", @@ -482,11 +478,11 @@ export const store = new Vuex.Store({ "utils/dates.js", "utils/notify.js", "utils/vuexer.js", - ]) { - this.createFile(common, `${dir}/${common}`, context, false); - } + ].forEach((common) => + this.createFile(common, `${dir}/${common}`, context, false) + ); - for (let pattern of [ + [ // modules "store/modules/%s/index.js", "store/modules/%s/create/actions.js", @@ -530,15 +526,15 @@ export const store = new Vuex.Store({ // routes "router/%s.js", - ]) { + ].forEach((pattern) => { if ( pattern === "components/%s/Filter.vue" && !context.parameters.length ) { - continue; + return; } this.createFileFromPattern(pattern, dir, lc, context); - } + }); // error this.createFile( diff --git a/src/generators/QuasarGenerator.test.js b/src/generators/QuasarGenerator.test.js index 392a4673..f159fc97 100644 --- a/src/generators/QuasarGenerator.test.js +++ b/src/generators/QuasarGenerator.test.js @@ -1,12 +1,16 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import QuasarGenerator from "./QuasarGenerator"; +import QuasarGenerator from "./QuasarGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a Quasar app", () => { const generator = new QuasarGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/ReactGenerator.js b/src/generators/ReactGenerator.js index fb740a0e..c637ad89 100644 --- a/src/generators/ReactGenerator.js +++ b/src/generators/ReactGenerator.js @@ -1,5 +1,5 @@ import chalk from "chalk"; -import BaseGenerator from "./BaseGenerator"; +import BaseGenerator from "./BaseGenerator.js"; export default class extends BaseGenerator { constructor(params) { diff --git a/src/generators/ReactGenerator.test.js b/src/generators/ReactGenerator.test.js index 049ff195..a81f9ca7 100644 --- a/src/generators/ReactGenerator.test.js +++ b/src/generators/ReactGenerator.test.js @@ -1,12 +1,16 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import ReactGenerator from "./ReactGenerator"; +import ReactGenerator from "./ReactGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a React app", () => { const generator = new ReactGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/ReactNativeGenerator.js b/src/generators/ReactNativeGenerator.js index 034727d6..72fa6401 100644 --- a/src/generators/ReactNativeGenerator.js +++ b/src/generators/ReactNativeGenerator.js @@ -1,6 +1,6 @@ import chalk from "chalk"; import handlebars from "handlebars"; -import BaseGenerator from "./BaseGenerator"; +import BaseGenerator from "./BaseGenerator.js"; export default class extends BaseGenerator { constructor(params) { diff --git a/src/generators/ReactNativeGenerator.test.js b/src/generators/ReactNativeGenerator.test.js index 011e9aaf..da48f1c9 100644 --- a/src/generators/ReactNativeGenerator.test.js +++ b/src/generators/ReactNativeGenerator.test.js @@ -1,12 +1,16 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import ReactNativeGenerator from "./ReactNativeGenerator"; +import ReactNativeGenerator from "./ReactNativeGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a React app", () => { const generator = new ReactNativeGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/TypescriptInterfaceGenerator.js b/src/generators/TypescriptInterfaceGenerator.js index 0b0cff78..5a6bec3e 100644 --- a/src/generators/TypescriptInterfaceGenerator.js +++ b/src/generators/TypescriptInterfaceGenerator.js @@ -1,4 +1,4 @@ -import BaseGenerator from "./BaseGenerator"; +import BaseGenerator from "./BaseGenerator.js"; export default class TypescriptInterfaceGenerator extends BaseGenerator { constructor(params) { diff --git a/src/generators/TypescriptInterfaceGenerator.test.js b/src/generators/TypescriptInterfaceGenerator.test.js index 40eef1a4..98358dca 100644 --- a/src/generators/TypescriptInterfaceGenerator.test.js +++ b/src/generators/TypescriptInterfaceGenerator.test.js @@ -1,11 +1,15 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import TypescriptInterfaceGenerator from "./TypescriptInterfaceGenerator"; +import TypescriptInterfaceGenerator from "./TypescriptInterfaceGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a typescript interface", () => { const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); @@ -64,7 +68,7 @@ test("Generate a typescript interface", () => { test("Generate a typescript interface without references to other interfaces", () => { const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); @@ -114,7 +118,7 @@ test("Generate a typescript interface without references to other interfaces", ( test("Generate a typescript interface with an explicit id field in the readableFields", () => { const generator = new TypescriptInterfaceGenerator({ - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/VueBaseGenerator.js b/src/generators/VueBaseGenerator.js index 4b4cc6f5..fd16924b 100644 --- a/src/generators/VueBaseGenerator.js +++ b/src/generators/VueBaseGenerator.js @@ -1,9 +1,9 @@ -import BaseGenerator from "./BaseGenerator"; import handlebars from "handlebars"; -import hbh_comparison from "handlebars-helpers/lib/comparison"; -import hbh_array from "handlebars-helpers/lib/array"; -import hbh_string from "handlebars-helpers/lib/string"; +import hbh_comparison from "handlebars-helpers/lib/comparison.js"; +import hbh_array from "handlebars-helpers/lib/array.js"; +import hbh_string from "handlebars-helpers/lib/string.js"; import { sprintf } from "sprintf-js"; +import BaseGenerator from "./BaseGenerator.js"; export default class extends BaseGenerator { constructor(params) { diff --git a/src/generators/VueBaseGenerator.test.js b/src/generators/VueBaseGenerator.test.js index c0af8aea..04e9d6ad 100644 --- a/src/generators/VueBaseGenerator.test.js +++ b/src/generators/VueBaseGenerator.test.js @@ -1,12 +1,16 @@ import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import VueBaseGenerator from "./VueBaseGenerator"; +import VueBaseGenerator from "./VueBaseGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Test VueBaseGenerator", () => { const generator = new VueBaseGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/VueGenerator.js b/src/generators/VueGenerator.js index 65dc3803..a6344ced 100644 --- a/src/generators/VueGenerator.js +++ b/src/generators/VueGenerator.js @@ -1,5 +1,5 @@ import chalk from "chalk"; -import BaseGenerator from "./BaseGenerator"; +import BaseGenerator from "./BaseGenerator.js"; export default class extends BaseGenerator { constructor(params) { @@ -102,16 +102,11 @@ export const store = new Vuex.Store({ // Create directories // These directories may already exist - for (let dir of [ - `${dir}/config`, - `${dir}/error`, - `${dir}/router`, - `${dir}/utils`, - ]) { - this.createDir(dir, false); - } - - for (let dir of [ + [`${dir}/config`, `${dir}/error`, `${dir}/router`, `${dir}/utils`].forEach( + (dir) => this.createDir(dir, false) + ); + + [ `${dir}/store/modules/${lc}`, `${dir}/store/modules/${lc}/create`, `${dir}/store/modules/${lc}/delete`, @@ -119,11 +114,9 @@ export const store = new Vuex.Store({ `${dir}/store/modules/${lc}/show`, `${dir}/store/modules/${lc}/update`, `${dir}/components/${lc}`, - ]) { - this.createDir(dir); - } + ].forEach((dir) => this.createDir(dir)); - for (let pattern of [ + [ // modules "store/modules/%s/index.js", "store/modules/%s/create/actions.js", @@ -156,9 +149,9 @@ export const store = new Vuex.Store({ // routes "router/%s.js", - ]) { - this.createFileFromPattern(pattern, dir, lc, context); - } + ].forEach((pattern) => + this.createFileFromPattern(pattern, dir, lc, context) + ); // error this.createFile( diff --git a/src/generators/VueGenerator.test.js b/src/generators/VueGenerator.test.js index be75333a..cf617e8a 100644 --- a/src/generators/VueGenerator.test.js +++ b/src/generators/VueGenerator.test.js @@ -1,12 +1,16 @@ -import { Api, Resource, Field } from "@api-platform/api-doc-parser/lib"; +import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import VueGenerator from "./VueGenerator"; +import VueGenerator from "./VueGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a Vue app", () => { const generator = new VueGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/generators/VuetifyGenerator.js b/src/generators/VuetifyGenerator.js index 3dc27476..03a5b2ad 100644 --- a/src/generators/VuetifyGenerator.js +++ b/src/generators/VuetifyGenerator.js @@ -1,5 +1,5 @@ import chalk from "chalk"; -import BaseVueGenerator from "./VueBaseGenerator"; +import BaseVueGenerator from "./VueBaseGenerator.js"; export default class extends BaseVueGenerator { constructor(params) { @@ -85,9 +85,9 @@ export const store = new Vuex.Store({ this.createDir(`${dir}/router`, false); this.createDir(`${dir}/locales`, false); - for (let dir of [`${dir}/components/${lc}`, `${dir}/views/${lc}`]) { - this.createDir(dir); - } + [`${dir}/components/${lc}`, `${dir}/views/${lc}`].forEach((dir) => + this.createDir(dir) + ); this.createFile("locales/en.js", `${dir}/locales/en.js`, context, false); diff --git a/src/generators/VuetifyGenerator.test.js b/src/generators/VuetifyGenerator.test.js index fbd08f87..2f74ceba 100644 --- a/src/generators/VuetifyGenerator.test.js +++ b/src/generators/VuetifyGenerator.test.js @@ -1,12 +1,16 @@ import { Api, Resource, Field } from "@api-platform/api-doc-parser"; +import path from "path"; +import { fileURLToPath } from "url"; import fs from "fs"; import tmp from "tmp"; -import VuetifyGenerator from "./VuetifyGenerator"; +import VuetifyGenerator from "./VuetifyGenerator.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); test("Generate a Vuetify app", () => { const generator = new VuetifyGenerator({ hydraPrefix: "hydra:", - templateDirectory: `${__dirname}/../../templates`, + templateDirectory: `${dirname}/../../templates`, }); const tmpobj = tmp.dirSync({ unsafeCleanup: true }); diff --git a/src/index.js b/src/index.js index a632b9cf..707d21a7 100755 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,22 @@ #!/usr/bin/env node +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; import "isomorphic-fetch"; -import program from "commander"; -import parseHydraDocumentation from "@api-platform/api-doc-parser/lib/hydra/parseHydraDocumentation"; -import parseSwaggerDocumentation from "@api-platform/api-doc-parser/lib/swagger/parseSwaggerDocumentation"; -import parseOpenApi3Documentation from "@api-platform/api-doc-parser/lib/openapi3/parseOpenApi3Documentation"; -import { version } from "../package.json"; -import generators from "./generators"; +import { program } from "commander"; +import apiDocParser from "@api-platform/api-doc-parser"; +import generators from "./generators.js"; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +const packageJson = JSON.parse( + fs.readFileSync(`${dirname}/../package.json`, "utf-8") +); async function main() { program - .version(version) + .version(packageJson.version) .description( "Generate apps built with Next, Nuxt, Quasar, React, React Native, Vue or Vuetify for any API documented using Hydra or OpenAPI" ) @@ -35,7 +41,7 @@ async function main() { .option( "-t, --template-directory [templateDirectory]", "The templates directory base to use. Final directory will be ${templateDirectory}/${generator}", - `${__dirname}/../templates/` + `${dirname}/../templates/` ) .option( "-f, --format [hydra|openapi3|openapi2]", @@ -96,11 +102,14 @@ async function main() { switch (options.format) { case "swagger": // deprecated case "openapi2": - return parseSwaggerDocumentation(entrypointWithSlash); + return apiDocParser.parseSwaggerDocumentation(entrypointWithSlash); case "openapi3": - return parseOpenApi3Documentation(entrypointWithSlash); + return apiDocParser.parseOpenApi3Documentation(entrypointWithSlash); default: - return parseHydraDocumentation(entrypointWithSlash, parserOptions); + return apiDocParser.parseHydraDocumentation( + entrypointWithSlash, + parserOptions + ); } }; diff --git a/templates/next/components/common/Layout.tsx b/templates/next/components/common/Layout.tsx new file mode 100644 index 00000000..a2d41bb9 --- /dev/null +++ b/templates/next/components/common/Layout.tsx @@ -0,0 +1,16 @@ +import { ReactNode, useState } from "react"; +import { DehydratedState, Hydrate, QueryClient, QueryClientProvider } from "react-query"; + +const Layout = ({ children, dehydratedState }: { children: ReactNode, dehydratedState: DehydratedState }) => { + const [queryClient] = useState(() => new QueryClient()); + + return ( + + + {children} + + + ); +} + +export default Layout; diff --git a/templates/next/components/common/Pagination.tsx b/templates/next/components/common/Pagination.tsx index dd316809..a0b3ce24 100644 --- a/templates/next/components/common/Pagination.tsx +++ b/templates/next/components/common/Pagination.tsx @@ -7,7 +7,7 @@ interface Props { const Pagination = ({ collection }: Props) => { const view = collection && collection['{{{hydraPrefix}}}view']; - if (!view) return; + if (!view) return null; const { '{{{hydraPrefix}}}first': first, diff --git a/templates/next/components/foo/Form.tsx b/templates/next/components/foo/Form.tsx index 6625ac11..c4272efb 100644 --- a/templates/next/components/foo/Form.tsx +++ b/templates/next/components/foo/Form.tsx @@ -2,59 +2,89 @@ import { FunctionComponent, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { ErrorMessage, Formik } from "formik"; -import { fetch } from "../../utils/dataAccess"; +import { useMutation } from "react-query"; + +import { fetch, FetchError, FetchResponse } from "../../utils/dataAccess"; import { {{{ucf}}} } from '../../types/{{{ucf}}}'; interface Props { {{{lc}}}?: {{{ucf}}}; } +interface SaveParams { + values: {{{ucf}}}; +} + +interface DeleteParams { + id: string; +} + +const save{{{ucf}}} = async ({ values }: SaveParams) => + await fetch<{{ucf}}>(!values["@id"] ? "/{{{name}}}" : values["@id"], { + method: !values["@id"] ? "POST" : "PUT", + body: JSON.stringify(values), + }); + +const delete{{{ucf}}} = async (id: string) => await fetch<{{ucf}}>(id, { method: "DELETE" }); + export const Form: FunctionComponent = ({ {{{lc}}} }) => { - const [error, setError] = useState(null); + const [, setError] = useState(null); const router = useRouter(); - const handleDelete = async () => { - if (!window.confirm("Are you sure you want to delete this item?")) return; + const saveMutation = useMutation | undefined, Error|FetchError, SaveParams>((saveParams) => save{{{ucf}}}(saveParams)); - try { - await fetch({{{lc}}}['@id'], { method: "DELETE" }); + const deleteMutation = useMutation | undefined, Error|FetchError, DeleteParams>(({ id }) => delete{{{ucf}}}(id), { + onSuccess: () => { router.push("/{{{name}}}"); - } catch (error) { + }, + onError: (error)=> { setError(`Error when deleting the resource: ${error}`); console.error(error); } + }); + + const handleDelete = () => { + if (!{{lc}} || !{{lc}}["@id"]) return; + if (!window.confirm("Are you sure you want to delete this item?")) return; + deleteMutation.mutate({ id: {{lc}}["@id"] }); }; - + return ( -
+

{ {{{lc}}} ? `Edit {{{ucf}}} ${ {{~lc}}['@id']}` : `Create {{{ucf}}}` }

{ + validate={() => { const errors = {}; // add your validation logic here return errors; }} - onSubmit={async (values, { setSubmitting, setStatus, setErrors }) => { + onSubmit={(values, { setSubmitting, setStatus, setErrors }) => { const isCreation = !values["@id"]; - try { - await fetch(isCreation ? "/{{{name}}}" : values["@id"], { - method: isCreation ? "POST" : "PUT", - body: JSON.stringify(values), - }); - setStatus({ - isValid: true, - msg: `Element ${isCreation ? 'created': 'updated'}.`, - }); - router.push("/{{{name}}}"); - } catch (error) { - setStatus({ - isValid: false, - msg: `${error.defaultErrorMsg}`, - }); - setErrors(error.fields); - } - setSubmitting(false); + saveMutation.mutate( + { values }, + { + onSuccess: () => { + setStatus({ + isValid: true, + msg: `Element ${isCreation ? "created" : "updated"}.`, + }); + router.push("/{{{name}}}"); + }, + onError: (error) => { + setStatus({ + isValid: false, + msg: `${error.message}`, + }); + if ("fields" in error) { + setErrors(error.fields); + } + }, + onSettled: ()=> { + setSubmitting(false); + } + } + ); }} > {({ @@ -85,7 +115,7 @@ export const Form: FunctionComponent = ({ {{{lc}}} }) => { placeholder="{{{description}}}" {{#if required}}required={true}{{/if}} className={`form-control${errors.{{name}} && touched.{{name}} ? ' is-invalid' : ''}`} - aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : null} + aria-invalid={errors.{{name}} && touched.{{name~}} ? 'true' : undefined} onChange={handleChange} onBlur={handleBlur} /> diff --git a/templates/next/components/foo/List.tsx b/templates/next/components/foo/List.tsx index 00b9c9e5..197a6b15 100644 --- a/templates/next/components/foo/List.tsx +++ b/templates/next/components/foo/List.tsx @@ -1,6 +1,7 @@ import { FunctionComponent } from "react"; import Link from "next/link"; -import ReferenceLinks from "../../components/common/ReferenceLinks"; + +import ReferenceLinks from "../common/ReferenceLinks"; import { {{{ucf}}} } from '../../types/{{{ucf}}}'; interface Props { @@ -17,28 +18,37 @@ export const List: FunctionComponent = ({ {{{name}}} }) => ( id -{{#each fields}} + {{#each fields}} {{name}} -{{/each}} + {{/each}} { {{{name}}} && ({{{name}}}.length !== 0) && {{{name}}}.map( ( {{{lc}}} ) => ( + {{{lc}}}['@id'] && {{#each fields}} - {{#if reference}}{{else}}{ {{{../lc}}}['{{{name}}}'] }{{/if}} + + {{#if reference}} + + {{else if (compare type "==" "Date") }} + { {{{../lc}}}['{{{name}}}']?.toLocaleString() } + {{else}} + { {{{../lc}}}['{{{name}}}'] } + {{/if}} + {{/each}} - +