Skip to content

Commit

Permalink
dynamically load typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
toonvanstrijp committed Jan 9, 2023
1 parent 0da1dc6 commit 8c06153
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 133 deletions.
25 changes: 17 additions & 8 deletions src/i18n.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ import { I18nLanguageInterceptor } from './interceptors/i18n-language.intercepto
import { APP_INTERCEPTOR, HttpAdapterHost } from '@nestjs/core';
import { getI18nResolverOptionsToken } from './decorators/i18n-resolver-options.decorator';
import {
annotateSourceCode,
createTypesFile,
isNestMiddleware,
shouldResolve,
usingFastify,
Expand All @@ -48,7 +46,7 @@ import { I18nJsonLoader } from './loaders/i18n.json.loader';
import { I18nMiddleware } from './middlewares/i18n.middleware';
import { mergeDeep } from './utils/merge';
import * as fs from 'fs';
import path = require('path');
import * as path from 'path';

export const logger = new Logger('I18nService');

Expand Down Expand Up @@ -94,18 +92,26 @@ export class I18nModule implements OnModuleInit, OnModuleDestroy, NestModule {
};
}

if (
process.env['NODE_ENV'] !== 'production' &&
!!this.i18nOptions.typesOutputPath
) {


if (!!this.i18nOptions.typesOutputPath) {
try {
const ts = await import('./utils/typescript');

this.translations.pipe(takeUntil(this.unsubscribe)).subscribe(async (t) => {
logger.log('Checking translation changes');
const object = Object.keys(t).reduce(
(result, key) => mergeDeep(result, t[key]),
{},
);

const outputFile = annotateSourceCode(await createTypesFile(object));
const rawContent = await ts.createTypesFile(object);

if (!rawContent) {
return;
}

const outputFile = ts.annotateSourceCode(rawContent);

fs.mkdirSync(path.dirname(this.i18nOptions.typesOutputPath), {
recursive: true,
Expand All @@ -126,6 +132,9 @@ export class I18nModule implements OnModuleInit, OnModuleDestroy, NestModule {
logger.log('No changes detected');
}
});
} catch (_) {
// NOOP: typescript package not found
}
}
}

Expand Down
101 changes: 101 additions & 0 deletions src/utils/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@

import * as ts from 'typescript';

export const convertObjectToTypeDefinition = async (
object: any,
): Promise<ts.TypeElement[]> => {
switch (typeof object) {
case 'object':
return Promise.all(Object.keys(object).map(async (key) => {
if (typeof object[key] === 'string') {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createStringLiteral(key),
undefined,
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
);
}
if (Array.isArray(object[key])) {
return ts.factory.createPropertySignature(
undefined,
ts.factory.createStringLiteral(key),
undefined,
ts.factory.createTupleTypeNode(
Array(object[key].length).fill(
ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
),
),
);
}
return ts.factory.createPropertySignature(
undefined,
ts.factory.createStringLiteral(key),
undefined,
ts.factory.createTypeLiteralNode(
await convertObjectToTypeDefinition(object[key]),
),
);
}));
}

return [];
};

const printer = ts.createPrinter();

export const createTypesFile = async (object: any) => {

const sourceFile = ts.createSourceFile(
'placeholder.ts',
'',
ts.ScriptTarget.ESNext,
true,
ts.ScriptKind.TS,
);

const i18nTranslationsType = ts.factory.createTypeAliasDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('I18nTranslations'),
undefined,
ts.factory.createTypeLiteralNode(await convertObjectToTypeDefinition(object)),
);

const nodes = ts.factory.createNodeArray([
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports([
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier('Path'),
),
]),
),
ts.factory.createStringLiteral('nestjs-i18n'),
undefined,
),
i18nTranslationsType,
ts.factory.createTypeAliasDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier('I18nPath'),
undefined,
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Path'), [
ts.factory.createTypeReferenceNode(
ts.factory.createIdentifier('I18nTranslations'),
undefined,
),
]),
),
]);

