Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 80 additions & 58 deletions packages/core-generators/src/generators/node/typescript/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {
BuilderAction,
GeneratorTask,
GeneratorTaskOutputBuilder,
InferProviderType,
ProviderType,
WriteFileOptions,
} from '@halfdomelabs/sync';

Expand All @@ -10,14 +11,14 @@ import {
createGenerator,
createGeneratorTask,
createProviderType,
createTaskPhase,
} from '@halfdomelabs/sync';
import { safeMergeAll } from '@halfdomelabs/utils';
import path from 'node:path';
import { z } from 'zod';

import type { CopyTypescriptFilesOptions } from '@src/actions/copy-typescript-files-action.js';
import type {
InferImportMapProvidersFromProviderTypeMap,
InferTsCodeTemplateVariablesFromMap,
TsCodeFileTemplate,
TsCodeTemplateVariableMap,
Expand Down Expand Up @@ -91,18 +92,30 @@ export const typescriptProvider =

interface WriteTemplatedFilePayload<
TVariables extends TsCodeTemplateVariableMap,
TImportMapProviders extends Record<string, ProviderType> = Record<
never,
ProviderType
>,
> {
template: TsCodeFileTemplate<TVariables>;
id: string;
template: TsCodeFileTemplate<TVariables, TImportMapProviders>;
destination: string;
variables: InferTsCodeTemplateVariablesFromMap<TVariables>;
fileId: string;
importMapProviders: InferImportMapProvidersFromProviderTypeMap<TImportMapProviders>;
options?: WriteFileOptions;
}

export interface TypescriptFileProvider {
writeTemplatedFile<TVariables extends TsCodeTemplateVariableMap>(
payload: WriteTemplatedFilePayload<TVariables>,
): void;
writeTemplatedFile<
TVariables extends TsCodeTemplateVariableMap,
TImportMapProviders extends Record<string, ProviderType> = Record<
never,
ProviderType
>,
>(
builder: GeneratorTaskOutputBuilder,
payload: WriteTemplatedFilePayload<TVariables, TImportMapProviders>,
): Promise<{ destination: string }>;
}

export const typescriptFileProvider =
Expand Down Expand Up @@ -157,61 +170,10 @@ export type TypescriptSetupProvider = InferProviderType<
typeof typescriptSetupProvider
>;

export const typescriptFileTaskPhase = createTaskPhase('typescript-file');

export function createTypescriptFileTask<
TVariables extends TsCodeTemplateVariableMap,
>(payload: WriteTemplatedFilePayload<TVariables>): GeneratorTask {
const task = createGeneratorTask({
phase: typescriptFileTaskPhase,
dependencies: { typescriptConfig: typescriptConfigProvider },
run({ typescriptConfig: { compilerOptions, includeMetadata } }) {
const {
baseUrl = '.',
paths = {},
moduleResolution = 'node',
} = compilerOptions;
const { fileId, template, destination, variables, options } = payload;
const pathMapEntries = generatePathMapEntries(baseUrl, paths);
const internalPatterns = pathMapEntriesToRegexes(pathMapEntries);

return {
async build(builder) {
const directory = path.dirname(destination);
const file = await renderTsCodeFileTemplate(template, variables, {
resolveModule(moduleSpecifier) {
return resolveModule(moduleSpecifier, directory, {
pathMapEntries,
moduleResolution,
});
},
importSortOptions: {
internalPatterns,
},
includeMetadata,
});

builder.writeFile({
id: fileId,
filePath: destination,
contents: file,
options: {
...options,
shouldFormat: true,
},
});
},
};
},
});
return task;
}

export const typescriptGenerator = createGenerator({
name: 'node/typescript',
generatorFileUrl: import.meta.url,
descriptorSchema: typescriptGeneratorDescriptorSchema,
preRegisteredPhases: [typescriptFileTaskPhase],
buildTasks: (descriptor) => ({
setup: createGeneratorTask(setupTask(descriptor)),
nodePackages: createNodePackagesTask({
Expand Down Expand Up @@ -321,5 +283,65 @@ export const typescriptGenerator = createGenerator({
};
},
}),
file: createGeneratorTask({
dependencies: { typescriptConfig: typescriptConfigProvider },
exports: { typescriptFile: typescriptFileProvider.export(projectScope) },
run({ typescriptConfig: { compilerOptions, includeMetadata } }) {
const {
baseUrl = '.',
paths = {},
moduleResolution = 'node',
} = compilerOptions;
const pathMapEntries = generatePathMapEntries(baseUrl, paths);
const internalPatterns = pathMapEntriesToRegexes(pathMapEntries);

return {
providers: {
typescriptFile: {
writeTemplatedFile: async (builder, payload) => {
const {
id,
template,
destination,
variables,
options,
importMapProviders,
} = payload;
const directory = path.dirname(destination);
const file = await renderTsCodeFileTemplate(
template,
variables,
{
resolveModule(moduleSpecifier) {
return resolveModule(moduleSpecifier, directory, {
pathMapEntries,
moduleResolution,
});
},
importSortOptions: {
internalPatterns,
},
includeMetadata,
importMapProviders,
},
);

builder.writeFile({
id,
filePath: destination,
contents: file,
options: {
...options,
shouldFormat: true,
},
});

return { destination };
},
},
},
};
},
}),
}),
});
4 changes: 2 additions & 2 deletions packages/core-generators/src/providers/project.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createOutputProviderType } from '@halfdomelabs/sync';
import { createReadOnlyProviderType } from '@halfdomelabs/sync';

export interface ProjectProvider {
getProjectName(): string;
}

export const projectProvider =
createOutputProviderType<ProjectProvider>('project');
createReadOnlyProviderType<ProjectProvider>('project');
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './transform-ts-imports-with-map.js';
export * from './ts-import-map.js';
export type * from './types.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { TsImportDeclaration } from '../imports/types.js';
import type { TsImportMap } from './types.js';

/**
* Transform import declarations with an import map.
*
* Import declarations whose module specifiers begin with `%` are transformed
* using the import map. We first look up the correct import map using the
* module specifier without the `%` prefix. If the import map is not found, an
* error is thrown.
*
* Then we transform each named import using the import map entry found in the
* import map.
*
* @param imports - The import declarations to transform.
* @param importMaps - The import maps to use to transform the imports.
* @returns The transformed import declarations.
*/
export function transformTsImportsWithMap(
imports: TsImportDeclaration[],
importMaps: Map<string, TsImportMap>,
): TsImportDeclaration[] {
return imports.flatMap((importDeclaration) => {
if (!importDeclaration.source.startsWith('%')) {
return [importDeclaration];
}

const importMapKey = importDeclaration.source.slice(1);

const importMap = importMaps.get(importMapKey);

if (!importMap) {
throw new Error(`Import map not found for ${importDeclaration.source}`);
}

if (importDeclaration.namespaceImport || importDeclaration.defaultImport) {
throw new Error(
`Import map does not support namespace or default imports: ${importDeclaration.source}`,
);
}

return (
importDeclaration.namedImports?.map((namedImport) => {
if (!(namedImport.name in importMap)) {
throw new Error(`Import map entry not found for ${namedImport.name}`);
}

const entry = importMap[namedImport.name];

return importDeclaration.isTypeOnly
? entry.typeDeclaration()
: entry.declaration();
}) ?? []
);
});
}
Loading