diff --git a/packages/x-components/build/build.ts b/packages/x-components/build/build.ts index 5b3d8ddb0e..41b29b90dd 100644 --- a/packages/x-components/build/build.ts +++ b/packages/x-components/build/build.ts @@ -1,6 +1,6 @@ import type { OutputOptions } from 'rollup' import { rollup } from 'rollup' -import { rollupConfig } from './rollup.config' +import rollupConfig from './rollup.config' /** * Entry point for building the project. diff --git a/packages/x-components/build/rollup-plugins/x-components.rollup-plugin.ts b/packages/x-components/build/rollup-plugins/x-components.rollup-plugin.ts deleted file mode 100644 index 331d3b5467..0000000000 --- a/packages/x-components/build/rollup-plugins/x-components.rollup-plugin.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { Plugin } from 'rollup' -import fs from 'node:fs' -import path from 'node:path' -import { forEach } from '@empathyco/x-utils' -import { ensureFilePathExists } from '../build.utils' - -/** - * Type alias of a reducer function that will generate a `Record` where the key is the chunk name, - * and the value is an array of strings containing the code. - */ -type ReducerFunctionOfEntryPoints = ( - files: Record, - line: string, -) => Record - -export interface GenerateEntryFilesOptions { - /** The path where the build will go. */ - buildPath: string - /** The path to the directory where generated js files are stored. */ - jsOutputDir: string - /** The path to the directory where generated .d.ts files are stored. */ - typesOutputDir: string -} - -/** - * Rollup plugin to generate one common entry point with shared code, and one for each x-module. - * This is needed because x-modules have side effects on import (like registering the wiring, and - * store modules). If these x-modules were imported from a single barrel file, then they will be - * executed always, except if the build had tree-shaking step that removed them from the code. Tree - * shaking is a costly step that is only run for production builds normally. So, for consistency - * between dev and prod builds, creating an entry point per each x-module is the safest way to - * achieve this. - * - * @param options - Options to configure the generation of the entry files. - * @returns Rollup plugin for generating project entry files. - */ -export function generateEntryFiles(options: GenerateEntryFilesOptions): Plugin { - return { - name: 'GenerateEntryFiles', - /** - * Takes the generated files in the dist directory, and generates in the root directory: - * - 1 Core entry point for shared code - * - 1 Entry point per x-module - * - 1 Typings file per entry point. - */ - writeBundle() { - generateEntryPoints(options.buildPath, options.jsOutputDir, 'js') - generateEntryPoints(options.buildPath, options.typesOutputDir, 'd.ts') - copyIndexSourcemap(options.buildPath, options.jsOutputDir) - }, - } -} - -/** Regex to split a read file per lines, supporting both Unix and Windows systems. */ -const BY_LINES = /\r?\n/ -/** Name of the x-modules folder. */ -const X_MODULES_DIR_NAME = 'x-modules' - -/** - * Generates an entry point for each x-component, and another one for the shared code. - * - * @param buildPath - The path where the build will go. - * @param outputDirectory - The directory to load it's barrel and generate the entry points. - * @param extension - The type of files to generate the entry points (i.e. `d.ts`, `js`). - */ -function generateEntryPoints(buildPath: string, outputDirectory: string, extension: string): void { - const jsEntry = fs.readFileSync(path.join(outputDirectory, `index.${extension}`), 'utf8') - const jsEntryPoints = jsEntry - .split(BY_LINES) - .filter(emptyLines) - .reduce(generateEntryPointsRecord(buildPath, outputDirectory, extension), {}) - forEach(jsEntryPoints, writeEntryFile(buildPath, extension)) -} - -/** - * Copies the index sourcemap. As long as the index.js file in the ./dist directory does not have - * any other code than exports it should be fine. If not done, the consumer project won't have - * sourcemaps. - * - * @param buildPath - The path where the build will go. - * @param outputDirectory - Directory where storing the index source map. - */ -function copyIndexSourcemap(buildPath: string, outputDirectory: string): void { - const fileName = 'index.js.map' - fs.copyFileSync(path.join(outputDirectory, fileName), path.join(buildPath, 'core', fileName)) -} - -/** - * Generates a reducer function to split the entry points into multiple chunks, the `core` for the - * shared code and one per each x module. - * - * @param buildPath - The path where the build will go. - * @param outputDirectory - The directory where the output files are stored. - * @param extension - The type of the files for generating the entry points. - * @returns A reducer function that will generate a `Record` where the key is the chunk name, and - * the value is an array of strings containing the code. - */ -function generateEntryPointsRecord( - buildPath: string, - outputDirectory: string, - extension: string, -): ReducerFunctionOfEntryPoints { - const relativeOutputDirectory = `../${path.relative(buildPath, outputDirectory)}/` - const getXModuleNameFromExport = extractXModuleFromExport(outputDirectory, extension) - - return (files: Record, line: string): Record => { - const xModuleFileName = getXModuleNameFromExport(line) - const adjustedExport = adjustExport(relativeOutputDirectory, line) - if (xModuleFileName) { - // If it is a file from a x-module, we adjust the export, and add it to an array with the - // x module name - files[xModuleFileName] = files[xModuleFileName] || [] - files[xModuleFileName].push(adjustedExport) - } else { - // If it is not an export from a x-module, we keep that export on the core file, adjusting - // its location - files.core = files.core || [] - files.core.push(adjustedExport) - } - return files - } -} - -/** - * Adjusts an export to a new location. - * - * @param location - The new base location. - * @param line - The export line to adjust the export. - * @returns String with the new location adjusted to the line export. - */ -function adjustExport(location: string, line: string): string { - return line.replace('./', location) -} - -/** - * Generates a function that receives a line that is an export sentence from a DTS file, and - * if the line is an export from an x-module, it extracts the x-module name. In other case it - * returns `null`. - * - * @param outputDirectory - The export line to extract the x-module name. - * @param extension - The extension (i.e. `js`, `d.ts`) to check if the export is from a file or a - * barrel. - * @returns A function to test if a line is an export from an x-module. - */ -function extractXModuleFromExport(outputDirectory: string, extension: string) { - return (line: string): string | null => { - const anyExportFromXModulesDirectoryRegex = new RegExp(`/${X_MODULES_DIR_NAME}/([^';/]+)`) - const [, xModuleName] = anyExportFromXModulesDirectoryRegex.exec(line) ?? [] - if (!xModuleName) { - return null - } else { - const xModuleFileName = addExtension(xModuleName, extension) - const isFile = fs.existsSync( - path.join(outputDirectory, `${X_MODULES_DIR_NAME}/${xModuleFileName}`), - ) - return isFile ? null : xModuleName - } - } -} - -/** - * Appends the extension to the file name. - * - * @param fileName - File name with or without extension. - * @param extension - Extension to append to file name. - * @returns The file name with the extension added. - */ -function addExtension(fileName: string, extension: string): string { - return fileName.endsWith(`.${extension}`) ? fileName : `${fileName}.${extension}` -} - -/** - * Returns whether a line is empty or not. - * - * @param line - The line to test if it is empty. - * @returns True if the line is empty or false in the opposite case. - */ -function emptyLines(line: string): boolean { - return !!line.trim() -} - -/** - * Generates a reusable function that will write a file with the extension passed. - * The function will receive the file name, and the file contents. - * - * @param buildPath - The path where the build will go. - * @param extension - The extension of the file to write. - * @returns Function which writes a file with the extension passed as parameter. - */ -function writeEntryFile(buildPath: string, extension: string) { - return (fileName: string, fileContents: string[]): string => { - const filePath = path.join(buildPath, `/${fileName}/index.${extension}`) - ensureFilePathExists(filePath) - fs.writeFileSync(filePath, fileContents.join('\n')) - return filePath - } -} diff --git a/packages/x-components/build/rollup.config.ts b/packages/x-components/build/rollup.config.ts index e970999269..1f3ff2952f 100644 --- a/packages/x-components/build/rollup.config.ts +++ b/packages/x-components/build/rollup.config.ts @@ -1,4 +1,5 @@ import type { Plugin, RollupOptions } from 'rollup' +import fs from 'node:fs' import path from 'node:path' import vue3 from '@vitejs/plugin-vue' import copy from 'rollup-plugin-copy' @@ -7,12 +8,11 @@ import styles from 'rollup-plugin-styles' import typescript from 'rollup-plugin-typescript2' import { dependencies as pkgDeps, peerDependencies as pkgPeerDeps } from '../package.json' import { apiDocumentation } from './docgen/documentation.rollup-plugin' -import { generateEntryFiles } from './rollup-plugins/x-components.rollup-plugin' const rootDir = path.resolve(__dirname, '../') const buildPath = path.join(rootDir, 'dist') +const r = (p: string) => path.join(rootDir, p) -const jsOutputDir = path.join(buildPath, 'js') const typesOutputDir = path.join(buildPath, 'types') const dependencies = new Set(Object.keys(pkgDeps).concat(Object.keys(pkgPeerDeps))) @@ -23,10 +23,24 @@ const vueDocs = { !/vue&type=docs/.test(id) ? undefined : `export default ''`, } -export const rollupConfig: RollupOptions = { - input: path.join(rootDir, 'src/index.ts'), +const getXModules = () => { + const xModulesPath = path.join(rootDir, 'src', 'x-modules') + return Object.fromEntries( + fs + .readdirSync(xModulesPath) + .filter(file => fs.statSync(path.join(xModulesPath, file)).isDirectory()) + .map(module => [`${module}/index`, r(`src/x-modules/${module}/index.ts`)]), + ) +} + +const rollupConfig: RollupOptions = { + input: { + 'core/index': r('src/core.entry.ts'), + ...getXModules(), + 'x-modules.types/index': r('src/x-modules/x-modules.types.ts'), + }, output: { - dir: jsOutputDir, + dir: buildPath, format: 'esm', sourcemap: true, preserveModules: true, @@ -61,6 +75,7 @@ export const rollupConfig: RollupOptions = { ], }), typescript({ + check: false, useTsconfigDeclarationDir: true, tsconfig: path.resolve(rootDir, 'tsconfig.json'), tsconfigOverride: { @@ -84,20 +99,21 @@ export const rollupConfig: RollupOptions = { mode: [ 'inject', varname => { - const pathInjector = path.resolve('./tools/inject-css.js') + const pathInjector = r('src/utils/inject-css.js') return `import injectCss from '${pathInjector}';injectCss(${varname});` }, ], }), vueDocs, - generateEntryFiles({ buildPath, jsOutputDir, typesOutputDir }), apiDocumentation({ buildPath }), copy({ targets: [ { src: ['build/tools'], dest: buildPath }, - { src: ['CHANGELOG.md', 'package.json', 'README.md', 'docs'], dest: buildPath }, + { src: ['CHANGELOG.md', 'package.json', 'README.md', 'docs', 'patches'], dest: buildPath }, ], hook: 'writeBundle', }), ], } + +export default rollupConfig diff --git a/packages/x-components/build/tsconfig.json b/packages/x-components/build/tsconfig.json index 616681bd99..3517e39e51 100644 --- a/packages/x-components/build/tsconfig.json +++ b/packages/x-components/build/tsconfig.json @@ -2,23 +2,25 @@ "compilerOptions": { "target": "es2020", "jsx": "preserve", - "lib": ["esnext", "dom", "scripthost"], - "experimentalDecorators": true, + "lib": ["esnext", "dom", "dom.iterable"], "baseUrl": ".", + "rootDir": "src", "module": "commonjs", "moduleResolution": "node", - "paths": {}, "resolveJsonModule": true, "types": ["node"], "allowJs": true, "strict": true, "noImplicitAny": true, "noImplicitThis": true, + "declaration": true, + "declarationMap": true, + "importHelpers": true, "sourceMap": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "skipLibCheck": true }, - "include": ["**/*.ts", "**/*.js"], + "include": ["**/*.ts", "**/*.js", "**/*.vue"], "exclude": ["node_modules"] } diff --git a/packages/x-components/package.json b/packages/x-components/package.json index 91fa0ff40d..246f28aab6 100644 --- a/packages/x-components/package.json +++ b/packages/x-components/package.json @@ -36,9 +36,100 @@ "**/*vue[0-9].js", "**/*.vue" ], - "main": "./core/index.js", - "module": "./core/index.js", - "types": "./core/index.d.ts", + "exports": { + ".": { + "types": "./types/core.entry.d.ts", + "import": "./core/index.js" + }, + "./ai": { + "types": "./types/x-modules/ai/index.d.ts", + "import": "./ai/index.js" + }, + "./device": { + "types": "./types/x-modules/device/index.d.ts", + "import": "./device/index.js" + }, + "./empathize": { + "types": "./types/x-modules/empathize/index.d.ts", + "import": "./empathize/index.js" + }, + "./experience-controls": { + "types": "./types/x-modules/experience-controls/index.d.ts", + "import": "./experience-controls/index.js" + }, + "./extra-params": { + "types": "./types/x-modules/extra-params/index.d.ts", + "import": "./extra-params/index.js" + }, + "./facets": { + "types": "./types/x-modules/facets/index.d.ts", + "import": "./facets/index.js" + }, + "./history-queries": { + "types": "./types/x-modules/history-queries/index.d.ts", + "import": "./history-queries/index.js" + }, + "./identifier-results": { + "types": "./types/x-modules/identifier-results/index.d.ts", + "import": "./identifier-results/index.js" + }, + "./next-queries": { + "types": "./types/x-modules/next-queries/index.d.ts", + "import": "./next-queries/index.js" + }, + "./popular-searches": { + "types": "./types/x-modules/popular-searches/index.d.ts", + "import": "./popular-searches/index.js" + }, + "./queries-preview": { + "types": "./types/x-modules/queries-preview/index.d.ts", + "import": "./queries-preview/index.js" + }, + "./query-suggestions": { + "types": "./types/x-modules/query-suggestions/index.d.ts", + "import": "./query-suggestions/index.js" + }, + "./recommendations": { + "types": "./types/x-modules/recommendations/index.d.ts", + "import": "./recommendations/index.js" + }, + "./related-prompts": { + "types": "./types/x-modules/related-prompts/index.d.ts", + "import": "./related-prompts/index.js" + }, + "./related-tags": { + "types": "./types/x-modules/related-tags/index.d.ts", + "import": "./related-tags/index.js" + }, + "./scroll": { + "types": "./types/x-modules/scroll/index.d.ts", + "import": "./scroll/index.js" + }, + "./search-box": { + "types": "./types/x-modules/search-box/index.d.ts", + "import": "./search-box/index.js" + }, + "./search": { + "types": "./types/x-modules/search/index.d.ts", + "import": "./search/index.js" + }, + "./semantic-queries": { + "types": "./types/x-modules/semantic-queries/index.d.ts", + "import": "./semantic-queries/index.js" + }, + "./tagging": { + "types": "./types/x-modules/tagging/index.d.ts", + "import": "./tagging/index.js" + }, + "./url": { + "types": "./types/x-modules/url/index.d.ts", + "import": "./url/index.js" + }, + "./types": { + "types": "./types/x-modules/x-modules.types.d.ts", + "import": "./x-modules.types/index.js" + } + }, "engines": { "node": ">=22" }, @@ -71,6 +162,7 @@ "cypress:open:firefox": "cypress open --e2e --browser firefox", "cypress:open:component": "cypress open --component --browser chrome", "cypress:open:component:firefox": "cypress open --component --browser firefox", + "postinstall": "node patches/vuex.mjs", "prepublishOnly": "pnpm run build" }, "peerDependencies": { diff --git a/packages/x-components/patches/vuex.mjs b/packages/x-components/patches/vuex.mjs new file mode 100644 index 0000000000..a8543d1b53 --- /dev/null +++ b/packages/x-components/patches/vuex.mjs @@ -0,0 +1,16 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' + +/** Add a types entry to the exports section in Vuex's package.json */ +function patchVuex(path) { + const vuexPackageJsonPath = resolve(import.meta.dirname, path) + if (!existsSync(vuexPackageJsonPath)) return + const pkg = JSON.parse(readFileSync(vuexPackageJsonPath, 'utf-8')) + pkg.exports['.'].types = './types/index.d.ts' + writeFileSync(vuexPackageJsonPath, JSON.stringify(pkg, null, 2)) +} + +// This project +patchVuex('node_modules/vuex/package.json') +// When is installed as a dependency +patchVuex('../../../vuex/package.json') diff --git a/packages/x-components/src/core.entry.ts b/packages/x-components/src/core.entry.ts new file mode 100644 index 0000000000..cac0f7217f --- /dev/null +++ b/packages/x-components/src/core.entry.ts @@ -0,0 +1,10 @@ +export * from './components' +export * from './composables' +export * from './directives' +export * from './plugins' +export * from './services' +export * from './store' +export * from './types' +export * from './utils' +export * from './wiring' +export * from './x-installer' diff --git a/packages/x-components/src/directives/typing.ts b/packages/x-components/src/directives/typing.ts index 075eb8f7a1..c3dd5e98c9 100644 --- a/packages/x-components/src/directives/typing.ts +++ b/packages/x-components/src/directives/typing.ts @@ -24,6 +24,11 @@ export interface TypingOptions { targetAttr?: string } +/** + * Typing element private data. + * + * @internal + */ export interface TypingHTMLElement extends HTMLElement { __timeoutId?: number } diff --git a/packages/x-components/src/index.ts b/packages/x-components/src/index.ts index b999ae0360..b69bd60e81 100644 --- a/packages/x-components/src/index.ts +++ b/packages/x-components/src/index.ts @@ -5,18 +5,7 @@ */ // TODO Write a complete description. -export * from './components' -export * from './composables' -export * from './directives' -export * from './plugins' -export * from './services' -export * from './store' -export * from './types' -export * from './utils' -export * from './wiring' -export * from './x-bus' -export * from './x-bus/x-priority-queue' -export * from './x-installer' +export * from './core.entry' export * from './x-modules/ai' export * from './x-modules/device' export * from './x-modules/empathize' diff --git a/packages/x-components/build/tools/inject-css.js b/packages/x-components/src/utils/inject-css.js similarity index 100% rename from packages/x-components/build/tools/inject-css.js rename to packages/x-components/src/utils/inject-css.js diff --git a/packages/x-components/tsconfig.json b/packages/x-components/tsconfig.json index 448e2234ce..449ed8dffc 100644 --- a/packages/x-components/tsconfig.json +++ b/packages/x-components/tsconfig.json @@ -1,15 +1,17 @@ { "compilerOptions": { - "target": "es2019", + "target": "es2020", "jsx": "preserve", - "lib": ["esnext", "dom", "dom.iterable", "scripthost"], - "experimentalDecorators": true, + "lib": ["esnext", "dom", "dom.iterable"], "baseUrl": ".", "rootDir": "src", "module": "esnext", "moduleResolution": "node", + "resolveJsonModule": true, "types": ["jest", "node", "@testing-library/jest-dom"], "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, "declaration": true, "declarationMap": true, "importHelpers": true, @@ -17,8 +19,9 @@ "sourceMap": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "isolatedModules": true + "isolatedModules": true, + "skipLibCheck": true }, - "include": ["src/**/*.ts", "src/**/*.vue"], + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.vue"], "exclude": ["node_modules"] }