diff --git a/alfred/npm.py b/alfred/npm.py index 7a4f52584..e6946dfcb 100644 --- a/alfred/npm.py +++ b/alfred/npm.py @@ -5,7 +5,8 @@ @alfred.command("npm.lint", help="lint check npm packages") def npm_lint(): - alfred.run("npm run ui:lint:ci") + alfred.run("npm run ui:lint.ci") + alfred.run("npm run ui:custom.check") @alfred.command("npm.e2e", help="run e2e tests") @alfred.option('--browser', '-b', help="run e2e tests on specified browser", default='chromium') diff --git a/docs/framework/custom-components.mdx b/docs/framework/custom-components.mdx index a37c26417..5ce899008 100644 --- a/docs/framework/custom-components.mdx +++ b/docs/framework/custom-components.mdx @@ -165,13 +165,18 @@ A single or multiple templates can be specified. Take into account that they wil ## Bundling templates -Execute `npm run custom.build`, this will generate the output `.js` and `.css` files into `ui/custom_components_dist`. +Execute `npm run custom.build` into `src/ui`, this will generate the output `.js` and `.css` files into `./custom_components_dist`. ```sh # "build" builds the entire front-end # "custom.build" only builds the custom templates +npm run custom.check # Optional: checks certain issues on custom components npm run custom.build ``` -Collect the files from `ui/custom_components_dist` and pack them in a folder such as `my_custom_bubbles`. The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup. +Collect the files from `./custom_components_dist` and pack them in a folder such as `my_custom_bubbles`. The folder containing the generated files, e.g. `my_custom_bubbles`, can now be placed in the `extensions/` folder of any Framework project. It'll be automatically detected during server startup. + + +The `custom.check` command is optional, but it's recommended to run it before building the custom components. It checks for common issues in the custom components, such as invalid key declaration, ... + diff --git a/package.json b/package.json index 2d749778c..2215771c0 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,9 @@ "ui:build": "npm run -w writer-ui build", "ui:preview": "npm run -w writer-ui preview", "ui:custom.build": "npm run -w writer-ui custom.build", + "ui:custom.check": "npm run -w writer-ui custom.check", "ui:lint": "npm run -w writer-ui lint", - "ui:lint:ci": "npm run -w writer-ui lint:ci", + "ui:lint.ci": "npm run -w writer-ui lint.ci", "docs:codegen": "npm run -w writer-docs codegen", "docs:preview": "npm run -w writer-docs preview", diff --git a/src/ui/package.json b/src/ui/package.json index 590dcfd78..aabcd24cd 100644 --- a/src/ui/package.json +++ b/src/ui/package.json @@ -7,10 +7,11 @@ "build": "vite build", "preview": "vite preview --port 5050", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path ../../.gitignore --ignore-path .gitignore", - "lint:ci": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path ../../.gitignore --ignore-path .gitignore", + "lint.ci": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --ignore-path ../../.gitignore --ignore-path .gitignore", "codegen": "node tools/generator.mjs", "custom.dev": "vite --config vite.config.custom.ts", "custom.build": "vite build --config vite.config.custom.ts", + "custom.check": "node tools/custom_check.mjs", "storybook": "storybook dev -p 6006", "storybook.build": "storybook build" }, @@ -61,4 +62,4 @@ "eslint-plugin-storybook": "0.8.0", "storybook": "8.0.5" } -} \ No newline at end of file +} diff --git a/src/ui/src/core/loadExtensions.ts b/src/ui/src/core/loadExtensions.ts index 74f312dbe..80a72fedb 100644 --- a/src/ui/src/core/loadExtensions.ts +++ b/src/ui/src/core/loadExtensions.ts @@ -20,12 +20,33 @@ async function importCustomComponentTemplate(path: string) { await import(/* @vite-ignore */ getRelativeExtensionsPath() + path); Object.entries(window[CUSTOM_COMPONENTS_GLOBAL_VAR])?.forEach( ([key, template]) => { - console.log(`Registering template for "${key}".`); - registerComponentTemplate(`custom_${key}`, template); + if (checkComponentKey(key)) { + registerComponentTemplate(`custom_${key}`, template); + console.log(`Registering template for "${key}".`); + } else { + console.warn( + `custom component '${key}' is ignored. A custom component should be declared using only alphanumeric lowercase and _.`, + ); + } }, ); } +/** + * Checks that the key contains only alphanumeric characters and underscores without capital letters + * The clipboard api use in drag and drop doesn't handle uppercase. + * + * mycomponent : valid + * myComponent : invalid + * myCOMPONENT : invalid + * + * @see https://github.com/writer/writer-framework/issues/517 + */ +export function checkComponentKey(key: string): boolean { + const isValidKey = /^[a-z0-9_]+$/.test(key); + return isValidKey; +} + function loadStylesheet(path: string) { const el: HTMLLinkElement = document.createElement("link"); el.rel = "stylesheet"; @@ -41,3 +62,5 @@ function getRelativeExtensionsPath() { return `${pathname}extensions/`; } + + diff --git a/src/ui/tools/core.mjs b/src/ui/tools/core.mjs index 742d71c28..8219eea0b 100644 --- a/src/ui/tools/core.mjs +++ b/src/ui/tools/core.mjs @@ -28,3 +28,21 @@ export async function loadComponents() { return data; } + +/** + * imports a vue-dependent module. + */ +export async function importVue(modulePath) { + const vite = await createServer({ + includeWriterComponentPath: true, + server: { + middlewareMode: true, + }, + appType: "custom", + }); + + const m = await vite.ssrLoadModule(path.join(__dirname, modulePath)); + await vite.close(); + + return m; +} diff --git a/src/ui/tools/custom_check.mjs b/src/ui/tools/custom_check.mjs new file mode 100644 index 000000000..49b26aa70 --- /dev/null +++ b/src/ui/tools/custom_check.mjs @@ -0,0 +1,42 @@ +import { importVue } from "./core.mjs"; + +async function checkDeclarationKey() { + let hasFailed = false; + const module = await importVue("../src/custom_components/index.ts"); + const { checkComponentKey } = await importVue( + "../src/core/loadExtensions.ts", + ); + const invalidCustomComponentKeys = []; + Object.keys(module.default).forEach((key) => { + if (!checkComponentKey(key)) { + invalidCustomComponentKeys.push(key); + hasFailed = true; + } + }); + + if (invalidCustomComponentKeys.length !== 0) { + // eslint-disable-next-line no-console + console.error( + `ERROR: Invalid component declaration: ${invalidCustomComponentKeys} into 'src/custom_components/index.ts'. Their key must be declared using only lowercase and alphanumeric characters.`, + ); + } + return hasFailed; +} + +/** + * Check the custom components in continuous integration + * + * npm run custom.check + * + */ +async function check() { + let hasFailed = false; + + hasFailed |= await checkDeclarationKey(); + + if (hasFailed) { + process.exit(1); + } +} + +check();