Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support Deno as a package manager #2491

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
31 changes: 19 additions & 12 deletions src/bin/commands/new-project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { constructStack } from "./utils/stack";
import type { Name } from "./utils/utils";
import { getCommands, pascalCase } from "./utils/utils";

export type PackageManager = "npm" | "yarn";
export type PackageManager = "npm" | "yarn" | "deno";

interface NewProjectProps {
init: boolean;
Expand Down Expand Up @@ -80,7 +80,7 @@ function getConfig(props: NewProjectProps): NewProjectConfig {
kebab: kebabAppName,
pascal: appName,
},
appPath: `${cdkDir}/bin/${kebabAppName}.ts`,
appPath: packageManager === "deno" ? `${cdkDir}/main.ts` : `${cdkDir}/bin/${kebabAppName}.ts`,
stackName: {
kebab: kebabStackName,
pascal: stackName,
Expand All @@ -101,7 +101,7 @@ export const newCdkProject = async (props: NewProjectProps): CliCommandResponse
const config = getConfig(props);

if (config.init) {
buildDirectory({ outputDir: config.cdkDir });
buildDirectory({ outputDir: config.cdkDir, packageManager: config.packageManager });
}

console.log(`New app ${config.appName.pascal} will be written to ${config.appPath}`);
Expand All @@ -110,10 +110,10 @@ export const newCdkProject = async (props: NewProjectProps): CliCommandResponse
// bin directory
await constructApp({
appName: config.appName,
outputFile: "cdk.ts",
outputFile: config.packageManager === "deno" ? "main.ts" : "cdk.ts",
outputDir: dirname(config.appPath),
stack: config.stackName,
imports: Imports.newAppImports(config.appName),
imports: Imports.newAppImports({ name: config.appName, packageManager: config.packageManager }),
stages: config.stages,
regions: config.regions,
});
Expand All @@ -129,11 +129,12 @@ export const newCdkProject = async (props: NewProjectProps): CliCommandResponse

// lib directory
await constructTest({
imports: Imports.newTestImports(config.appName),
imports: Imports.newTestImports({ name: config.appName, packageManager: config.packageManager }),
stackName: config.stackName,
appName: config.appName,
outputFile: basename(config.testPath),
outputDir: dirname(config.stackPath),
packageManager: config.packageManager,
});

const commands = getCommands(config.packageManager, config.cdkDir);
Expand All @@ -145,13 +146,19 @@ export const newCdkProject = async (props: NewProjectProps): CliCommandResponse
}

// Run `eslint --fix` on the generated files instead of trying to generate code that completely satisfies the linter
await execute(
"./node_modules/.bin/eslint",
["lib/** bin/**", "--ext .ts", "--no-error-on-unmatched-pattern", "--fix"],
{
if (config.packageManager === "deno") {
await execute("deno lint", ["--fix"], {
cwd: config.cdkDir,
},
);
});
} else {
await execute(
"./node_modules/.bin/eslint",
["lib/** bin/**", "--ext .ts", "--no-error-on-unmatched-pattern", "--fix"],
{
cwd: config.cdkDir,
},
);
}

ux.action.start(chalk.yellow("Running lint check..."));
await commands.lint();
Expand Down
35 changes: 31 additions & 4 deletions src/bin/commands/new-project/utils/imports.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CodeMaker } from "codemaker";
import type { PackageManager } from "..";
import type { Name } from "./utils";

interface Import {
Expand Down Expand Up @@ -37,7 +38,16 @@ export class Imports {
});
}

