Skip to content

Commit 401baf8

Browse files
committed
Integrate markup formatter to starter template and forge.
1 parent 56d13b3 commit 401baf8

34 files changed

Lines changed: 229 additions & 35 deletions

File tree

‎frontend/purestack.studio/guides/getting-started/purestack-cli.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Replace `my-site` with your folder name. Skipped installation? Run `yarn install
131131
| Command | What it does |
132132
| --- | --- |
133133
| `yarn dev` | Open your site locally. Saved changes reload in your browser. |
134+
| `yarn format` | Format your code, MDX components, and TypeScript templates. |
134135
| `yarn check` | Check code quality and formatting. |
135136
| `yarn test` | Run app tests while you work. |
136137
| `yarn build` | Generate the complete site in `dist/dev`. |

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": true,
33
"name": "purestack-repo",
4-
"version": "1.1.23",
4+
"version": "1.1.24",
55
"packageManager": "yarn@4.18.1",
66
"type": "module",
77
"sideEffects": false,

‎packages/purestack/forge/modules/components/setup-step.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const setupStepTemplate = html`<StepPanel
2020
:checked="workspace.install"
2121
tone="accent"/>
2222
<p class="text-subtle fs-xs mt-2 mb-0">
23-
Use the package manager configured by the template.
23+
Install dependencies, then format your project if the template provides
24+
a format script.
2425
</p>
2526
</div>
2627
<div>

‎packages/purestack/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "purestack",
3-
"version": "1.1.23",
3+
"version": "1.1.24",
44
"packageManager": "yarn@4.9.2",
55
"module": "./dist/purestack.mjs",
66
"types": "./dist/purestack.d.mts",

‎packages/purestack/src/cli/forge/session.test.ts‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import type {
1313
DevWebSocketConnection,
1414
DevWebSocketMessage,
1515
} from '@purestack/ts-ssg'
16-
import { describe, expect, it } from 'vitest'
16+
import { describe, expect, it, vi } from 'vitest'
1717
import type {
1818
SessionCommand,
1919
SessionEvent,
2020
} from '../../../forge/modules/shared/protocol'
2121
import { createSite } from '../../init/create-site'
22+
import * as managers from '../../init/package-manager'
2223
import { readTemplateUi } from '../../init/template-ui'
2324
import { parseInitArguments } from '../arguments'
2425
import { Session } from './session'
@@ -122,6 +123,51 @@ async function fixture(previewFails = false) {
122123
}
123124

