@graphql-mesh/cli
Version:
450 lines (441 loc) • 20.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMeshArtifactEmitPlan = getMeshArtifactEmitPlan;
exports.getMeshArtifactsPackageJson = getMeshArtifactsPackageJson;
exports.readResolvedTsConfigModule = readResolvedTsConfigModule;
exports.generateTsArtifacts = generateTsArtifacts;
exports.compileTS = compileTS;
const tslib_1 = require("tslib");
const get_tsconfig_1 = require("get-tsconfig");
const json5_1 = tslib_1.__importDefault(require("json5"));
const typescript_1 = tslib_1.__importDefault(require("typescript"));
const core_1 = require("@graphql-codegen/core");
const typedDocumentNodePlugin = tslib_1.__importStar(require("@graphql-codegen/typed-document-node"));
const typescriptGenericSdk = tslib_1.__importStar(require("@graphql-codegen/typescript-generic-sdk"));
const tsOperationsPlugin = tslib_1.__importStar(require("@graphql-codegen/typescript-operations"));
const tsResolversPlugin = tslib_1.__importStar(require("@graphql-codegen/typescript-resolvers"));
const cross_helpers_1 = require("@graphql-mesh/cross-helpers");
const incontext_sdk_codegen_1 = require("@graphql-mesh/incontext-sdk-codegen");
const utils_1 = require("@graphql-mesh/utils");
const utils_2 = require("@graphql-tools/utils");
const generate_operations_js_1 = require("./generate-operations.js");
const BASEDIR_ASSIGNMENT_COMMENT = `/* BASEDIR_ASSIGNMENT */`;
function getMeshArtifactEmitPlan({ hasTsConfig, tsConfigModule, hasPackageJson, packageJsonType, fileType, }) {
const tsModule = tsConfigModule?.toLowerCase() ?? '';
const isPackageModule = packageJsonType === 'module';
const esmAsJs = () => ({
esmExt: 'js',
cjs: false,
artifactsPackageType: fileType === 'ts' ? undefined : 'module',
});
const cjsOnly = () => ({
cjs: true,
artifactsPackageType: fileType === 'ts' ? undefined : 'commonjs',
});
const dualCjsPackage = () => {
// `fileType: 'ts'` keeps the source artifact only — dual esm+cjs jobs would
// both write `index.ts` and package metadata would point at missing JS files.
if (fileType === 'ts') {
return {
cjs: true,
artifactsPackageType: undefined,
};
}
return {
esmExt: 'mjs',
cjs: fileType !== 'js',
artifactsPackageType: fileType === 'js' ? 'module' : 'commonjs',
};
};
if (hasTsConfig) {
if (tsModule.startsWith('es')) {
return esmAsJs();
}
if (tsModule.startsWith('node') && hasPackageJson) {
return isPackageModule ? esmAsJs() : cjsOnly();
}
// `"type": "module"` means Node loads `.js` as ESM; emitting CJS artifacts
// into that package (hello-world-esm) makes `exports is not defined`.
if (hasPackageJson && isPackageModule) {
return esmAsJs();
}
return cjsOnly();
}
if (hasPackageJson && isPackageModule) {
return esmAsJs();
}
return dualCjsPackage();
}
function getMeshArtifactsPackageJson(moduleType, esmEntry, options = {}) {
const emitCjs = options.emitCjs ?? esmEntry !== 'index.mjs';
const dualPackage = esmEntry === 'index.mjs' && emitCjs;
const mjsOnly = esmEntry === 'index.mjs' && !emitCjs;
return {
name: 'mesh-artifacts',
private: true,
type: moduleType,
main: mjsOnly ? 'index.mjs' : 'index.js',
...(esmEntry ? { module: esmEntry } : {}),
sideEffects: false,
typings: 'index.d.ts',
typescript: {
definition: 'index.d.ts',
},
exports: dualPackage
? {
'.': {
require: './index.js',
import: './index.mjs',
},
'./*': {
require: './*.js',
import: './*.mjs',
},
}
: mjsOnly
? {
'.': './index.mjs',
'./*': './*.mjs',
}
: {
'.': './index.js',
'./*': './*.js',
},
};
}
/**
* Load `compilerOptions.module` from the project's `tsconfig.json`, including
* values inherited via `extends`. Returns `undefined` when there is no
* tsconfig in `projectDir` itself (parent configs are ignored).
*/
function readResolvedTsConfigModule(projectDir) {
const tsconfig = (0, get_tsconfig_1.getTsconfig)(projectDir, 'tsconfig.json');
if (!tsconfig) {
return undefined;
}
if (cross_helpers_1.path.resolve(cross_helpers_1.path.dirname(tsconfig.path)) !== cross_helpers_1.path.resolve(projectDir)) {
return undefined;
}
const moduleOption = tsconfig.config.compilerOptions?.module;
return typeof moduleOption === 'string' ? moduleOption : undefined;
}
async function loadTypeScriptCodegenPlugin() {
return (0, utils_1.defaultImportFn)('@graphql-codegen/typescript');
}
async function generateTsArtifacts({ unifiedSchema, rawSources, mergerType = 'stitching', documents, flattenTypes, importedModulesSet, baseDir, meshConfigImportCodes, meshConfigCodes, logger, sdkConfig, fileType, codegenConfig = {}, pollingInterval, }, cliParams) {
const artifactsDir = cross_helpers_1.path.join(baseDir, cliParams.artifactsDir);
logger.info('Generating index file in TypeScript');
for (const rawSource of rawSources) {
const transformedSchema = unifiedSchema.extensions.sourceMap.get(rawSource);
const sdl = (0, utils_2.printSchemaWithDirectives)(transformedSchema);
await (0, utils_1.writeFile)(cross_helpers_1.path.join(artifactsDir, `sources/${rawSource.name}/schema.graphql`), sdl);
}
const documentsInput = sdkConfig?.generateOperations
? (0, generate_operations_js_1.generateOperations)(unifiedSchema, sdkConfig.generateOperations)
: documents;
const pluginsInput = [
{
typescript: {},
},
{
resolvers: {},
},
{
contextSdk: {},
},
];
if (documentsInput.length) {
pluginsInput.push({
typescriptOperations: {},
}, {
typedDocumentNode: {},
}, {
typescriptGenericSdk: {
documentMode: 'external',
importDocumentNodeExternallyFrom: 'NOWHERE',
},
});
const documentHashMap = {};
for (const document of documentsInput) {
if (document.sha256Hash) {
documentHashMap[document.sha256Hash] = document.rawSDL || (0, utils_1.printWithCache)(document.document);
}
}
await (0, utils_1.writeFile)(cross_helpers_1.path.join(artifactsDir, `persisted_operations.json`), JSON.stringify(documentHashMap, null, 2));
}
const tsBasePlugin = await loadTypeScriptCodegenPlugin();
const codegenOutput = '// @ts-nocheck\n' +
(await (0, core_1.codegen)({
filename: 'types.ts',
documents: documentsInput,
config: {
skipTypename: true,
flattenGeneratedTypes: flattenTypes,
onlyOperationTypes: flattenTypes,
preResolveTypes: flattenTypes,
namingConvention: 'keep',
documentMode: 'graphQLTag',
gqlImport: '@graphql-mesh/utils#gql',
enumsAsTypes: true,
ignoreEnumValuesFromSchema: true,
useIndexSignature: true,
noSchemaStitching: false,
contextType: 'MeshContext',
federation: mergerType === 'federation',
...codegenConfig,
},
schemaAst: unifiedSchema,
schema: undefined, // This is not necessary on codegen.
// skipDocumentsValidation: true,
pluginMap: {
typescript: tsBasePlugin,
typescriptOperations: tsOperationsPlugin,
typedDocumentNode: typedDocumentNodePlugin,
typescriptGenericSdk,
resolvers: tsResolversPlugin,
contextSdk: {
plugin: async () => {
const importCodes = new Set([
...meshConfigImportCodes,
`import { getMesh, type ExecuteMeshFn, type SubscribeMeshFn, type MeshContext as BaseMeshContext, type MeshInstance } from '@graphql-mesh/runtime';`,
`import { MeshStore, FsStoreStorageAdapter } from '@graphql-mesh/store';`,
`import { path as pathModule } from '@graphql-mesh/cross-helpers';`,
`import type { ImportFn } from '@graphql-mesh/types';`,
]);
const results = await Promise.all(rawSources.map(async (source) => {
const sourceMap = unifiedSchema.extensions.sourceMap;
const sourceSchema = sourceMap.get(source);
const { identifier, codeAst } = await (0, incontext_sdk_codegen_1.generateIncontextSDKTypes)({
schema: sourceSchema,
name: source.name,
contextVariables: source.contextVariables,
flattenTypes,
codegenConfig,
unifiedContextIdentifier: 'BaseMeshContext',
});
if (codeAst) {
const content = '// @ts-nocheck\n' + codeAst;
await (0, utils_1.writeFile)(cross_helpers_1.path.join(artifactsDir, `sources/${source.name}/types.ts`), content);
}
if (identifier) {
importCodes.add(`import type { ${identifier} } from './sources/${source.name}/types';`);
}
return {
identifier,
codeAst,
};
}));
let contextType = (0, incontext_sdk_codegen_1.generateUnifiedContextTypeFromIdentifiers)(results.map(r => r.identifier).filter((id) => Boolean(id)));
contextType += '\n\nexport type MeshContext = BaseMeshContext & MeshInContextSDK;';
let meshMethods = `
${BASEDIR_ASSIGNMENT_COMMENT}
const importFn: ImportFn = <T>(moduleId: string) => {
const relativeModuleId = (pathModule.isAbsolute(moduleId) ? pathModule.relative(baseDir, moduleId) : moduleId).split('\\\\').join('/').replace(baseDir + '/', '');
switch(relativeModuleId) {${[...importedModulesSet]
.map(importedModuleName => {
const importPathRelativeToBaseDir = cross_helpers_1.path
.relative(baseDir, importedModuleName)
.split('\\')
.join('/');
let importPath = importedModuleName;
if (importPath.startsWith('.')) {
importPath = cross_helpers_1.path.join(baseDir, importPath);
}
if (cross_helpers_1.path.isAbsolute(importPath)) {
importPath = `./${cross_helpers_1.path
.relative(artifactsDir, importedModuleName)
.split('\\')
.join('/')}`;
importPath = replaceTypeScriptExtension(importPath);
}
return `
case ${JSON.stringify(importPathRelativeToBaseDir)}:
return import(${JSON.stringify(importPath)}) as T;
`;
})
.join('')}
default:
return Promise.reject(new Error(\`Cannot find module '\${relativeModuleId}'.\`));
}
};
const rootStore = new MeshStore('${cliParams.artifactsDir}', new FsStoreStorageAdapter({
cwd: baseDir,
importFn,
fileType: ${JSON.stringify(fileType)},
}), {
readonly: ${!pollingInterval},
validate: false
});
${[...meshConfigCodes].join('\n')}
let meshInstance$: Promise<MeshInstance> | undefined;
export const pollingInterval = ${pollingInterval || null};
export function ${cliParams.builtMeshFactoryName}(): Promise<MeshInstance> {
if (meshInstance$ == null) {
if (pollingInterval) {
setInterval(() => {
getMeshOptions()
.then(meshOptions => getMesh(meshOptions))
.then(newMesh =>
meshInstance$.then(oldMesh => {
oldMesh.destroy()
meshInstance$ = Promise.resolve(newMesh)
})
).catch(err => {
console.error("Mesh polling failed so the existing version will be used:", err);
});
}, pollingInterval)
}
meshInstance$ = getMeshOptions().then(meshOptions => getMesh(meshOptions)).then(mesh => {
const id = mesh.pubsub.subscribe('destroy', () => {
meshInstance$ = undefined;
mesh.pubsub.unsubscribe(id);
});
return mesh;
}).catch((err) => {
meshInstance$ = undefined;
return Promise.reject(err);
});
}
return meshInstance$;
}
export const execute: ExecuteMeshFn = (...args) => ${cliParams.builtMeshFactoryName}().then(({ execute }) => execute(...args));
export const subscribe: SubscribeMeshFn = (...args) => ${cliParams.builtMeshFactoryName}().then(({ subscribe }) => subscribe(...args));`;
if (documentsInput.length) {
meshMethods += `
export function ${cliParams.builtMeshSDKFactoryName}<TGlobalContext = any, TOperationContext = any>(globalContext?: TGlobalContext) {
const sdkRequester$ = ${cliParams.builtMeshFactoryName}().then(({ sdkRequesterFactory }) => sdkRequesterFactory(globalContext));
return getSdk<TOperationContext, TGlobalContext>((...args) => sdkRequester$.then(sdkRequester => sdkRequester(...args)));
}`;
}
return {
prepend: [[...importCodes].join('\n'), '\n\n'],
content: [contextType, meshMethods].join('\n\n'),
};
},
},
},
plugins: pluginsInput,
}))
.replace(`import * as Operations from 'NOWHERE';\n`, '')
.replace(`import { DocumentNode } from 'graphql';`, '')
.split('(Operations.')
.join('(');
const endpointAssignmentESM = `import { fileURLToPath } from '@graphql-mesh/utils';
const baseDir = pathModule.join(pathModule.dirname(fileURLToPath(import.meta.url)), '${cross_helpers_1.path.relative(artifactsDir, baseDir)}');`;
const endpointAssignmentCJS = `const baseDir = pathModule.join(typeof __dirname === 'string' ? __dirname : '/', '${cross_helpers_1.path.relative(artifactsDir, baseDir)}');`;
const tsFilePath = cross_helpers_1.path.join(artifactsDir, 'index.ts');
const jobs = [];
const jsFilePath = cross_helpers_1.path.join(artifactsDir, 'index.js');
const dtsFilePath = cross_helpers_1.path.join(artifactsDir, 'index.d.ts');
const esmJob = (ext) => async () => {
logger.info('Writing index.ts for ESM to the disk.');
await (0, utils_1.writeFile)(tsFilePath, codegenOutput.replace(BASEDIR_ASSIGNMENT_COMMENT, endpointAssignmentESM));
const esmJsFilePath = cross_helpers_1.path.join(artifactsDir, `index.${ext}`);
if (await (0, utils_1.pathExists)(esmJsFilePath)) {
await cross_helpers_1.fs.promises.unlink(esmJsFilePath);
}
if (fileType !== 'ts') {
logger.info(`Compiling TS file as ES Module to "index.${ext}"`);
compileTS(tsFilePath, typescript_1.default.ModuleKind.ESNext, [jsFilePath, dtsFilePath]);
if (ext === 'mjs') {
const mjsFilePath = cross_helpers_1.path.join(artifactsDir, 'index.mjs');
await cross_helpers_1.fs.promises.rename(jsFilePath, mjsFilePath);
}
logger.info('Deleting index.ts');
await cross_helpers_1.fs.promises.unlink(tsFilePath);
}
};
const cjsJob = async () => {
logger.info('Writing index.ts for CJS to the disk.');
await (0, utils_1.writeFile)(tsFilePath, codegenOutput.replace(BASEDIR_ASSIGNMENT_COMMENT, endpointAssignmentCJS));
if (await (0, utils_1.pathExists)(jsFilePath)) {
await cross_helpers_1.fs.promises.unlink(jsFilePath);
}
if (fileType !== 'ts') {
logger.info('Compiling TS file as CommonJS Module to `index.js`');
compileTS(tsFilePath, typescript_1.default.ModuleKind.CommonJS, [jsFilePath, dtsFilePath]);
logger.info('Deleting index.ts');
await cross_helpers_1.fs.promises.unlink(tsFilePath);
}
};
const packageJsonJob = (moduleType, esmEntry, options) => () => (0, utils_1.writeJSON)(cross_helpers_1.path.join(artifactsDir, 'package.json'), getMeshArtifactsPackageJson(moduleType, esmEntry, options));
const tsConfigPath = cross_helpers_1.path.join(baseDir, 'tsconfig.json');
const packageJsonPath = cross_helpers_1.path.join(baseDir, 'package.json');
const hasTsConfig = await (0, utils_1.pathExists)(tsConfigPath);
const hasPackageJson = await (0, utils_1.pathExists)(packageJsonPath);
let tsConfigModule;
let packageJsonType;
if (hasTsConfig) {
try {
tsConfigModule = readResolvedTsConfigModule(baseDir);
}
catch {
// Keep the default emit plan if tsconfig cannot be read.
}
}
if (hasPackageJson) {
try {
const packageJson = json5_1.default.parse(await cross_helpers_1.fs.promises.readFile(packageJsonPath, 'utf-8'));
packageJsonType = packageJson?.type;
}
catch {
// Keep the default emit plan if package.json cannot be read.
}
}
const plan = getMeshArtifactEmitPlan({
hasTsConfig,
tsConfigModule,
hasPackageJson,
packageJsonType,
fileType,
});
if (plan.esmExt) {
jobs.push(esmJob(plan.esmExt));
}
if (plan.cjs) {
jobs.push(cjsJob);
}
if (plan.artifactsPackageType) {
jobs.push(packageJsonJob(plan.artifactsPackageType, plan.esmExt ? `index.${plan.esmExt}` : undefined, { emitCjs: plan.cjs }));
}
for (const job of jobs) {
await job();
}
}
function compileTS(tsFilePath, module, outputFilePaths) {
const options = {
target: typescript_1.default.ScriptTarget.ESNext,
module,
sourceMap: false,
inlineSourceMap: false,
importHelpers: true,
allowSyntheticDefaultImports: true,
esModuleInterop: true,
declaration: true,
};
const host = typescript_1.default.createCompilerHost(options);
const hostWriteFile = host.writeFile.bind(host);
host.writeFile = (fileName, ...rest) => {
if (outputFilePaths.some(f => cross_helpers_1.path.normalize(f) === cross_helpers_1.path.normalize(fileName))) {
return hostWriteFile(fileName, ...rest);
}
};
// Prepare and emit the d.ts files
const program = typescript_1.default.createProgram([tsFilePath], options, host);
program.emit();
}
/**
* If the specified path corresponds to a TypeScript file, replace
* its extension to `.js`.
*
* @param {string} path The path to a potential TypeScript file
* @returns {string}
*/
function replaceTypeScriptExtension(path) {
let modifiedPath = path;
if (modifiedPath.toLowerCase().endsWith('.ts')) {
const extensionStart = modifiedPath.lastIndexOf('.');
modifiedPath = modifiedPath.substring(0, extensionStart).concat('.js');
}
return modifiedPath;
}