return printer.printList(ts.ListFormat.MultiLine, nodes, sourceFile);
};

export const annotateSourceCode = (code: string) => {
return `/* DO NOT EDIT, file generated by nestjs-i18n */
${code}`;
};
126 changes: 1 addition & 125 deletions src/utils/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { I18nService, TranslateOptions } from '../services/i18n.service';
import { MiddlewareConsumer } from '@nestjs/common';
import { NestMiddlewareConsumer, Path } from '../types';
import type * as ts from 'typescript';

export function shouldResolve(e: I18nOptionResolver) {
return typeof e === 'function' || e['use'];
Expand Down Expand Up @@ -80,127 +79,4 @@ export const usingFastify = (consumer: NestMiddlewareConsumer) => {
return consumer.httpAdapter.constructor.name
.toLowerCase()
.startsWith('fastify');
};

let t: typeof ts | undefined;

const loadTypescript = async (): Promise<boolean> => {
if (t == undefined) {
try {
t = await import('typescript');
return true;
} catch (_) {
// no typescript found
return false;
}
}

return true;
}

export const convertObjectToTypeDefinition = async (
object: any,
): Promise<ts.TypeElement[]> => {
if(!(await loadTypescript())) {
return [];
}

switch (typeof object) {
case 'object':
return Promise.all(Object.keys(object).map(async (key) => {
if (typeof object[key] === 'string') {
return t.factory.createPropertySignature(
undefined,
t.factory.createStringLiteral(key),
undefined,
t.factory.createKeywordTypeNode(t.SyntaxKind.StringKeyword),
);
}
if (Array.isArray(object[key])) {
return t.factory.createPropertySignature(
undefined,
t.factory.createStringLiteral(key),
undefined,
t.factory.createTupleTypeNode(
Array(object[key].length).fill(
t.factory.createKeywordTypeNode(t.SyntaxKind.StringKeyword),
),
),
);
}
return t.factory.createPropertySignature(
undefined,
t.factory.createStringLiteral(key),
undefined,
t.factory.createTypeLiteralNode(
await convertObjectToTypeDefinition(object[key]),
),
);
}));
}

return [];
};


export const createTypesFile = async (object: any) => {
if(!(await loadTypescript())) {
return;
}

const printer = t.createPrinter();

const sourceFile = t.createSourceFile(
'placeholder.ts',
'',
t.ScriptTarget.ESNext,
true,
t.ScriptKind.TS,
);

const i18nTranslationsType = t.factory.createTypeAliasDeclaration(
[t.factory.createModifier(t.SyntaxKind.ExportKeyword)],
t.factory.createIdentifier('I18nTranslations'),
undefined,
t.factory.createTypeLiteralNode(await convertObjectToTypeDefinition(object)),
);

const nodes = t.factory.createNodeArray([
t.factory.createImportDeclaration(
undefined,
t.factory.createImportClause(
false,
undefined,
t.factory.createNamedImports([
t.factory.createImportSpecifier(
false,
undefined,
t.factory.createIdentifier('Path'),
),
]),
),
t.factory.createStringLiteral('nestjs-i18n'),
undefined,
),
i18nTranslationsType,
t.factory.createTypeAliasDeclaration(
[t.factory.createModifier(t.SyntaxKind.ExportKeyword)],
t.factory.createIdentifier('I18nPath'),
undefined,
t.factory.createTypeReferenceNode(t.factory.createIdentifier('Path'), [
t.factory.createTypeReferenceNode(
t.factory.createIdentifier('I18nTranslations'),
undefined,
),
]),
),
]);

return printer.printList(t.ListFormat.MultiLine, nodes, sourceFile);
};

export const annotateSourceCode = (code: string) => {
return `/* DO NOT EDIT, file generated by nestjs-i18n */
${code}`;
};
};

0 comments on commit 8c06153

Please sign in to comment.