124125
describe('Forge session', () => {
126+
it('streams formatting progress after installation and before starting the live site', async () => {
127+
const test = await fixture()
128+
const prepare = vi
129+
.spyOn(managers, 'preparePackageManager')
130+
.mockResolvedValue(undefined)
131+
const run = vi.spyOn(managers, 'runPackageManager').mockResolvedValue('')
132+
test.gate.resolve()
133+
try {
134+
test.peer.request({
135+
type: 'create',
136+
configuration: {
137+
...test.session.snapshot.configuration,
138+
install: true,
139+
},
140+
})
141+
await expect.poll(() => test.session.snapshot.status).toBe('ready')
142+
const tasks = test.session.snapshot.tasks
143+
const labels = tasks.map((task) => task.label)
144+
expect(labels.indexOf('Format project files')).toBeGreaterThan(
145+
labels.indexOf('Install dependencies'),
146+
)
147+
expect(labels.indexOf('Format project files')).toBeLessThan(
148+
labels.indexOf('Start live site'),
149+
)
150+
expect(tasks.every((task) => task.state === 'done')).toBe(true)
151+
expect(
152+
test.peer.events.some(
153+
(event) =>
154+
event.type === 'snapshot' &&
155+
event.snapshot.tasks.some(
156+
(task) =>
157+
task.label === 'Format project files' &&
158+
task.state === 'running',
159+
),
160+
),
161+
).toBe(true)
162+
expect(test.session.snapshot.result?.failures).toEqual([])
163+
expect(test.counts().previews).toBe(1)
164+
} finally {
165+
await test.close()
166+
prepare.mockRestore()
167+
run.mockRestore()
168+
}
169+
})
170+
125171
it('creates once, streams tasks, survives reconnects and keeps the live site open until finishing', async () => {
126172
const test = await fixture()
127173
try {

‎packages/purestack/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const version: string = '1.1.23'
1+
export const version: string = '1.1.24'
22

33
export * from '@purestack/ts-ssg'

‎packages/purestack/src/init/create-site.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export async function createSite(
6767
const temporary = await mkdtemp(path.join(tmpdir(), 'purestack-init-'))
6868
let template: string
6969
let packageManager: PackageManager
70+
let hasFormatScript: boolean
7071
try {
7172
const unpacked = path.join(temporary, 'unpacked')
7273
const templateRoot = options.templateRoot
@@ -104,6 +105,7 @@ export async function createSite(
104105
})
105106
template = `${project.name}@${project.version}`
106107
packageManager = project.packageManager
108+
hasFormatScript = project.hasFormatScript
107109
} finally {
108110
await rm(temporary, { recursive: true, force: true })
109111
}
@@ -141,6 +143,23 @@ export async function createSite(
141143
)
142144
}
143145
}
146+
if (result.installed && hasFormatScript) {
147+
try {
148+
await perform('Format project files', () =>
149+
runPackageManager(packageManager, ['run', 'format'], directory, {
150+
capture: true,
151+
signal: options.signal,
152+
}),
153+
)
154+
} catch (error) {
155+
if (error instanceof Error && error.name === 'AbortError') throw error
156+
const command =
157+
packageManager === 'yarn' ? 'yarn format' : 'npm run format'
158+
result.failures.push(
159+
`Project formatting failed: ${error instanceof Error ? error.message : String(error)}\nRetry ${command} in the project directory.`,
160+
)
161+
}
162+
}
144163
if (plan.git) {
145164
try {
146165
await perform('Set up Git', async () => {

‎packages/purestack/src/init/init.test.ts‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,122 @@ describe('project generation', () => {
562562
expect(result.failures).toEqual([
563563
expect.stringContaining('Retry yarn install'),
564564
])
565+
expect(install).toHaveBeenCalledTimes(1)
565566
expect(
566567
await readFile(path.join(destination, 'README.md'), 'utf8'),
567568
).toContain('PureStack')
568569
})
569570

571+
it.each(['yarn', 'npm'] as const)(
572+
'formats after installing with %s and before setting up Git',
573+
async (manager) => {
574+
const templateRoot = await fixture()
575+
if (manager === 'npm') {
576+
const file = path.join(templateRoot, 'template/package.json')
577+
const project = JSON.parse(await readFile(file, 'utf8'))
578+
delete project.packageManager
579+
await writeFile(file, JSON.stringify(project))
580+
}
581+
const destination = path.join(temporary, 'project')
582+
const run = vi.spyOn(managers, 'runPackageManager').mockResolvedValue('')
583+
vi.spyOn(processes, 'runProcess').mockResolvedValue(repository)
584+
const labels: string[] = []
585+
const signal = new AbortController().signal
586+
const result = await createSite(
587+
plan(destination, 'starter', { install: true, git: true }),
588+
cliVersion,
589+
repository,
590+
async (label, action) => {
591+
labels.push(label)
592+
return action()
593+
},
594+
{ templateRoot, signal },
595+
)
596+
expect(result.installed).toBe(true)
597+
expect(result.failures).toEqual([])
598+
expect(run.mock.calls.map((call) => call[1])).toEqual([
599+
manager === 'yarn' ? ['install', '--no-immutable'] : ['install'],
600+
['run', 'format'],
601+
])
602+
expect(run).toHaveBeenLastCalledWith(
603+
manager,
604+
['run', 'format'],
605+
destination,
606+
{ capture: true, signal },
607+
)
608+
expect(labels.slice(-3)).toEqual([
609+
'Install dependencies',
610+
'Format project files',
611+
'Set up Git',
612+
])
613+
},
614+
)
615+
616+
it.each([undefined, ' '])(
617+
'skips formatting when the template has no usable format script (%s)',
618+
async (format) => {
619+
const templateRoot = await fixture()
620+
const file = path.join(templateRoot, 'template/package.json')
621+
const project = JSON.parse(await readFile(file, 'utf8'))
622+
project.scripts.format = format
623+
await writeFile(file, JSON.stringify(project))
624+
const run = vi.spyOn(managers, 'runPackageManager').mockResolvedValue('')
625+
const result = await createSite(
626+
plan(path.join(temporary, 'project'), 'starter', { install: true }),
627+
cliVersion,
628+
repository,
629+
undefined,
630+
{ templateRoot },
631+
)
632+
expect(result.installed).toBe(true)
633+
expect(result.failures).toEqual([])
634+
expect(run).toHaveBeenCalledTimes(1)
635+
},
636+
)
637+
638+
it('preserves the installed project and continues Git setup when formatting fails', async () => {
639+
const destination = path.join(temporary, 'project')
640+
vi.spyOn(managers, 'runPackageManager')
641+
.mockResolvedValueOnce('')
642+
.mockRejectedValueOnce(new Error('Invalid markup'))
643+
vi.spyOn(processes, 'runProcess').mockResolvedValue(repository)
644+
const result = await createSite(
645+
plan(destination, 'starter', { install: true, git: true }),
646+
cliVersion,
647+
repository,
648+
undefined,
649+
{ templateRoot: await fixture() },
650+
)
651+
expect(result.installed).toBe(true)
652+
expect(result.git).toBe('existing')
653+
expect(result.failures).toEqual([
654+
'Project formatting failed: Invalid markup\nRetry yarn format in the project directory.',
655+
])
656+
expect(await readdir(destination)).toContain('README.md')
657+
})
658+
659+
it('propagates formatting cancellation and does not start Git setup', async () => {
660+
const cancelled = new Error('Cancelled')
661+
cancelled.name = 'AbortError'
662+
vi.spyOn(managers, 'runPackageManager')
663+
.mockResolvedValueOnce('')
664+
.mockRejectedValueOnce(cancelled)
665+
const git = vi.spyOn(processes, 'runProcess')
666+
await expect(
667+
createSite(
668+
plan(path.join(temporary, 'project'), 'starter', {
669+
install: true,
670+
git: true,
671+
}),
672+
cliVersion,
673+
repository,
674+
undefined,
675+
{ templateRoot: await fixture() },
676+
),
677+
).rejects.toBe(cancelled)
678+
expect(git).not.toHaveBeenCalled()
679+
})
680+
570681
it('skips Git initialization inside an existing repository', async () => {
571682
const archive = await archiveFixture()
572683
vi.spyOn(console, 'log').mockImplementation(() => {})

‎packages/purestack/src/init/template.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ export async function prepareProject(
357357
name: pkg.name,
358358
version: pkg.version,
359359
packageManager,
360+
hasFormatScript:
361+
typeof scripts.format === 'string' && scripts.format.trim().length > 0,
360362
}
361363
}
362364

‎packages/ts-common/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@purestack/ts-common",
3-
"version": "1.1.23",
3+
"version": "1.1.24",
44
"packageManager": "yarn@4.9.2",
55
"module": "./dist/ts-common.mjs",
66
"types": "./dist/ts-common.d.mts",

0 commit comments

Comments
 (0)