public static newAppImports({ kebab, pascal }: Name): Imports {
public static newAppImports({
name: { kebab, pascal },
packageManager,
}: {
name: Name;
packageManager: PackageManager;
}): Imports {
// Deno requires local imports to specify file extensions
// https://docs.deno.com/runtime/fundamentals/modules
const fileExtension = packageManager === "deno" ? ".ts" : "";
return new Imports({
"source-map-support/register": {
basic: true,
Expand All @@ -48,14 +58,23 @@ export class Imports {
types: [],
components: ["GuRoot"],
},
[`../lib/${kebab}`]: {
[`../lib/${kebab}${fileExtension}`]: {
types: [],
components: [pascal],
},
});
}

public static newTestImports({ kebab, pascal }: Name): Imports {
public static newTestImports({
name: { kebab, pascal },
packageManager,
}: {
name: Name;
packageManager: PackageManager;
}): Imports {
// Deno requires local imports to specify file extensions
// https://docs.deno.com/runtime/fundamentals/modules
const fileExtension = packageManager === "deno" ? ".ts" : "";
return new Imports({
"aws-cdk-lib": {
types: [],
Expand All @@ -65,10 +84,18 @@ export class Imports {
types: [],
components: ["Template"],
},
[`./${kebab}`]: {
[`./${kebab}${fileExtension}`]: {
types: [],
components: [pascal],
},
...(packageManager === "deno"
? {
"@std/testing/snapshot": {
types: [],
components: ["assertSnapshot"],
},
}
: {}),
});
}

Expand Down
74 changes: 69 additions & 5 deletions src/bin/commands/new-project/utils/init.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, writeFileSync } from "fs";
import { join } from "path";
import type { PackageManager } from "..";
import { getDevDependency, LibraryInfo } from "../../../../constants";

export interface InitConfig {
outputDir: string;
packageManager: PackageManager;
}

// TODO: Add project name flag
Expand All @@ -30,28 +32,55 @@ export class ProjectBuilder {
mkdirSync(this.config.outputDir);
}

console.log("Creating package.json");
createPackageJson(this.config.outputDir);
if (this.config.packageManager === "deno") {
console.log("Creating deno.json");
createDenoJson(this.config.outputDir);
} else {
console.log("Creating package.json");
createPackageJson(this.config.outputDir);
}

console.log("Copying template files");
// TODO: Replace any params in files with .template extensions
this.copyFiles(this.templateDir, this.config.outputDir);
this.copyFiles({
sourcePath: this.templateDir,
targetPath: this.config.outputDir,
packageManager: this.config.packageManager,
});

console.log("Success!");
}

copyFiles(sourcePath: string, targetPath: string): void {
copyFiles({
sourcePath,
targetPath,
packageManager,
}: {
sourcePath: string;
targetPath: string;
packageManager: string;
}): void {
const denoIgnore = ["bin", "jest.setup.js", "tsconfig.json.template"];

for (const file of readdirSync(sourcePath)) {
const path = join(sourcePath, file);

if (packageManager === "deno" && denoIgnore.includes(file)) {
continue;
}

if (path.endsWith(".ignore")) {
continue;
} else if (lstatSync(path).isDirectory()) {
const nestedTargetPath = join(targetPath, file);
if (!existsSync(nestedTargetPath)) {
mkdirSync(nestedTargetPath);
}
this.copyFiles(path, nestedTargetPath);
this.copyFiles({
sourcePath: path,
targetPath: nestedTargetPath,
packageManager,
});
} else if (path.endsWith(".template")) {
copyFileSync(path, join(targetPath, file.replace(".template", "")));
} else {
Expand Down Expand Up @@ -154,3 +183,38 @@ function createPackageJson(outputDirectory: string): void {
};
writeFileSync(`${outputDirectory}/package.json`, JSON.stringify(contents, null, 2));
}

function createDenoJson(outputDirectory: string): void {
const { NODE_ENV, CI } = process.env;
const isTest = NODE_ENV?.toUpperCase() === "TEST" || CI?.toUpperCase() === "TRUE";

const deps: Record<string, string> = {
/*
Do not add `@guardian/cdk` to the generated `package.json` file when in TEST as we'll `npm link` it instead.
See https://docs.npmjs.com/cli/v8/commands/npm-link#caveat

TODO remove this once the `new` command allows opting out of automatic dependency installation
*/
...(!isTest && { "@guardian/cdk": `npm:@guardian/cdk@${LibraryInfo.VERSION}` }),

"@std/testing": "jsr:@std/[email protected]",
"aws-cdk": `npm:aws-cdk@${LibraryInfo.AWS_CDK_VERSION}`,
"aws-cdk-lib": `npm:aws-cdk-lib@${LibraryInfo.AWS_CDK_VERSION}`,
constructs: `npm:constructs@${LibraryInfo.CONSTRUCTS_VERSION}`,
};

const imports: Record<string, string> = Object.keys(deps)
.sort()
.reduce((acc, depName) => ({ ...acc, [depName]: deps[depName] }), {});

const contents = {
tasks: {
test: "deno test --allow-env=TMPDIR --allow-read --allow-write --allow-sys",
"test-update": "deno test --allow-env=TMPDIR --allow-read --allow-write --allow-sys -- -u",
synth: "cdk synth --path-metadata false --version-reporting false",
diff: "cdk diff --path-metadata false --version-reporting false",
},
imports,
};
writeFileSync(`${outputDirectory}/deno.json`, JSON.stringify(contents, null, 2));
}
20 changes: 19 additions & 1 deletion src/bin/commands/new-project/utils/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PackageManager } from "aws-sdk/clients/ecr";
import { CodeMaker } from "codemaker";
import type { Imports } from "./imports";
import type { Name } from "./utils";
Expand All @@ -9,6 +10,7 @@ export interface TestBuilderProps {
outputFile: string;
outputDir: string;
comment?: string;
packageManager: PackageManager;
}

export class TestBuilder {
Expand All @@ -34,7 +36,7 @@ export class TestBuilder {

this.config.imports.render(this.code);

this.addTest();
this.config.packageManager === "deno" ? this.addTestDeno() : this.addTest();

this.code.closeFile(this.config.outputFile);
await this.code.save(this.config.outputDir);
Expand All @@ -57,6 +59,22 @@ export class TestBuilder {
this.code.closeBlock("});");
this.code.closeBlock("});");
}

addTestDeno(): void {
const { appName, stackName } = this.config;

this.code.openBlock(`Deno.test("The ${appName.pascal} stack matches the screenshot", async (ctx) =>`);

this.code.line("const app = new App();");
this.code.line(
`const stack = new ${appName.pascal}(app, "${appName.pascal}", { stack: "${stackName.kebab}", stage: "TEST" });`,
);

this.code.line(`const template = Template.fromStack(stack);`);
this.code.line(`await assertSnapshot(ctx, template.toJSON());`);

this.code.closeBlock("});");
}
}

export const constructTest = async (props: TestBuilderProps): Promise<void> => {
Expand Down
6 changes: 4 additions & 2 deletions src/bin/commands/new-project/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ export function getCommands(
synth: () => Promise<string>;
test: () => Promise<string>;
} {
const isDeno = packageManager === "deno";

return {
installDependencies: () => runTask(packageManager, cwd, "install"),
lint: () => runTask(packageManager, cwd, "run lint"),
test: () => runTask(packageManager, cwd, "test -- -u"),
lint: () => runTask(packageManager, cwd, isDeno ? "lint" : "run lint"),
test: () => runTask(packageManager, cwd, isDeno ? "run test -- -u" : "test -- -u"),
synth: () => runTask(packageManager, cwd, "run synth"),
};
}
2 changes: 1 addition & 1 deletion src/bin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const parseCommandLineArguments = () => {
.option("package-manager", {
description:
"The Node package manager to use. Match this to the repository (package-lock.json = npm, yarn.lock = yarn). If the repository has neither file, and there is no strong convention in your team, we recommend npm.",
choices: ["npm", "yarn"],
choices: ["npm", "yarn", "deno"],
demandOption: true,
}),
)
Expand Down