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(create-astro): add astro checks to build script when user choose TypeScript #8853

Merged
merged 13 commits into from
Oct 23, 2023
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 setup `astro check` command when configuring TypeScript
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Automatically setup `astro check` command when configuring TypeScript
Automatically installs the required dependencies to run the `astro check` command when `strict` or `strictest`
TypeScript mode is selected.

Since running astro check requires both @astrojs/check and typescript, maybe makes sense to say "required dependencies" (plural) here, because these will be added to your project?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree. But I have to adjust some wording at the end because astro check will be applied to all TypeScript options (relaxed, strict, strictest).

Let me know if this change looks good to you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I commented above to clarify that this happens on relaxed (which seems now far removed from the PR title itself, and maybe installs a dependency for people who won't really use it?) But, wording should be fine.

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 });
rm(ctx.cwd, { recursive: true, force: true });
rayriffy marked this conversation as resolved.
Show resolved Hide resolved
}
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()]);
Loading