Skip to content

Commit

Permalink
feat(create-astro): automatically configure astro check (#8853)
Browse files Browse the repository at this point in the history
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
  • Loading branch information
rayriffy and natemoo-re committed Oct 23, 2023
1 parent e3c18be commit ce807a2
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/ninety-onions-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-astro': minor
---

Automatically installs the required dependencies to run the astro check command when the user indicates they plan to write TypeScript.
105 changes: 76 additions & 29 deletions packages/create-astro/src/actions/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import type { Context } from './context.js';

import { color } from '@astrojs/cli-kit';
import fs from 'node:fs';
import { readFile } from 'node:fs/promises';
import { readFile, writeFile, rm } from 'node:fs/promises';
import path from 'node:path';
import stripJsonComments from 'strip-json-comments';
import { error, info, spinner, title, typescriptByDefault } from '../messages.js';
import { shell } from '../shell.js';

export async function typescript(
ctx: Pick<Context, 'typescript' | 'yes' | 'prompt' | 'dryRun' | 'cwd' | 'exit'>
) {
type PickedTypeScriptContext = Pick<
Context,
'typescript' | 'yes' | 'prompt' | 'dryRun' | 'cwd' | 'exit' | 'packageManager' | 'install'
>;

export async function typescript(ctx: PickedTypeScriptContext) {
let ts = ctx.typescript ?? (typeof ctx.yes !== 'undefined' ? 'strict' : undefined);
if (ts === undefined) {
const { useTs } = await ctx.prompt({
Expand Down Expand Up @@ -39,7 +42,7 @@ export async function typescript(
} else {
if (!['strict', 'strictest', 'relaxed', 'default', 'base'].includes(ts)) {
if (!ctx.dryRun) {
fs.rmSync(ctx.cwd, { recursive: true, force: true });
await rm(ctx.cwd, { recursive: true, force: true });
}
error(
'Error',
Expand All @@ -62,7 +65,7 @@ export async function typescript(
start: 'TypeScript customizing...',
end: 'TypeScript customized',
while: () =>
setupTypeScript(ts!, { cwd: ctx.cwd }).catch((e) => {
setupTypeScript(ts!, ctx).catch((e) => {
error('error', e);
process.exit(1);
}),
Expand All @@ -71,29 +74,73 @@ export async function typescript(
}
}

export async function setupTypeScript(value: string, { cwd }: { cwd: string }) {
const templateTSConfigPath = path.join(cwd, 'tsconfig.json');
try {
const data = await readFile(templateTSConfigPath, { encoding: 'utf-8' });
const templateTSConfig = JSON.parse(stripJsonComments(data));
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = Object.assign(templateTSConfig, {
extends: `astro/tsconfigs/${value}`,
});
const FILES_TO_UPDATE = {
'package.json': async (
file: string,
options: { value: string; ctx: PickedTypeScriptContext }
) => {
try {
// add required dependencies for astro check
if (options.ctx.install)
await shell(options.ctx.packageManager, ['install', '@astrojs/check', 'typescript'], {
cwd: path.dirname(file),
stdio: 'ignore',
});

fs.writeFileSync(templateTSConfigPath, JSON.stringify(result, null, 2));
} else {
throw new Error(
"There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed"
);
// inject addtional command to build script
const data = await readFile(file, { encoding: 'utf-8' });
const indent = /(^\s+)/m.exec(data)?.[1] ?? '\t';
const parsedPackageJson = JSON.parse(data);

const buildScript = parsedPackageJson.scripts?.build;

// in case of any other template already have astro checks defined, we don't want to override it
if (typeof buildScript === 'string' && !buildScript.includes('astro check')) {
const newPackageJson = Object.assign(parsedPackageJson, {
scripts: {
build: 'astro check && ' + buildScript,
},
});

await writeFile(file, JSON.stringify(newPackageJson, null, indent), 'utf-8');
}
} catch (err) {
// if there's no package.json (which is very unlikely), then do nothing
if (err && (err as any).code === 'ENOENT') return;
if (err instanceof Error) throw new Error(err.message);
}
} catch (err) {
if (err && (err as any).code === 'ENOENT') {
// If the template doesn't have a tsconfig.json, let's add one instead
fs.writeFileSync(
templateTSConfigPath,
JSON.stringify({ extends: `astro/tsconfigs/${value}` }, null, 2)
);
},
'tsconfig.json': async (file: string, options: { value: string }) => {
try {
const data = await readFile(file, { encoding: 'utf-8' });
const templateTSConfig = JSON.parse(stripJsonComments(data));
if (templateTSConfig && typeof templateTSConfig === 'object') {
const result = Object.assign(templateTSConfig, {
extends: `astro/tsconfigs/${options.value}`,
});

await writeFile(file, JSON.stringify(result, null, 2));
} else {
throw new Error(
"There was an error applying the requested TypeScript settings. This could be because the template's tsconfig.json is malformed"
);
}
} catch (err) {
if (err && (err as any).code === 'ENOENT') {
// If the template doesn't have a tsconfig.json, let's add one instead
await writeFile(
file,
JSON.stringify({ extends: `astro/tsconfigs/${options.value}` }, null, 2)
);
}
}
}
},
};

export async function setupTypeScript(value: string, ctx: PickedTypeScriptContext) {
await Promise.all(
Object.entries(FILES_TO_UPDATE).map(async ([file, update]) =>
update(path.resolve(path.join(ctx.cwd, file)), { value, ctx })
)
);
}
7 changes: 5 additions & 2 deletions packages/create-astro/test/fixtures/not-empty/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"name": "@test/create-astro-not-empty",
"private": true
}
"private": true,
"scripts": {
"build": "astro build"
}
}
2 changes: 1 addition & 1 deletion packages/create-astro/test/project-name.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { expect } from 'chai';
import { projectName } from '../dist/index.js';
import { setup } from './utils.js';

describe('project name', () => {
describe('project name', async () => {
const fixture = setup();

it('pass in name', async () => {
Expand Down
38 changes: 34 additions & 4 deletions packages/create-astro/test/typescript.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import fs from 'node:fs';
import { fileURLToPath } from 'node:url';

import { typescript, setupTypeScript } from '../dist/index.js';
import { setup } from './utils.js';
import { setup, resetFixtures } from './utils.js';
import { describe } from 'node:test';

describe('typescript', () => {
const fixture = setup();
Expand Down Expand Up @@ -82,7 +83,7 @@ describe('typescript', () => {
});
});

describe('typescript: setup', () => {
describe('typescript: setup tsconfig', () => {
it('none', async () => {
const root = new URL('./fixtures/empty/', import.meta.url);
const tsconfig = new URL('./tsconfig.json', root);
Expand All @@ -91,7 +92,8 @@ describe('typescript: setup', () => {
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({
extends: 'astro/tsconfigs/strict',
});
fs.rmSync(tsconfig);

await resetFixtures();
});

it('exists', async () => {
Expand All @@ -101,6 +103,34 @@ describe('typescript: setup', () => {
expect(JSON.parse(fs.readFileSync(tsconfig, { encoding: 'utf-8' }))).to.deep.eq({
extends: 'astro/tsconfigs/strict',
});
fs.writeFileSync(tsconfig, `{}`);

await resetFixtures();
});
});

describe('typescript: setup package', () => {
it('none', async () => {
const root = new URL('./fixtures/empty/', import.meta.url);
const packageJson = new URL('./package.json', root);

await setupTypeScript('strictest', { cwd: fileURLToPath(root), install: false });
expect(fs.existsSync(packageJson)).to.be.false;

await resetFixtures();
});

it('none', async () => {
const root = new URL('./fixtures/not-empty/', import.meta.url);
const packageJson = new URL('./package.json', root);

expect(
JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf-8' })).scripts.build
).to.be.eq('astro build');
await setupTypeScript('strictest', { cwd: fileURLToPath(root), install: false });
expect(JSON.parse(fs.readFileSync(packageJson, { encoding: 'utf-8' })).scripts.build).to.be.eq(
'astro check && astro build'
);

await resetFixtures();
});
});
27 changes: 27 additions & 0 deletions packages/create-astro/test/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import fs from 'node:fs';
import { setStdout } from '../dist/index.js';
import stripAnsi from 'strip-ansi';

Expand Down Expand Up @@ -29,3 +30,29 @@ export function setup() {
},
};
}

const resetEmptyFixture = () =>
fs.promises.rm(new URL('./fixtures/empty/tsconfig.json', import.meta.url));
const resetNotEmptyFixture = async () => {
const packagePath = new URL('./fixtures/not-empty/package.json', import.meta.url);
const tsconfigPath = new URL('./fixtures/not-empty/tsconfig.json', import.meta.url);

const overriddenPackageJson = Object.assign(
JSON.parse(await fs.promises.readFile(packagePath, { encoding: 'utf-8' })),
{
scripts: {
build: 'astro build',
},
}
);

return Promise.all([
fs.promises.writeFile(packagePath, JSON.stringify(overriddenPackageJson, null, 2), {
encoding: 'utf-8',
}),
fs.promises.writeFile(tsconfigPath, '{}', { encoding: 'utf-8' }),
]);
};

export const resetFixtures = () =>
Promise.allSettled([resetEmptyFixture(), resetNotEmptyFixture()]);

0 comments on commit ce807a2

Please sign in to comment.