packages/eui/scripts/dtsgenerator.js (145 lines of code) (raw):
const findup = require('findup');
const resolve = require('resolve');
const fs = require('fs');
const path = require('path');
const dtsGenerator = require('dts-generator').default;
const baseDir = path.resolve(__dirname, '..');
const srcDir = path.resolve(baseDir, 'src');
function hasParentIndex(pathToFile) {
const isIndexFile =
path.basename(pathToFile, path.extname(pathToFile)) === 'index';
try {
const fileDirectory = path.dirname(pathToFile);
const parentIndex = findup.sync(
// if this is an index file start looking in its parent directory
isIndexFile ? path.resolve(fileDirectory, '..') : fileDirectory,
'index.ts'
);
// ensure the found file is in the project
return parentIndex.startsWith(baseDir);
} catch (e) {
return false;
}
}
const generator = dtsGenerator({
prefix: '@elastic/eui',
project: baseDir,
out: 'eui.d.ts',
exclude: [
'node_modules/**/*.d.ts',
'*/custom_typings/**/*.d.ts',
'**/*.test.{ts,tsx}',
'**/*.testenv.{ts,tsx}',
'**/*.spec.{ts,tsx}',
'**/*.stories.{ts,tsx}',
'**/*.mock.{ts,tsx}',
'**/__mocks__/*',
'src/test/**/*', // Separate d.ts files are generated for test utils
'**/*.docgen.tsx', // Don't include "components" generated just for react-docgen
'**/*.mdx', // Don't include storybook mdx files
],
resolveModuleId(params) {
if (
path.basename(params.currentModuleId) === 'index' &&
!hasParentIndex(path.resolve(baseDir, params.currentModuleId))
) {
// this module is exporting from an `index(.d)?.ts` file, declare its exports straight to @elastic/eui module
return '@elastic/eui';
} else {
// otherwise export as the module's path relative to the @elastic/eui namespace
if (params.currentModuleId.endsWith('/index')) {
return path.join('@elastic/eui', path.dirname(params.currentModuleId));
} else {
return path.join('@elastic/eui', params.currentModuleId);
}
}
},
resolveModuleImport(params) {
// only intercept relative imports (don't modify node-modules references)
const importFromBaseDir = path.resolve(
baseDir,
path.dirname(params.currentModuleId)
);
const isFromEuiSrc = importFromBaseDir.startsWith(srcDir);
const isRelativeImport = isFromEuiSrc && params.importedModuleId[0] === '.';
if (isRelativeImport) {
// if importing from an `index` file (directly or targeting a directory with an index),
// then if there is no parent index file this should import from @elastic/eui
const importPathTarget = resolve.sync(params.importedModuleId, {
basedir: importFromBaseDir,
extensions: ['.ts', '.tsx', '.d.ts'],
});
const isIndexFile = importPathTarget.endsWith('/index.ts');
const isModuleIndex = isIndexFile && !hasParentIndex(importPathTarget);
if (isModuleIndex) {
// importing an `index` file, in `resolveModuleId` above we change those modules to '@elastic/eui'
return '@elastic/eui';
} else {
// importing from a non-index TS source file, keep the import path but re-scope it to '@elastic/eui' namespace
let importedModuleId = params.importedModuleId;
if (importedModuleId.endsWith('/'))
importedModuleId = importedModuleId.slice(
0,
importedModuleId.length - 1
);
return path.join(
'@elastic/eui',
path.dirname(params.currentModuleId),
importedModuleId
);
}
} else {
return params.importedModuleId;
}
},
});
// NOTE: once EUI is all converted to typescript this madness can be deleted forever
// 1. strip any `/// <reference` lines from the generated eui.d.ts
// 2. replace any import("src/...") declarations to import("@elastic/eui/src/...")
// 3. replace any import("./...") declarations to import("@elastic/eui/src/...)
// 4. generate & add EuiTokenObject
// 5. Fix React.ElementType being incorrectly expanded to React.ElementType<any, keyof React.JSX.IntrinsicElements>
generator.then(() => {
const defsFilePath = path.resolve(baseDir, 'eui.d.ts');
fs.writeFileSync(
defsFilePath,
fs
.readFileSync(defsFilePath)
.toString()
.replace(/\/\/\/\W+<reference.*/g, '') // 1.
.replace(/import\("src\/(.*?)"\)/g, 'import("@elastic/eui/src/$1")') // 2.
.replace(
// start 3.
// find any singular `declare module { ... }` block
// {.*?^} matches anything until a } starts a new line (via `m` regex option, and `s` is dotall)
//
// aren't regex really bad for this? Yes.
// However, @babel/preset-typescript doesn't understand some syntax generated in eui.d.ts
// and the tooling around typescript's parsing & code generation is lacking and undocumented
// so... because this works with the guarantee that the newline-brace combination matches a module...
/declare module '(.*?)' {.*?^}/gms,
(module, moduleName) => {
// `moduleName` is the namespace for this ambient module
return module.replace(
// replace relative imports by attaching them to the module's namespace
/import\("([.]{1,2}\/.*?)"\)/g,
(importStatement, importPath) => {
let target = path.join(path.dirname(moduleName), importPath);
// if the target resolves to an orphaned index.ts file, remap to '@elastic/eui'
const filePath = target.replace('@elastic/eui', baseDir);
const filePathTs = `${filePath}.ts`;
const filePathTsx = `${filePath}.tsx`;
const filePathResolvedToIndex = path.join(filePath, 'index.ts');
if (
// fs.existsSync(filePath) === false && // target file doesn't exist
fs.existsSync(filePathTs) === false && // target file (.ts) doesn't exist
fs.existsSync(filePathTsx) === false && // target file (.tsx) doesn't exist
fs.existsSync(filePathResolvedToIndex) && // and it resolves to an index.ts
hasParentIndex(filePathResolvedToIndex) === false // does not get exported at a higher level
) {
target = '@elastic/eui';
}
return `import ("${target}")`;
}
);
}
) // end 3.
.replace(/$/, `\n\n${buildEuiTokensObject()}`) // 4.
.replaceAll(
'React.ElementType<any, keyof React.JSX.IntrinsicElements>',
'React.ElementType'
) // 5.
);
});
/** For step 4 **/
// i18ntokens.json is generated as the first step in the build and can be relied upon here
function buildEuiTokensObject() {
// reduce over the tokens list as a few of the tokens are used multiple times and must be
// filtered down to a list
const { i18ndefs } = require('../i18ntokens.json').reduce(
({ i18ndefs, tokens }, def) => {
if (!tokens.has(def.token)) {
tokens.add(def.token);
i18ndefs.push(def);
}
return { i18ndefs, tokens };
},
{ i18ndefs: [], tokens: new Set() }
);
return `
declare module '@elastic/eui' {
export type EuiTokensObject = {
${i18ndefs.map(({ token }) => `"${token}": any;`).join('\n')}
}
}
`;
}