Skip to content

Commit

Permalink
Oops hooks need to be bundled
Browse files Browse the repository at this point in the history
  • Loading branch information
rix0rrr committed Sep 19, 2022
1 parent 78c5834 commit d25cf01
Show file tree
Hide file tree
Showing 15 changed files with 95 additions and 167 deletions.
90 changes: 90 additions & 0 deletions packages/aws-cdk/lib/init-hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import * as path from 'path';
import { shell } from './os';

export type SubstitutePlaceholders = (...fileNames: string[]) => Promise<void>;

/**
* Helpers passed to hook functions
*/
export interface HookContext {
/**
* Callback function to replace placeholders on arbitrary files
*
* This makes token substitution available to non-`.template` files.
*/
readonly substitutePlaceholdersIn: SubstitutePlaceholders;

/**
* Return a single placeholder
*/
placeholder(name: string): string;
}

export type InvokeHook = (targetDirectory: string, context: HookContext) => Promise<void>;

export interface HookTarget {
readonly targetDirectory: string;
readonly templateName: string;
readonly language: string;
}

/**
* Invoke hooks for the given init template
*
* Sometimes templates need more complex logic than just replacing tokens. A 'hook' can be
* used to do additional processing other than copying files.
*
* Hooks used to be defined externally to the CLI, by running arbitrarily
* substituted shell scripts in the target directory.
*
* In practice, they're all TypeScript files and all the same, and the dynamism
* that the original solution allowed wasn't used at all. Worse, since the CLI
* is now bundled the hooks can't even reuse code from the CLI libraries at all
* anymore, so all shared code would have to be copy/pasted.
*
* Bundle hooks as built-ins into the CLI, so they get bundled and can take advantage
* of all shared code.
*/
export async function invokeBuiltinHooks(target: HookTarget, context: HookContext) {
switch (target.language) {
case 'csharp':
if (['app', 'sample-app'].includes(target.templateName)) {
return dotnetAddProject(target.targetDirectory, context);
}
break;

case 'fsharp':
if (['app', 'sample-app'].includes(target.templateName)) {
return dotnetAddProject(target.targetDirectory, context, 'fsproj');
}
break;

case 'python':
// We can't call this file 'requirements.template.txt' because Dependabot needs to be able to find it.
// Therefore, keep the in-repo name but still substitute placeholders.
await context.substitutePlaceholdersIn('requirements.txt');
break;

case 'java':
// We can't call this file 'pom.template.xml'... for the same reason as Python above.
await context.substitutePlaceholdersIn('pom.xml');
break;

case 'javascript':
case 'typescript':
// See above, but for 'package.json'.
await context.substitutePlaceholdersIn('package.json');

}
}

async function dotnetAddProject(targetDirectory: string, context: HookContext, ext = 'csproj') {
const pname = context.placeholder('name.PascalCased');
const slnPath = path.join(targetDirectory, 'src', `${pname}.sln`);
const csprojPath = path.join(targetDirectory, 'src', pname, `${pname}.${ext}`);
try {
await shell(['dotnet', 'sln', slnPath, 'add', csprojPath]);
} catch (e) {
throw new Error(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${e.message}`);
}
};
14 changes: 0 additions & 14 deletions packages/aws-cdk/lib/init-templates/app/csharp/add-project.hook.ts

This file was deleted.

14 changes: 0 additions & 14 deletions packages/aws-cdk/lib/init-templates/app/fsharp/add-project.hook.ts

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

53 changes: 5 additions & 48 deletions packages/aws-cdk/lib/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,12 @@ import * as path from 'path';
import * as cxapi from '@aws-cdk/cx-api';
import * as chalk from 'chalk';
import * as fs from 'fs-extra';
import { invokeBuiltinHooks } from './init-hooks';
import { error, print, warning } from './logging';
import { cdkHomeDir, rootDir } from './util/directories';
import { rangeFromSemver } from './util/version-range';


export type SubstitutePlaceholders = (...fileNames: string[]) => Promise<void>;

/**
* Helpers passed to hook functions
*/
export interface HookContext {
/**
* Callback function to replace placeholders on arbitrary files
*
* This makes token substitution available to non-`.template` files.
*/
readonly substitutePlaceholdersIn: SubstitutePlaceholders;

/**
* Return a single placeholder
*/
placeholder(name: string): string;
}

export type InvokeHook = (targetDirectory: string, context: HookContext) => Promise<void>;

/* eslint-disable @typescript-eslint/no-var-requires */ // Packages don't have @types module
// eslint-disable-next-line @typescript-eslint/no-require-imports
const camelCase = require('camelcase');
Expand Down Expand Up @@ -124,7 +104,9 @@ export class InitTemplate {

const sourceDirectory = path.join(this.basePath, language);

const hookContext: HookContext = {
await this.installFiles(sourceDirectory, targetDirectory, language, projectInfo);
await this.applyFutureFlags(targetDirectory);
await invokeBuiltinHooks({ targetDirectory, language, templateName: this.name }, {
substitutePlaceholdersIn: async (...fileNames: string[]) => {
for (const fileName of fileNames) {
const fullPath = path.join(targetDirectory, fileName);
Expand All @@ -133,11 +115,7 @@ export class InitTemplate {
}
},
placeholder: (ph: string) => this.expand(`%${ph}%`, language, projectInfo),
};

await this.installFiles(sourceDirectory, targetDirectory, language, projectInfo);
await this.applyFutureFlags(targetDirectory);
await this.invokeHooks(sourceDirectory, targetDirectory, hookContext);
});
}

private async installFiles(sourceDirectory: string, targetDirectory: string, language:string, project: ProjectInfo) {
Expand All @@ -160,27 +138,6 @@ export class InitTemplate {
}
}

/**
* @summary Invoke any javascript hooks that exist in the template.
* @description Sometimes templates need more complex logic than just replacing tokens. A 'hook' is
* any file that ends in .hook.js. It should export a single function called "invoke"
* that accepts a single string parameter. When the template is installed, each hook
* will be invoked, passing the target directory as the only argument. Hooks are invoked
* in lexical order.
*/
private async invokeHooks(sourceDirectory: string, targetDirectory: string, hookContext: HookContext) {
const files = await fs.readdir(sourceDirectory);
files.sort(); // Sorting allows template authors to control the order in which hooks are invoked.

for (const file of files) {
if (file.match(/^.*\.hook\.js$/)) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const invoke: InvokeHook = require(path.join(sourceDirectory, file)).invoke;
await invoke(targetDirectory, hookContext);
}
}
}

private async installProcessed(templatePath: string, toFile: string, language: string, project: ProjectInfo) {
const template = await fs.readFile(templatePath, { encoding: 'utf-8' });
await fs.writeFile(toFile, this.expand(template, language, project));
Expand Down

0 comments on commit d25cf01

Please sign in to comment.